Source code for ezgpx.writers.gpx_writer

"""
This module contains the GPXWriter class.
"""

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

from ..complex_types import (
    Bounds,
    Copyright,
    Email,
    Extensions,
    Gpx,
    Link,
    Metadata,
    Person,
    Pt,
    Ptseg,
    Rte,
    Trk,
    Trkseg,
    Wpt,
)
from .gpx_writer_method_creator import GPXWriterMethodCreator
from .writer import GPXLike, Writer


[docs] class GPXWriter(Writer): """ GPX file writer. """ def __init__(self, gpx: GPXLike) -> None: """ Initialise GPXWriter instance. Args: gpx (GPXLike): GPX instance to write. """ super().__init__(gpx) # Utility attributes self.gpx_string: str = "" self.gpx_root = None # Methods behavior creator self.method_creator: GPXWriterMethodCreator = GPXWriterMethodCreator() # Methods behaviors self._add_bounds = self.placeholder_method self._add_copyright = self.placeholder_method self._add_email = self.placeholder_method self._add_link = self.placeholder_method self._add_metadata = self.placeholder_method self._add_person = self.placeholder_method self._add_ptseg = self.placeholder_method self._add_pt = self.placeholder_method self._add_rte = self.placeholder_method self._add_trkseg = self.placeholder_method self._add_trk = self.placeholder_method self._add_wpt = self.placeholder_method self._add_trkpt = self.placeholder_method # Fields self.properties: bool = None self.bounds_fields: list[str] = None self.copyright_fields: list[str] = None self.email_fields: list[str] = None self.extensions_fields: dict = None self.gpx_fields: list[str] = None self.link_fields: list[str] = None self.metadata_fields: list[str] = None self.person_fields: list[str] = None self.ptseg_fields: list[str] = None self.pt_fields: list[str] = None self.rte_fields: list[str] = None self.trkseg_fields: list[str] = None self.trk_fields: list[str] = None self.wpt_fields: list[str] = None self.trkpt_fields: list[str] = None
[docs] def placeholder_method(self, element, subelement): """ Placeholder function for adding subelement to element in XML tree. """
[docs] def add_bounds(self, element: ET.Element, bounds: Bounds) -> ET.Element: """ Add Bounds instance to GPX element. Args: element (ET.Element): GPX element. bounds (Bounds): Bounds instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_bounds(self, element, bounds)
[docs] def add_email(self, element: ET.Element, email: Email) -> ET.Element: """ Add Email instance element to GPX element. Args: element (ET.Element): GPX element. email (Email): Email instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_email(self, element, email)
def _add_extensions_rec( self, extensions_: ET.Element, values: dict, extensions_fields: dict ) -> ET.Element: if extensions_ is not None: # Add n-th level extensions for k0, v0 in values.items(): if k0 in extensions_fields.keys(): # Set attributes for k1, v1 in v0["attrib"].items(): if k1 in extensions_fields[k0]["attrib"].keys(): self.set_not_none(extensions_, k1, v1) # If k0 contains sub-elements if isinstance(v0["elmts"], dict): sub_extensions_ = ET.SubElement( extensions_, k0 ) # Create sub-element sub_extensions_ = self._add_extensions_rec( sub_extensions_, v0["elmts"], extensions_fields[k0]["elmts"] ) # Add (n+1)-th level extensions # Else, k0 contains a value else: extensions_, sub_extensions_ = self.add_subelement( extensions_, k0, v0["elmts"] ) return extensions_
[docs] def add_extensions( self, element: ET.Element, extensions: Extensions, extensions_fields: dict ) -> ET.Element: """ Add Extensions instance element to GPX element. Args: element (ET.Element): GPX element. extensions (Extensions): Extensions instance to add. extensions_fields (dict): Extensions fields to add. Returns: ET.Element: GPX element. """ if extensions is not None and extensions.values is not None: extensions_ = ET.SubElement(element, extensions.tag) extensions_ = self._add_extensions_rec( extensions_, extensions.values, extensions_fields ) return element
[docs] def add_metadata(self, element: ET.Element, metadata: Metadata) -> ET.Element: """ Add Metadata instance element to GPX element. Args: element (ET.Element): GPX element. metadata (Metadata): Metadata instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_metadata(self, element, metadata)
[docs] def add_person(self, element: ET.Element, person: Person) -> ET.Element: """ Add Person instance element to GPX element. Args: element (ET.Element): GPX element. person (Person): Person instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_person(self, element, person)
[docs] def add_ptseg(self, element: ET.Element, pt_segment: Ptseg) -> ET.Element: """ Add Ptseg instance element to GPX element. Args: element (ET.Element): GPX element. pt_segment (Ptseg): Ptseg instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_ptseg(self, element, pt_segment)
[docs] def add_pt(self, element: ET.Element, pt: Pt) -> ET.Element: """ Add Pt instance element to GPX element. Args: element (ET.Element): GPX element. pt (Pt): Pt instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_pt(self, element, pt)
[docs] def add_rte(self, element: ET.Element, rte: Rte) -> ET.Element: """ Add Rte instance element to GPX element. Args: element (ET.Element): GPX element. rte (Rte): Rte instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_rte(self, element, rte)
[docs] def add_trkseg(self, element: ET.Element, trkseg: Trkseg) -> ET.Element: """ Add Trkseg instance element to GPX element. Args: element (ET.Element): GPX element. trkseg (Trkseg): Trkseg instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_trkseg(self, element, trkseg)
[docs] def add_trk(self, element: ET.Element, trk: Trk) -> ET.Element: """ Add Trk instance element to GPX element. Args: element (ET.Element): GPX element. trk (Trk): Trk instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_trk(self, element, trk)
[docs] def add_wpt(self, element: ET.Element, wpt: Wpt) -> ET.Element: """ Add Wpt instance element to GPX element. Args: element (ET.Element): GPX element. wpt (Wpt): Wpt instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_wpt(self, element, wpt)
[docs] def add_trkpt(self, element: ET.Element, wpt: Wpt) -> ET.Element: """ Add Wpt instance (with trkpt tag) element to GPX element. Args: element (ET.Element): GPX element. wpt (Wpt): Wpt instance to add. Returns: xml.etree.ElementTree.Element: GPX element. """ return self._add_trkpt(self, element, wpt)
[docs] def add_root_properties(self) -> None: """ Add properties to the GPX root element and register name spaces. """ self.set_not_none(self.gpx_root, "version", self.gpx.gpx.version) self.set_not_none(self.gpx_root, "creator", "ezGPX") for k, v in self.gpx.xmlns.items(): ET.register_namespace(k, v) # prefix, URI if k in ["", "xsi"]: self.set_not_none( self.gpx_root, f"xmlns:{k}" if k != "" else "xmlns", v ) self.set_not_none( self.gpx_root, "xsi:schemaLocation", " ".join(f"{k} {v}" for k, v in self.gpx.xsi_schema_location.items()), )
[docs] def gpx_to_string(self) -> str | None: """ Convert Gpx instance to a string (the content of a .gpx file). Returns: str | None: String corresponding to the Gpx instance. """ if self.gpx is None: return None # Reset string self.gpx_string = "" # Root self.gpx_root = ET.Element("gpx") # Properties self.add_root_properties() # Metadata if self.metadata_fields: self.gpx_root = self.add_metadata(self.gpx_root, self.gpx.gpx.metadata) # Way points if self.wpt_fields: for wpt in self.gpx.gpx.wpt: self.gpx_root = self.add_wpt(self.gpx_root, wpt) # Rtes if self.rte_fields: for rte in self.gpx.gpx.rte: self.gpx_root = self.add_rte(self.gpx_root, rte) # Trks if self.trk_fields: for trk in self.gpx.gpx.trk: self.gpx_root = self.add_trk(self.gpx_root, trk) # Extensions if self.extensions_fields.get("gpx"): if self.gpx.gpx.extensions is not None: self.gpx_root = self.add_extensions( self.gpx_root, self.gpx.gpx.extensions, self.extensions_fields.get("gpx"), ) # Convert data to string self.gpx_string = ET.tostring(self.gpx_root, encoding="unicode") # self.gpx_string = ET.tostring(gpx_root, encoding="utf-8") return self.gpx_string
[docs] def write( self, file_path: str, *, bounds_fields: Optional[list[str]] = None, copyright_fields: Optional[list[str]] = None, email_fields: Optional[list[str]] = None, extensions_fields: Optional[dict] = None, gpx_fields: Optional[list[str]] = None, link_fields: Optional[list[str]] = None, metadata_fields: Optional[list[str]] = None, person_fields: Optional[list[str]] = None, ptseg_fields: Optional[list[str]] = None, pt_fields: Optional[list[str]] = None, rte_fields: Optional[list[str]] = None, trkseg_fields: Optional[list[str]] = None, trk_fields: Optional[list[str]] = None, wpt_fields: Optional[list[str]] = None, trkpt_fields: Optional[list[str]] = None, mandatory_fields: bool = True, ) -> None: """ TODO """ 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 # Set parameters self.bounds_fields = Bounds._fields if bounds_fields is None else bounds_fields self.copyright_fields = ( Copyright._fields if copyright_fields is None else copyright_fields ) self.email_fields = Email._fields if email_fields is None else email_fields self.extensions_fields = {} if extensions_fields is None else extensions_fields self.gpx_fields = Gpx._fields if gpx_fields is None else gpx_fields self.link_fields = Link._fields if link_fields is None else link_fields self.metadata_fields = ( Metadata._fields if metadata_fields is None else metadata_fields ) self.person_fields = Person._fields if person_fields is None else person_fields self.ptseg_fields = Ptseg._fields if ptseg_fields is None else ptseg_fields self.pt_fields = Pt._fields if pt_fields is None else pt_fields self.rte_fields = Rte._fields if rte_fields is None else rte_fields self.trkseg_fields = Trkseg._fields if trkseg_fields is None else trkseg_fields self.trk_fields = Trk._fields if trk_fields is None else trk_fields self.wpt_fields = Wpt._fields if wpt_fields is None else wpt_fields self.trkpt_fields = Wpt._fields if trkpt_fields is None else trkpt_fields # Check mandatory fields if mandatory_fields: def check_mandatory_fields(element, fields, mandatory_fields): if any(f not in fields for f in mandatory_fields): warnings.warn( f"{element} element must have following fields: {mandatory_fields}" "Missing mandatory fields will automatically be added." ) fields = set(fields + mandatory_fields) return fields self.bounds_fields = check_mandatory_fields( "Bounds", self.bounds_fields, Bounds._mandatory_fields ) self.copyright_fields = check_mandatory_fields( "Copyright", self.copyright_fields, Copyright._mandatory_fields ) self.email_fields = check_mandatory_fields( "Email", self.email_fields, Email._mandatory_fields ) self.gpx_fields = check_mandatory_fields( "Gpx", self.gpx_fields, Gpx._mandatory_fields ) self.link_fields = check_mandatory_fields( "Link", self.link_fields, Link._mandatory_fields ) self.metadata_fields = check_mandatory_fields( "Metadata", self.metadata_fields, Metadata._mandatory_fields ) self.person_fields = check_mandatory_fields( "Person", self.person_fields, Person._mandatory_fields ) self.ptseg_fields = check_mandatory_fields( "Ptseg", self.ptseg_fields, Ptseg._mandatory_fields ) self.pt_fields = check_mandatory_fields( "Pt", self.pt_fields, Pt._mandatory_fields ) self.rte_fields = check_mandatory_fields( "Rte", self.rte_fields, Rte._mandatory_fields ) self.trkseg_fields = check_mandatory_fields( "Trkseg", self.trkseg_fields, Trkseg._mandatory_fields ) self.trk_fields = check_mandatory_fields( "Trk", self.trk_fields, Trk._mandatory_fields ) self.wpt_fields = check_mandatory_fields( "Wpt", self.wpt_fields, Wpt._mandatory_fields ) self.trkpt_fields = check_mandatory_fields( "Trkpt", self.trkpt_fields, Wpt._mandatory_fields ) # Create methods behaviors self._add_bounds = self.method_creator.add_bounds_creator(self.bounds_fields) self._add_copyright = self.method_creator.add_copyright_creator( self.copyright_fields ) self._add_email = self.method_creator.add_email_creator(self.email_fields) self._add_link = self.method_creator.add_link_creator(self.link_fields) self._add_metadata = self.method_creator.add_metadata_creator( self.metadata_fields ) self._add_person = self.method_creator.add_person_creator(self.person_fields) self._add_ptseg = self.method_creator.add_ptseg_creator(self.ptseg_fields) self._add_pt = self.method_creator.add_pt_creator(self.pt_fields) self._add_rte = self.method_creator.add_rte_creator(self.rte_fields) self._add_trkseg = self.method_creator.add_trkseg_creator(self.trkseg_fields) self._add_trk = self.method_creator.add_trk_creator(self.trk_fields) self._add_wpt = self.method_creator.add_wpt_creator(self.wpt_fields) self._add_trkpt = self.method_creator.add_trkpt_creator(self.trkpt_fields) # Write .gpx file self.gpx_to_string() with open(self.file_path, "w", encoding="utf-8") as f: f.write('<?xml version="1.0" encoding="UTF-8"?>' + self.gpx_string)