From 991b8ff287c50ecc1b83fc6491959fbb1b1d423b Mon Sep 17 00:00:00 2001 From: FABallemand Date: Mon, 19 Jun 2023 12:05:38 +0200 Subject: [PATCH] #1 Work in progress: -GPX standard 1.1 -Improved Parser -Improved Writer --- .vscode/settings.json | 3 + README.md | 14 +- ezgpx/gpx_elements/__init__.py | 1 + ezgpx/gpx_elements/person.py | 16 ++ ezgpx/gpx_parser/parser.py | 301 +++++++++++++++++++++++++++------ ezgpx/gpx_writer/writer.py | 87 +++++++--- 6 files changed, 351 insertions(+), 71 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 ezgpx/gpx_elements/person.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..70e34ec --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "C_Cpp.errorSquiggles": "disabled" +} \ No newline at end of file diff --git a/README.md b/README.md index 5f4e643..5731c5f 100644 --- a/README.md +++ b/README.md @@ -40,4 +40,16 @@ test_gpx.plot(start_stop=True, elevation_color=True) ## 👤 Author - Fabien ALLEMAND -## 📝 TO DO LIST !! \ No newline at end of file +## 📝 TO DO LIST !! +- Complete gpx +```xml + + metadataType [0..1] ? + wptType [0..*] ? + rteType [0..*] ? + trkType [0..*] ? + extensionsType [0..1] ? + +``` \ No newline at end of file diff --git a/ezgpx/gpx_elements/__init__.py b/ezgpx/gpx_elements/__init__.py index 7e6f57e..b486877 100644 --- a/ezgpx/gpx_elements/__init__.py +++ b/ezgpx/gpx_elements/__init__.py @@ -5,6 +5,7 @@ from .gpx import * from .link import * from .metadata import * +from .person import * from .track import * from .track_segment import * from .track_point import * \ No newline at end of file diff --git a/ezgpx/gpx_elements/person.py b/ezgpx/gpx_elements/person.py new file mode 100644 index 0000000..64f4ef9 --- /dev/null +++ b/ezgpx/gpx_elements/person.py @@ -0,0 +1,16 @@ +from .email import Email +from .link import Link + +class Person(): + """ + Person (person) element in GPX file. + """ + + def __init__( + self, + name: str = None, + email: Email = None, + link: Link = None) -> None: + self.name: str = name + self.email: Email = email + self.link: Link = link \ No newline at end of file diff --git a/ezgpx/gpx_parser/parser.py b/ezgpx/gpx_parser/parser.py index 9cfde96..947f517 100644 --- a/ezgpx/gpx_parser/parser.py +++ b/ezgpx/gpx_parser/parser.py @@ -3,7 +3,7 @@ import xml.etree.ElementTree as ET -from ..gpx_elements import Bounds, Copyright, Email, Extensions, Gpx, Link, Metadata, TrackPoint, TrackSegment, Track +from ..gpx_elements import Bounds, Copyright, Email, Extensions, Gpx, Link, Metadata, Person, TrackPoint, TrackSegment, Track class Parser(): @@ -20,83 +20,282 @@ def __init__(self, file_path: str = ""): def check_schema(self): pass - def parse_link(self, link) -> Link: - href = link.get("href") - text = link.find("topo:text", self.name_space) - type = link.find("topo:type", self.name_space) - return Link(href, text, type) + def get_text(self, element, sub_element: str) -> str: + """ + Get text from sub-element. + + Args: + element (???): Parsed element from GPX file. + sub_element (str): Sub-element name. + + Returns: + str: Text from sub-element. + """ + try: + text = element.get(sub_element).text + except: + logging.DEBUG(f"{element} has no attribute {sub_element}") + text = None + return text + + def find_text(self, element, sub_element: str) -> str: + """ + Find text from sub-element. + + Args: + element (???): Parsed element from GPX file. + sub_element (str): Sub-element name. + + Returns: + str: Text from sub-element. + """ + try: + text = element.find(sub_element, self.name_space).text + except: + logging.debug(f"{element} has no attribute {sub_element}") + text = None + return text + + def parse_bounds(self, bounds) -> Bounds: + """ + Parse bounds element in GPX file. + + Args: + bounds (???): Parsed bounds element. + + Returns: + Bounds: Bounds object. + """ + if bounds is None: + return None + + minlat = self.get_text(bounds, "minlat") + minlon = self.get_text(bounds, "minlon") + maxlat = self.get_text(bounds, "maxlat") + maxlon = self.get_text(bounds, "maxlon") + return Bounds(minlat, minlon, maxlat, maxlon) def parse_copyright(self, copyright) -> Copyright: - author = copyright.get("author") - year = copyright.find("topo:year", self.name_space) - licence = copyright.find("topo:licence", self.name_space) + """ + Parse copyright element in GPX file. + + Args: + copyright (???): Parsed copyright element. + + Returns: + Copyright: Copyright object. + """ + if copyright is None: + return None + + author = self.get_text(copyright, "author") + year = self.find_text(copyright, "topo:year") + licence = self.find_text(copyright, "topo:licence") return Copyright(author, year, licence) def parse_email(self, email) -> Email: - id = email.get("id") - domain = email.get("domain") + """ + Parse email element in GPX file. + + Args: + email (???): Parsed email element. + + Returns: + Email: Email object. + """ + if email is None: + return None + + id = self.get_text(email, "id") + domain = self.get_text(email, "domain") return Email(id, domain) - def parse_bounds(self, bounds) -> Bounds: - minlat = bounds.get("minlat") - minlon = bounds.get("minlon") - maxlat = bounds.get("maxlat") - maxlon = bounds.get("maxlon") - return Bounds(minlat, minlon, maxlat, maxlon) + def parse_extensions(self, extensions) -> Extensions: + """ + Parse extensions element in GPX file. + + Args: + extensions (???): Parsed extensions element. + + Returns: + Extensions: Extensions object. + """ + if extensions is None: + return None + + display_color = self.find_text(extensions, "topo:DisplayColor") + distance = self.find_text(extensions, "topo:Distance") + total_elapsed_time = self.find_text(extensions, "topo:TotalElapsedTime") + moving_time = self.find_text(extensions, "topo:MovingTime") + stopped_time = self.find_text(extensions, "topo:StoppedTime") + moving_speed = self.find_text(extensions, "topo:MovingSpeed") + max_speed = self.find_text(extensions, "topo:MaxSpeed") + max_elevation = self.find_text(extensions, "topo:MaxElevation") + min_elevation = self.find_text(extensions, "topo:MinElevation") + ascent = self.find_text(extensions, "topo:Ascent") + descent = self.find_text(extensions, "topo:Descent") + avg_ascent_rate = self.find_text(extensions, "topo:AvgAscentRate") + max_ascent_rate = self.find_text(extensions, "topo:MaxAscentRate") + avg_descent_rate = self.find_text(extensions, "topo:AvgDescentRate") + max_descent_rate = self.find_text(extensions, "topo:MaxDescentRate") + return Extensions(display_color, distance, total_elapsed_time, moving_time, stopped_time, moving_speed, max_speed, max_elevation, min_elevation, ascent, descent, avg_ascent_rate, max_ascent_rate, avg_descent_rate, max_descent_rate) + + def parse_link(self, link) -> Link: + """ + Parse link element in GPX file. + + Args: + link (???): Parsed link element. + + Returns: + Link: Link object. + """ + if link is None: + return None + + href = self.get_text(link, "href") + text = self.find_text(link, "topo:text") + type = self.find_text(link, "topo:type") + return Link(href, text, type) + + def parse_person(self, person) -> Person: + """ + Parse person element in GPX file. + + Args: + person (???): Parsed person element. + Returns: + Person: Person object. + """ + if person is None: + return None + + name = self.find_text(person, "topo:name") + email = self.parse_email(person.find("topo:email", self.name_space)) + link = self.parse_link(person.find("topo:link", self.name_space)) + return Person(name, email, link) + def parse_metadata(self): + """ + Parse metadata element in GPX File. + """ metadata = self.gpx_root.find("topo:metadata", self.name_space) - name = metadata.find("topo:name", self.name_space) - desc = metadata.find("topo:desc", self.name_space) - author = metadata.find("topo:author", self.name_space) + name = self.find_text(metadata, "topo:name") + desc = self.find_text(metadata, "topo:desc") + author = self.parse_person(metadata.find("topo:author", self.name_space)) copyright = self.parse_copyright(metadata.find("topo:copyright", self.name_space)) link = self.parse_link(metadata.find("topo:link", self.name_space)) - time = metadata.find("topo:time", self.name_space) - keywords = metadata.find("topo:keywords", self.name_space) + time = datetime.strptime(metadata.find("topo:time", self.name_space).text, "%Y-%m-%dT%H:%M:%SZ") + keywords = self.find_text(metadata, "topo:keywords") bounds = self.parse_bounds(metadata.find("topo:bounds", self.name_space)) - extensions = metadata.find("topo:extensions", self.name_space) + extensions = self.parse_extensions(metadata.find("topo:extensions", self.name_space)) self.gpx.metadata = Metadata(name, desc, author, copyright, link, time, keywords, bounds, extensions) - def parse_tracks(self): + def parse_point(self, point) -> TrackPoint: + """ + Parse trkpt element from GPX file. - # Tracks - tracks = self.gpx_root.findall("topo:trk", self.name_space) - for track in tracks: - name = track.find("topo:name", self.name_space) - segments = track.findall("topo:trkseg", self.name_space) + Args: + point (???): Parsed trkpt element. - # Create new track - gpx_track = Track(name=name.text) + Returns: + TrackPoint: TrackPoint object. + """ + try: + lat = float(point.get("lat")) + except: + logging.error(f"{point} contains invalid latitude: {point.get('lat')}") + lat = None - # Track segments - for segment in segments: - points = segment.findall("topo:trkpt", self.name_space) + try: + lon = float(float(point.get("lon"))) + except: + logging.error(f"{point} contains invalid longitude: {float(point.get('lon'))}") + lon = None + + try: + elevation = float(self.find_text(point, "topo:ele")) + except: + logging.error(f"{point} contains invalid elevation: {self.find_text(point, 'topo:ele')}") + elevation = None + time = datetime.strptime(self.find_text(point, "topo:time"), "%Y-%m-%dT%H:%M:%SZ") + + return TrackPoint(lat, lon, elevation, time) - # Crete new segment - gpx_segment = TrackSegment() + def parse_segment(self, segment) -> TrackSegment: + """ + Parse trkseg element from GPX file. - # Track points - for point in points: - elevation = point.find("topo:ele", self.name_space) - time = point.find("topo:time", self.name_space) - - # Create new point - gpx_point = TrackPoint(float(point.get("lat")), - float(point.get("lon")), - float(elevation.text), - datetime.strptime(time.text, "%Y-%m-%dT%H:%M:%SZ")) + Args: + segment (???): Parsed trkseg element. - gpx_segment.track_points.append(gpx_point) + Returns: + TrackSegment: TrackSegment object. + """ + # Points + trkpt = [] + points = segment.findall("topo:trkpt", self.name_space) + for point in points: + trkpt.append(self.parse_point(point)) - gpx_track.track_segments.append(gpx_segment) - - self.gpx.tracks.append(gpx_track) + # Extensions + extensions = self.parse_extensions(segment.find("topo:extensions", self.name_space)) + + return TrackSegment(trkpt, extensions) + + def parse_track(self, track) -> Track: + """ + Parse trk element in GPX file. + + Args: + track (???): Parsed trk element. + + Returns: + Track: Track object. + """ + name = self.find_text(track, "topo:name") + cmt = self.find_text(track, "topo:cmt") + desc = self.find_text(track, "topo:desc") + src = self.find_text(track, "topo:src") + link = self.parse_link(track.find("topo:link", self.name_space)) + try: + number = int(self.find_text(track, "topo:number")) + except: + logging.error(f"{track} contains invalid number: {self.find_text(track, 'topo:number')}") + number = None + type = self.find_text(track, "topo:type") + extensions = self.parse_extensions(track.find("topo:extensions", self.name_space)) + + trkseg = [] + segments = track.findall("topo:trkseg", self.name_space) + for segment in segments: + trkseg.append(self.parse_segment(segment)) + + return Track(name, cmt, desc, src, link, number, type, extensions, trkseg) + + def parse_tracks(self): + """ + Parse track elements in GPX file. + """ + # Tracks + tracks = self.gpx_root.findall("topo:trk", self.name_space) + for track in tracks: + self.gpx.tracks.append(self.parse_track(track)) def parse(self, file_path: str = "") -> Gpx: - + """ + Parse GPX file. + + Args: + file_path (str, optional): Path to the file to parse. Defaults to "". + + Returns: + Gpx: Gpx object., self.name_space).text + """ # File if file_path != "": self.file_path = file_path diff --git a/ezgpx/gpx_writer/writer.py b/ezgpx/gpx_writer/writer.py index f7b28bd..b85aebe 100644 --- a/ezgpx/gpx_writer/writer.py +++ b/ezgpx/gpx_writer/writer.py @@ -1,6 +1,6 @@ import xml.etree.ElementTree as ET -from ..gpx_elements import Gpx +from ..gpx_elements import Gpx, Metadata, Track class Writer(): @@ -15,25 +15,25 @@ def __init__(self, gpx: Gpx = None, path: str = "", metadata: bool = True, ele: self.ele = ele self.time = time - def write(self, gpx: Gpx = None, path: str = "", metadata: bool = True, ele: bool = True, time: bool = True): - - if gpx is not None: - self.gpx = gpx + def add_subelement(self, element, sub_element, text): + """ + Add sub-element to GPX element. - if path != "": - self.path = path - - if self.gpx is not None: - self.gpx_to_string() - - # Is it smart ?? - self.metadata = metadata - self.ele = ele - self.time = time + Args: + element (???): GPX element. + sub_element (str): GPX sub-element. + text (str): GPX sub-element text. - if self.path != "": # + Check if path is correct - self.write_gpx() + Returns: + ???: GPX element. + """ + if text is not None: + sub_elmnt = ET.SubElement(element, sub_element) + sub_elmnt.text = text + return element + def metadata_to_string(self, metadata: Metadata): + pass def gpx_to_string(self, gpx: Gpx = None, metadata: bool = True, ele: bool = True, time: bool = True) -> str: @@ -46,12 +46,43 @@ def gpx_to_string(self, gpx: Gpx = None, metadata: bool = True, ele: bool = True self.time = time if self.gpx is not None: + # Reset string + self.gpx_string = "" + # Root gpx_root = ET.Element("gpx") # Metadata if self.metadata: - pass + # self.gpx_string += self.metadata_to_string(self.gpx.metadata) + metadata = ET.SubElement(gpx_root, "metadata") + + metadata = self.add_subelement(metadata, "name", self.gpx.metadata.name) + metadata = self.add_subelement(metadata, "desc", self.gpx.metadata.desc) + + if self.gpx.metadata.author is not None: + author = ET.SubElement(metadata, "author") + author = self.add_subelement(author, "name", self.gpx.metadata.author.name) + # Add email and link + + if self.gpx.metadata.copyright is not None: + # Add copyright + pass + + if self.gpx.metadata.link is not None: + # Add link + pass + + metadata = self.add_subelement(metadata, "time", self.gpx.metadata.time) + metadata = self.add_subelement(metadata, "keywords", self.gpx.metadata.keywords) + + if self.gpx.metadata.bounds is not None: + # Add bounds + pass + + if self.gpx.metadata.extensions is not None: + # Add extensions + pass # Tracks for gpx_track in self.gpx.tracks: @@ -75,7 +106,6 @@ def gpx_to_string(self, gpx: Gpx = None, metadata: bool = True, ele: bool = True time = ET.SubElement(point, "time") time.text = gpx_point.time - # Convert data to string self.gpx_string = ET.tostring(gpx_root) @@ -92,3 +122,22 @@ def write_gpx(self, path: str = ""): f.write(b"") f.write(self.gpx_string) + def write(self, gpx: Gpx = None, path: str = "", metadata: bool = True, ele: bool = True, time: bool = True): + + if gpx is not None: + self.gpx = gpx + + if path != "": + self.path = path + + if self.gpx is not None: + self.gpx_to_string() + + # Is it smart ?? + self.metadata = metadata + self.ele = ele + self.time = time + + if self.path != "": # + Check if path is correct + self.write_gpx() +