Source code for ezgpx.writers.kml_writer

"""
This module contains the KMLWriter class.
"""

import errno
import os
import warnings
import xml.etree.ElementTree as ET
from typing import Optional

from .writer import GPXLike, Writer

DEFAULT_NORMAL_STYLE = {"color": "ff0000ff", "width": 2, "fill": 0}

DEFAULT_HIGHLIGHT_STYLE = {"color": "ff0000ff", "width": 2, "fill": 0}

DEFAULT_STYLES = [
    ("normal", DEFAULT_NORMAL_STYLE),
    ("highlight", DEFAULT_HIGHLIGHT_STYLE),
]


[docs] class KMLWriter(Writer): """ KML file writer. """ def __init__( self, gpx: GPXLike = None, *, properties: bool = True, metadata: bool = True, # unused waypoints: bool = True, routes: bool = True, extensions: bool = True, ele: bool = True, time: bool = True, styles: list[tuple[str, dict]] = None, ) -> None: """ Initialise GPXWriter instance. """ super().__init__(gpx) # Parameters self.properties: bool = properties self.metadata: bool = metadata self.waypoints: bool = waypoints self.routes: bool = routes self.extensions: bool = extensions self.ele: bool = ele self.time: bool = time self.styles = DEFAULT_STYLES if styles is None else styles # Utility attributes self.kml_string: str = "" self.kml_root = None
[docs] def add_pair(self, element: ET.Element, key: str, style_url: str) -> ET.Element: """ Add StyleMap to KML element. Args: element (ET.Element): KML element. key (str): Key. style_url (str): Style URL. Returns: ET.Element: KML element. """ pair_ = ET.SubElement(element, "Pair") pair_, _ = self.add_subelement(pair_, "key", key) pair_, _ = self.add_subelement(pair_, "styleUrl", style_url) return element
[docs] def add_stylemap(self, element: ET.Element, id_: str) -> ET.Element: """ Add StyleMap to KML element. Args: element (ET.Element): KML element. id_ (str): StyleMap element id. Returns: ET.Element: KML element. """ stylemap_ = ET.SubElement(element, "StyleMap") self.set_not_none(stylemap_, "id", id_) style_id = 1 for style_key, _ in self.styles: # _ used to be called style stylemap_ = self.add_pair(stylemap_, style_key, "#style" + str(style_id)) style_id += 1 return element
[docs] def add_polystyle(self, element: ET.Element, style: dict) -> ET.Element: """ Add PolyStyle to KML element. Args: element (ET.Element): KML element. style (dict): Style. Returns: ET.Element: KML element. """ polystyle_ = ET.SubElement(element, "PolyStyle") try: polystyle_, _ = self.add_subelement_number( polystyle_, "fill", style["fill"] ) except KeyError: warnings.warn("No fill attribute in style") polystyle_, _ = self.add_subelement_number(polystyle_, "fill", 0) return element
[docs] def add_linestyle(self, element: ET.Element, style: dict) -> ET.Element: """ Add LineStyle to KML element. Args: element (ET.Element): KML element. style (dict): Style. Returns: ET.Element: KML element. """ linestyle_ = ET.SubElement(element, "LineStyle") try: linestyle_, _ = self.add_subelement(linestyle_, "color", style["color"]) except KeyError: warnings.warn("No color attribute in style") linestyle_, _ = self.add_subelement(linestyle_, "color", "ff0000ff") try: linestyle_, _ = self.add_subelement_number( linestyle_, "width", style["width"] ) except KeyError: warnings.warn("No width attribute in style") linestyle_, _ = self.add_subelement_number(linestyle_, "width", 2) return element
[docs] def add_style( self, element: ET.Element, id: str, # pylint: disable=redefined-builtin style: dict, ) -> ET.Element: """ Add Style to KML element. Args: element (ET.Element): KML element. id (str): Style element id. style (dict): Line style. Returns: ET.Element: KML element. """ style_ = ET.SubElement(element, "Style") self.set_not_none(style_, "id", id) style_ = self.add_linestyle(style_, style) style_ = self.add_polystyle(style_, style) return element
[docs] def add_linestring(self, element: ET.Element) -> ET.Element: """ Add LineString to KML element. Args: element (ET.Element): KML element. Returns: ET.Element: KML element. """ linestring_ = ET.SubElement(element, "LineString") linestring_, _ = self.add_subelement_number(linestring_, "tessellate", 1) if self.ele: coordinates = self.gpx.to_csv( dest=None, values=["lon", "lat", "ele"], include_header=False, ).replace("\n", " ") else: coordinates = self.gpx.to_csv( dest=None, values=["lon", "lat"], include_header=False, ).replace("\n", " ") linestring_, _ = self.add_subelement(linestring_, "coordinates", coordinates) return element
[docs] def add_placemark(self, element: ET.Element) -> ET.Element: """ Add Placemark to KML element. Args: element (ET.Element): KML element. Returns: ET.Element: KML element. """ name = None if self.gpx.gpx.metadata.name: name = self.gpx.gpx.metadata.name elif self.gpx.gpx.trk: if self.gpx.gpx.trk[0].name: name = self.gpx.gpx.trk[0].name placemark_ = ET.SubElement(element, "Placemark") if name: placemark_, _ = self.add_subelement(placemark_, "name", name) placemark_, _ = self.add_subelement(placemark_, "styleUrl", "#stylemap") placemark_ = self.add_linestring(placemark_) return element
[docs] def add_document(self, element: ET.Element) -> ET.Element: """ Add Document to KML element. Args: element (ET.Element): KML element. Returns: ET.Element: KML element. """ document_ = ET.SubElement(element, "Document") document_, _ = self.add_subelement( document_, "name", os.path.basename(self.file_path) ) id_ = 1 for _, style in self.styles: # _ used to be called style_key document_ = self.add_style(document_, "style" + str(id), style) id_ += 1 document_ = self.add_stylemap(document_, "stylemap") document_ = self.add_placemark(document_) return element
[docs] def add_root_document(self) -> None: """ Add Document element to the KML root element. """ self.kml_root = self.add_document(self.kml_root)
[docs] def add_root_properties(self) -> None: """ Add properties to the GPX root element. """ # According to .kml file from Google Earth Pro self.kml_root.set("xmlns", "http://www.opengis.net/kml/2.2") self.kml_root.set("xmlns:gx", "http://www.google.com/kml/ext/2.2") self.kml_root.set("xmlns:kml", "http://www.opengis.net/kml/2.2") self.kml_root.set("xmlns:atom", "http://www.w3.org/2005/Atom")
[docs] def gpx_to_string(self) -> str | None: """ Convert Gpx instance to a string (the content of a .kml file). Returns: str | None: String corresponding to the Gpx instance. """ if self.gpx is None: return None # Reset string self.kml_string = "" # Root self.kml_root = ET.Element("kml") # Properties if self.properties: self.add_root_properties() # Document self.add_root_document() # Convert data to string self.kml_string = ET.tostring(self.kml_root, encoding="unicode") return self.kml_string
[docs] def write_gpx(self): """ Convert Gpx instance to string and write to .kml file. """ # Open/create KML file try: f = open(self.file_path, "w", encoding="utf-8") except OSError as e: raise OSError(f"Could not open or read file: {self.file_path}") from e # Write KML file with f: f.write('<?xml version="1.0" encoding="UTF-8"?>') f.write(self.kml_string)
[docs] def write( self, file_path: str, styles: Optional[list[tuple[str, dict]]] = None, ) -> None: """ Handle writing. Args: file_path (str): Path to write the KML file. styles (Optional[list[tuple[str, dict]]], optional): List of (style_id, style) tuples. Defaults to None. Returns: bool: Return True if written file follows checked schemas, False otherwise. """ directory_path = os.path.dirname(os.path.realpath(file_path)) if not os.path.exists(directory_path): raise NotADirectoryError( errno.ENOENT, os.strerror(errno.ENOENT), directory_path ) self.file_path = file_path # Update style if styles is not None: self.styles = styles # Write .kml file self.gpx_to_string() self.write_gpx()