aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2025-12-26 00:54:30 +0100
committerDaniel Schadt <kingdread@gmx.de>2025-12-30 19:16:32 +0100
commit1fa875181f7ab321bb1fad7a929f05dfcd083f17 (patch)
tree4113d59d51bcb552974feb920a4147a33aca72f2
parenta00cea3286310c93f55dc678335e6d4d8ea2d850 (diff)
downloadfietsboek-1fa875181f7ab321bb1fad7a929f05dfcd083f17.tar.gz
fietsboek-1fa875181f7ab321bb1fad7a929f05dfcd083f17.tar.bz2
fietsboek-1fa875181f7ab321bb1fad7a929f05dfcd083f17.zip
de-duplicate gpx_xml
-rw-r--r--fietsboek/geo.py88
-rw-r--r--fietsboek/models/journey.py42
-rw-r--r--fietsboek/models/track.py79
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):