diff options
-rw-r--r-- | fietsboek/transformers/__init__.py | 95 | ||||
-rw-r--r-- | fietsboek/transformers/elevation.py | 106 |
2 files changed, 107 insertions, 94 deletions
diff --git a/fietsboek/transformers/__init__.py b/fietsboek/transformers/__init__.py index 330e699..7df700c 100644 --- a/fietsboek/transformers/__init__.py +++ b/fietsboek/transformers/__init__.py @@ -11,7 +11,6 @@ function to load and apply transformers. from abc import ABC, abstractmethod from collections.abc import Callable, Iterable, Mapping -from itertools import islice from typing import Literal, NamedTuple, TypeVar from gpxpy.gpx import GPX, GPXTrackPoint @@ -128,99 +127,6 @@ class Transformer(ABC): """ -class FixNullElevation(Transformer): - """A transformer that fixes points with zero elevation.""" - - @classmethod - def identifier(cls) -> str: - return "fix-null-elevation" - - @classmethod - def name(cls) -> TranslationString: - return _("transformers.fix-null-elevation.title") - - @classmethod - def description(cls) -> TranslationString: - return _("transformers.fix-null-elevation.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): - def all_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) - for segment in reversed(track.segments) - for point in reversed(segment.points) - ) - - # First, from the front - self.fixup(all_points) - # Then, from the back - self.fixup(rev_points) - - @classmethod - def fixup(cls, points: Callable[[], Iterable[GPXTrackPoint]]): - """Fixes the given GPX points. - - This iterates over the points and checks for the first point that has a - non-zero elevation, and a slope that doesn't exceed 100%. All previous - points will have their elevation adjusted to match this first "good - point". - - :param points: A function that generates the iterable of points. - """ - max_slope = 1.0 - - bad_until = 0 - final_elevation = 0.0 - for i, (point, next_point) in enumerate(zip(points(), islice(points(), 1, None))): - if ( - point.elevation is not None - and point.elevation != 0.0 - and cls.slope(point, next_point) < max_slope - ): - bad_until = i - final_elevation = point.elevation - break - - for point in islice(points(), None, bad_until): - point.elevation = final_elevation - - @staticmethod - def slope(point_a: GPXTrackPoint, point_b: GPXTrackPoint) -> float: - """Returns the slope between two GPX points. - - This is defined as delta_h / euclid_distance. - - :param point_a: First point. - :param point_b: Second point. - :return: The slope, as percentage. - """ - if point_a.elevation is None or point_b.elevation is None: - return 0.0 - delta_h = abs(point_a.elevation - point_b.elevation) - dist = point_a.distance_2d(point_b) - if dist == 0.0 or dist is None: - return 0.0 - return delta_h / dist - - def list_transformers() -> list[type[Transformer]]: """Returns a list of all available transformers. @@ -228,6 +134,7 @@ def list_transformers() -> list[type[Transformer]]: """ # pylint: disable=import-outside-toplevel,cyclic-import from .breaks import RemoveBreaks + from .elevation import FixNullElevation return [ FixNullElevation, diff --git a/fietsboek/transformers/elevation.py b/fietsboek/transformers/elevation.py new file mode 100644 index 0000000..0e6f3b0 --- /dev/null +++ b/fietsboek/transformers/elevation.py @@ -0,0 +1,106 @@ +"""Transformers that deal with elevation changes in the track.""" +from collections.abc import Callable, Iterable +from itertools import islice + +from gpxpy.gpx import GPX, GPXTrackPoint +from pyramid.i18n import TranslationString + +from . import Parameters, Transformer + +_ = TranslationString + + +class FixNullElevation(Transformer): + """A transformer that fixes points with zero elevation.""" + + @classmethod + def identifier(cls) -> str: + return "fix-null-elevation" + + @classmethod + def name(cls) -> TranslationString: + return _("transformers.fix-null-elevation.title") + + @classmethod + def description(cls) -> TranslationString: + return _("transformers.fix-null-elevation.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): + def all_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) + for segment in reversed(track.segments) + for point in reversed(segment.points) + ) + + # First, from the front + self.fixup(all_points) + # Then, from the back + self.fixup(rev_points) + + @classmethod + def fixup(cls, points: Callable[[], Iterable[GPXTrackPoint]]): + """Fixes the given GPX points. + + This iterates over the points and checks for the first point that has a + non-zero elevation, and a slope that doesn't exceed 100%. All previous + points will have their elevation adjusted to match this first "good + point". + + :param points: A function that generates the iterable of points. + """ + max_slope = 1.0 + + bad_until = 0 + final_elevation = 0.0 + for i, (point, next_point) in enumerate(zip(points(), islice(points(), 1, None))): + if ( + point.elevation is not None + and point.elevation != 0.0 + and cls.slope(point, next_point) < max_slope + ): + bad_until = i + final_elevation = point.elevation + break + + for point in islice(points(), None, bad_until): + point.elevation = final_elevation + + @staticmethod + def slope(point_a: GPXTrackPoint, point_b: GPXTrackPoint) -> float: + """Returns the slope between two GPX points. + + This is defined as delta_h / euclid_distance. + + :param point_a: First point. + :param point_b: Second point. + :return: The slope, as percentage. + """ + if point_a.elevation is None or point_b.elevation is None: + return 0.0 + delta_h = abs(point_a.elevation - point_b.elevation) + dist = point_a.distance_2d(point_b) + if dist == 0.0 or dist is None: + return 0.0 + return delta_h / dist + + +__all__ = ["FixNullElevation"] |