"""
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()