aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst1
-rw-r--r--doc/user/transformers.rst15
-rw-r--r--fietsboek/transformers/__init__.py13
-rw-r--r--fietsboek/transformers/breaks.py123
4 files changed, 146 insertions, 6 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b1e573b..4ce963a 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -10,6 +10,7 @@ Added
- Profile pages with "milestone tracks" (longest, shortest, ...).
- Integration with ``hittekaart`` for heatmaps on the profile.
- A data version check at startup.
+- The *Remove Breaks* transformer.
Changed
^^^^^^^
diff --git a/doc/user/transformers.rst b/doc/user/transformers.rst
index c8ddf01..d053052 100644
--- a/doc/user/transformers.rst
+++ b/doc/user/transformers.rst
@@ -43,3 +43,18 @@ point.
To fix those points, the transformer will find the first correct point, and
copy its elevation to the wrong points.
+
+Remove Breaks
+-------------
+
+The *remove breaks* transformer removes longer breaks of inactivity by deleting
+the points and shifting the following points back in time. This is useful e.g.
+if you are waiting at a single spot for a while, and you would like that to be
+removed for a cleaner log.
+
+Note that this transformer modifies the track's timings. Therefore, the
+recording will end earlier than it did in reality, and the stopped time will be
+reduced.
+
+A break is removed if the speed is below 1 kilometer per hour (approx. 0.28
+meters per second) for more than 5 minutes.
diff --git a/fietsboek/transformers/__init__.py b/fietsboek/transformers/__init__.py
index f6318d7..330e699 100644
--- a/fietsboek/transformers/__init__.py
+++ b/fietsboek/transformers/__init__.py
@@ -157,14 +157,11 @@ class FixNullElevation(Transformer):
def execute(self, gpx: GPX):
def all_points():
- return (
- point
- for track in gpx.tracks
- for segment in track.segments
- for point in segment.points
- )
+ return gpx.walk(only_points=True)
def rev_points():
+ # We cannot use reversed(gpx.walk(...)) since that is not a
+ # generator, so we do it manually.
return (
point
for track in reversed(gpx.tracks)
@@ -229,8 +226,12 @@ def list_transformers() -> list[type[Transformer]]:
:return: A list of transformers.
"""
+ # pylint: disable=import-outside-toplevel,cyclic-import
+ from .breaks import RemoveBreaks
+
return [
FixNullElevation,
+ RemoveBreaks,
]
diff --git a/fietsboek/transformers/breaks.py b/fietsboek/transformers/breaks.py
new file mode 100644
index 0000000..1c56414
--- /dev/null
+++ b/fietsboek/transformers/breaks.py
@@ -0,0 +1,123 @@
+"""Transformers that deal with breaks in the track."""
+import datetime
+
+from gpxpy.gpx import GPX, GPXTrack
+from pyramid.i18n import TranslationString
+
+from . import Parameters, Transformer
+
+_ = TranslationString
+
+# 1km/h is the default limit of gpxpy. We convert this into m/s to make our
+# live easier.
+STOPPED_SPEED_LIMIT: float = 1 * 1000 / (60 * 60)
+"""Speed limit (in m/s) at which the track is considered to be stopped."""
+
+# We don't want to remove "breaks" at traffic lights or similar, so 5 minutes
+# should be a good default.
+MIN_BREAK_TO_REMOVE: datetime.timedelta = datetime.timedelta(minutes=5)
+"""Minimum length of the break to trigger the removal."""
+
+
+class RemoveBreaks(Transformer):
+ """A transformer that fixes points with zero elevation."""
+
+ @classmethod
+ def identifier(cls) -> str:
+ return "remove-breaks"
+
+ @classmethod
+ def name(cls) -> TranslationString:
+ return _("transformers.remove-breaks.title")
+
+ @classmethod
+ def description(cls) -> TranslationString:
+ return _("transformers.remove-breaks.description")
+
+ @classmethod
+ def parameter_model(cls) -> type[Parameters]:
+ return Parameters
+
+ @property
+ def parameters(self) -> Parameters:
+ return Parameters()
+
+ @parameters.setter
+ def parameters(self, value):
+ pass
+
+ def execute(self, gpx: GPX):
+ for track in gpx.tracks:
+ self._clean(track)
+
+ def _clean(self, track: GPXTrack):
+ if not track.get_points_no():
+ return
+
+ i = 0
+ while i < track.get_points_no():
+ segment_idx, point_idx = index(track, i)
+ point = track.segments[segment_idx].points[point_idx]
+
+ # We check if the following points constitute a break, and if yes,
+ # how many of them
+ count = 0
+ current_length = 0.0
+ last_point = point
+ while True:
+ try:
+ j_segment, j_point = index(track, i + count + 1)
+ except IndexError:
+ break
+ current_point = track.segments[j_segment].points[j_point]
+ current_length += last_point.distance_3d(current_point) or 0.0
+ last_point = current_point
+
+ delta_t = datetime.timedelta(seconds=point.time_difference(last_point) or 0.0)
+ if not delta_t or current_length / delta_t.total_seconds() > STOPPED_SPEED_LIMIT:
+ break
+ count += 1
+
+ # No break, carry on!
+ if not count:
+ i += 1
+ continue
+
+ # At this point, check if the break is long enough to be removed
+ delta_t = datetime.timedelta(seconds=point.time_difference(last_point) or 0.0)
+ if delta_t < MIN_BREAK_TO_REMOVE:
+ i += 1
+ continue
+
+ # Here, we have a proper break to remove
+ # Delete the points belonging to the break ...
+ for _ in range(count):
+ j_segment, j_point = index(track, i + 1)
+ del track.segments[j_segment].points[j_point]
+
+ # ... and shift the time of the following points
+ j = i + 1
+ while j < track.get_points_no():
+ j_segment, j_point = index(track, j)
+ track.segments[j_segment].points[j_point].adjust_time(-delta_t)
+ j += 1
+
+
+def index(track: GPXTrack, idx: int) -> tuple[int, int]:
+ """Takes a one-dimensional index (the point index) and returns an index
+ into the segment/segment points.
+
+ :raises IndexError: When the given index is out of bounds.
+ :param track: The track for which to get the index.
+ :param idx: The "1D" index.
+ :return: A tuple with the segment index, and the index of the point within
+ the segment.
+ """
+ for segment_idx, segment in enumerate(track.segments):
+ if idx < len(segment.points):
+ return (segment_idx, idx)
+ idx -= len(segment.points)
+ raise IndexError
+
+
+__all__ = ["RemoveBreaks"]