diff options
| author | Daniel Schadt <kingdread@gmx.de> | 2025-12-26 00:54:30 +0100 |
|---|---|---|
| committer | Daniel Schadt <kingdread@gmx.de> | 2025-12-30 19:16:32 +0100 |
| commit | 1fa875181f7ab321bb1fad7a929f05dfcd083f17 (patch) | |
| tree | 4113d59d51bcb552974feb920a4147a33aca72f2 | |
| parent | a00cea3286310c93f55dc678335e6d4d8ea2d850 (diff) | |
| download | fietsboek-1fa875181f7ab321bb1fad7a929f05dfcd083f17.tar.gz fietsboek-1fa875181f7ab321bb1fad7a929f05dfcd083f17.tar.bz2 fietsboek-1fa875181f7ab321bb1fad7a929f05dfcd083f17.zip | |
de-duplicate gpx_xml
| -rw-r--r-- | fietsboek/geo.py | 88 | ||||
| -rw-r--r-- | fietsboek/models/journey.py | 42 | ||||
| -rw-r--r-- | fietsboek/models/track.py | 79 |
3 files changed, 117 insertions, 92 deletions
diff --git a/fietsboek/geo.py b/fietsboek/geo.py index c0a10e7..e6abb71 100644 --- a/fietsboek/geo.py +++ b/fietsboek/geo.py @@ -1,9 +1,13 @@ """This module implements GPS related functionality.""" +import datetime +import io from dataclasses import dataclass from itertools import islice from math import cos, radians, sin, sqrt +from . import util + # WGS-84 equatorial radius, also called the semi-major axis. # https://en.wikipedia.org/wiki/Earth_radius EARTH_RADIUS = 6378137.0 @@ -15,6 +19,17 @@ MOVING_THRESHOLD = 1.1 @dataclass +class Waypoint: + """A waypoint, a special landmark marked in the track.""" + + longitude: float + latitude: float + elevation: float | None + name: str | None + description: str | None + + +@dataclass class MovementData: """Movement statistics for a path.""" @@ -146,3 +161,76 @@ class Path: else: movement_data.average_speed = 0.0 return movement_data + + +def gpx_xml( + title: str | None, + description: str | None, + date: datetime.datetime, + points: list[Point], + waypoints: list[Waypoint], +) -> bytes: + """Returns an XML representation of the given path. + + :param title: The title of the resulting track. + :param description: The description of the resulting track. + :param points: The points that make up this track. + :param waypoints: The waypoints that should be included. + :return: The XML representation (a GPX file). + """ + # This is a cumbersome way to do it, as we're re-implementing XML + # serialization logic. However, recreating the track in gpxpy and + # letting it serialize it is much slower: + # For a track with around 50,000 points, the gpxpy method takes + # ~5.9 seconds here, while the "manual" buffer takes only ~2.4 seconds. + # This is a speed-up we're happy to take! + buf = io.BytesIO() + buf.write(b'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>') + buf.write(b'<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">') + + buf.write(b"<metadata>") + if title: + buf.write(b"<name>%s</name>" % util.xml_escape(title)) + if description: + buf.write(b"<desc>%s</desc>" % util.xml_escape(description)) + buf.write(b"</metadata>") + + # Cache for fast access + write = buf.write + + write(b"<trk>") + write(b"<trkseg>") + for point in points: + write(b'<trkpt lat="') + write(str(point.latitude).encode("ascii")) + write(b'" lon="') + write(str(point.longitude).encode("ascii")) + write(b'">') + write(b"<ele>") + write(str(point.elevation).encode("ascii")) + write(b"</ele>") + write(b"<time>") + write(str(date + datetime.timedelta(seconds=point.time_offset)).encode("ascii")) + write(b"</time>") + write(b"</trkpt>\n") + write(b"</trkseg>") + write(b"</trk>") + + # This loop is not as hot: + for wpt in waypoints: + write( + b'<wpt lat="%s" lon="%s">' + % (util.xml_escape(str(wpt.latitude)), util.xml_escape(str(wpt.longitude))) + ) + if wpt.elevation is not None: + write(b"<ele>%s</ele>" % util.xml_escape(str(wpt.elevation))) + if wpt.name is not None: + write(b"<name>%s</name>" % util.xml_escape(wpt.name)) + if wpt.description is not None: + write(b"<cmt>%s</cmt>" % util.xml_escape(wpt.description)) + write(b"<desc>%s</desc>" % util.xml_escape(wpt.description)) + write(b"</wpt>") + + write(b"</gpx>") + + return buf.getvalue() diff --git a/fietsboek/models/journey.py b/fietsboek/models/journey.py index d90db55..81bf30c 100644 --- a/fietsboek/models/journey.py +++ b/fietsboek/models/journey.py @@ -1,3 +1,4 @@ +import datetime import dataclasses import io import logging @@ -159,39 +160,14 @@ class Journey(Base): return geo.Path(points) def gpx_xml(self) -> bytes: - buf = io.BytesIO() - buf.write(b'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>') - buf.write(b'<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">') - - buf.write(b"<metadata>") - if self.title: - buf.write(b"<name>%s</name>" % util.xml_escape(self.title)) - if self.description: - buf.write(b"<desc>%s</desc>" % util.xml_escape(self.description)) - buf.write(b"</metadata>") - - # Cache for easy access, especially the date is important since it's a - # dynamic property - write = buf.write - - write(b"<trk>") - write(b"<trkseg>") - for point in self.path().points: - write(b'<trkpt lat="') - write(str(point.latitude).encode("ascii")) - write(b'" lon="') - write(str(point.longitude).encode("ascii")) - write(b'">') - write(b"<ele>") - write(str(point.elevation).encode("ascii")) - write(b"</ele>") - write(b"</trkpt>\n") - write(b"</trkseg>") - write(b"</trk>") - - write(b"</gpx>") - - return buf.getvalue() + return geo.gpx_xml( + self.title, + self.description, + datetime.datetime.fromtimestamp(0).replace(tzinfo=datetime.UTC), + self.path().points, + [] + ) + __all__ = [ ] diff --git a/fietsboek/models/track.py b/fietsboek/models/track.py index fc5a68b..2fec844 100644 --- a/fietsboek/models/track.py +++ b/fietsboek/models/track.py @@ -17,7 +17,6 @@ meta information. import datetime import enum import gzip -import io import json import logging from itertools import chain @@ -176,6 +175,19 @@ class Waypoint(Base): track: Mapped["Track"] = relationship("Track", back_populates="waypoints") + def to_geo_waypoint(self) -> geo.Waypoint: + """Converts this waypoint (a database object) to a plain waypoint. + + :return: The converted point. + """ + return geo.Waypoint( + latitude=self.latitude, + longitude=self.longitude, + elevation=self.elevation, + name=self.name, + description=self.description, + ) + class TrackPoint(Base): """A track point represents a single GPS point along a path.""" @@ -438,64 +450,13 @@ class Track(Base): :return: The XML representation (a GPX file). """ - # This is a cumbersome way to do it, as we're re-implementing XML - # serialization logic. However, recreating the track in gpxpy and - # letting it serialize it is much slower: - # For a track with around 50,000 points, the gpxpy method takes - # ~5.9 seconds here, while the "manual" buffer takes only ~2.4 seconds. - # This is a speed-up we're happy to take! - buf = io.BytesIO() - buf.write(b'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>') - buf.write(b'<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">') - - buf.write(b"<metadata>") - if self.title: - buf.write(b"<name>%s</name>" % util.xml_escape(self.title)) - if self.description: - buf.write(b"<desc>%s</desc>" % util.xml_escape(self.description)) - buf.write(b"</metadata>") - - # Cache for easy access, especially the date is important since it's a - # dynamic property - date = self.date - write = buf.write - - write(b"<trk>") - write(b"<trkseg>") - for point in self.path().points: - write(b'<trkpt lat="') - write(str(point.latitude).encode("ascii")) - write(b'" lon="') - write(str(point.longitude).encode("ascii")) - write(b'">') - write(b"<ele>") - write(str(point.elevation).encode("ascii")) - write(b"</ele>") - write(b"<time>") - write(str(date + datetime.timedelta(seconds=point.time_offset)).encode("ascii")) - write(b"</time>") - write(b"</trkpt>\n") - write(b"</trkseg>") - write(b"</trk>") - - # This loop is not as hot: - for wpt in self.waypoints: - write( - b'<wpt lat="%s" lon="%s">' - % (util.xml_escape(str(wpt.latitude)), util.xml_escape(str(wpt.longitude))) - ) - if wpt.elevation is not None: - write(b"<ele>%s</ele>" % util.xml_escape(str(wpt.elevation))) - if wpt.name is not None: - write(b"<name>%s</name>" % util.xml_escape(wpt.name)) - if wpt.description is not None: - write(b"<cmt>%s</cmt>" % util.xml_escape(wpt.description)) - write(b"<desc>%s</desc>" % util.xml_escape(wpt.description)) - write(b"</wpt>") - - write(b"</gpx>") - - return buf.getvalue() + return geo.gpx_xml( + self.title, + self.description, + self.date, + self.path().points, + [wpt.to_geo_waypoint() for wpt in self.waypoints], + ) @property def date(self): |
