aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2025-10-15 21:42:19 +0200
committerDaniel Schadt <kingdread@gmx.de>2025-10-15 21:42:19 +0200
commit9442a623fe191048b544e150a8ec9c3527222e0b (patch)
tree453065f8aa2329bab8f49beb2682562d13ecf0c6
parent572a3c68266efa1add97eee17e4d94c2a075961d (diff)
downloadfietsboek-gpx-influx.tar.gz
fietsboek-gpx-influx.tar.bz2
fietsboek-gpx-influx.zip
initial geo.Path implementationgpx-influx
Since we want to move GPX data into the database, we need to do all the things that gpxpy currently does for us, including the length and speed computations. This is the start.
-rw-r--r--fietsboek/geo.py132
1 files changed, 132 insertions, 0 deletions
diff --git a/fietsboek/geo.py b/fietsboek/geo.py
new file mode 100644
index 0000000..c665705
--- /dev/null
+++ b/fietsboek/geo.py
@@ -0,0 +1,132 @@
+from dataclasses import dataclass
+from itertools import islice
+from math import sqrt, sin, cos, radians
+
+import gpxpy
+
+
+# WGS-84 equatorial radius, also called the semi-major axis.
+# https://en.wikipedia.org/wiki/Earth_radius
+EARTH_RADIUS = 6378137.0
+"""Radius of the earth, in meters."""
+
+# https://en.wikipedia.org/wiki/Preferred_walking_speed
+MOVING_THRESHOLD = 1.1
+"""Speed which is considered to be the moving threshold, in m/s."""
+
+
+@dataclass
+class MovementData:
+ duration: float = 0.0
+ """Duration of the path, in seconds."""
+
+ moving_duration: float = 0.0
+ """Duration spent moving, in seconds."""
+
+ stopped_duration: float = 0.0
+ """Duration spent stopped, in seconds."""
+
+ length: float = 0.0
+ """Length of the path, in meters."""
+
+ average_speed: float = 0.0
+ """Average speed, in m/s."""
+
+ maximum_speed: float = 0.0
+ """Maximum speed, in m/s."""
+
+ uphill: float = 0.0
+ """Uphill elevation, in meters."""
+
+ downhill: float = 0.0
+ """Downhill elevation, in meters."""
+
+
+@dataclass(slots=True)
+class Point:
+ longitude: float
+ latitude: float
+ elevation: float
+ time_offset: float
+
+ def distance(self, other: "Point") -> float:
+ """Returns the distance between this point and the given other point in
+ meters.
+ """
+ r_1 = EARTH_RADIUS + self.elevation
+ r_2 = EARTH_RADIUS + other.elevation
+ # The formula assumes that 0° is straight upward, but 0° in geo
+ # coordinates is actually on the equator plane.
+ t_1 = radians(90 - self.latitude)
+ t_2 = radians(90 - other.latitude)
+ p_1 = radians(self.longitude)
+ p_2 = radians(other.longitude)
+ # See
+ # https://en.wikipedia.org/wiki/Spherical_coordinate_system#Distance_in_spherical_coordinates
+ # While this is not the Haversine formula for distances along the
+ # circle curvature, it allows us to take the elevation into account,
+ # and for most GPS point differences that we encounter it should be
+ # enough.
+ radicand = (
+ r_1**2 +
+ r_2**2 -
+ 2 * r_1 * r_2 * (
+ sin(t_1) * sin(t_2) * cos(p_1 - p_2) +
+ cos(t_1) * cos(t_2)
+ )
+ )
+ if radicand < 0.0:
+ return 0.0
+ return sqrt(radicand)
+
+
+class Path:
+ @classmethod
+ def from_gpx(cls, gpx: gpxpy.gpx.GPX) -> "Path":
+ points = []
+ start_time = None
+
+ for track in gpx.tracks:
+ for segment in track.segments:
+ for point in segment.points:
+ if start_time is None:
+ start_time = point.time
+
+ time_offset = (point.time - start_time).total_seconds()
+ points.append(Point(
+ longitude=point.longitude,
+ latitude=point.latitude,
+ elevation=point.elevation,
+ time_offset=time_offset,
+ ))
+
+ return cls(points)
+
+ def __init__(self, points: list[Point]):
+ self.points = points
+
+ def _point_pairs(self):
+ return zip(self.points, islice(self.points, 1, None))
+
+ def movement_data(self) -> MovementData:
+ """Returns the movement data."""
+ movement_data = MovementData()
+ for a, b in self._point_pairs():
+ distance = a.distance(b)
+ time = b.time_offset - a.time_offset
+ speed = distance / time
+ elevation = b.elevation - a.elevation
+
+ movement_data.length += distance
+ if speed >= MOVING_THRESHOLD:
+ movement_data.moving_duration += time
+ else:
+ movement_data.stopped_duration += time
+ movement_data.maximum_speed = max(movement_data.maximum_speed, speed)
+ if elevation > 0.0:
+ movement_data.uphill += elevation
+ else:
+ movement_data.downhill += -elevation
+ movement_data.duration = b.time_offset
+ movement_data.average_speed = movement_data.length / movement_data.moving_duration
+ return movement_data