Skip to content

Track Reference

GPXFileTrack(gpx_file, n_track=0, stopped_speed_threshold=1, max_speed_percentile=95, require_data_extensions=None, heartrate_zones=None, power_zones=None, cadence_zones=None)

Bases: Track

Track that should be initialized by loading a .gpx file

Initialize a Track object from a gpx file

Parameters:

Name Type Description Default
gpx_file str

Path to the gpx file.

required
n_track int

Index of track in the gpx file, defaults to 0

0
stopped_speed_threshold float

Minium speed required for a point to be count as moving, defaults to 1

1
max_speed_percentile int

Points with speed outside of the percentile are not counted when analyzing the track, defaults to 95

95
heartrate_zones None | Zones

Optional heartrate Zones, defaults to None

None
power_zones None | Zones

Optional power Zones, defaults to None

None
cadence_zones None | Zones

Optional cadence Zones, defaults to None

None
Source code in geo_track_analyzer/track.py
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
def __init__(
    self,
    gpx_file: str,
    n_track: int = 0,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    require_data_extensions: set[str] | None = None,
    heartrate_zones: None | Zones = None,
    power_zones: None | Zones = None,
    cadence_zones: None | Zones = None,
) -> None:
    """Initialize a Track object from a gpx file

    :param gpx_file: Path to the gpx file.
    :param n_track: Index of track in the gpx file, defaults to 0
    :param stopped_speed_threshold: Minium speed required for a point to be count
        as moving, defaults to 1
    :param max_speed_percentile: Points with speed outside of the percentile are not
        counted when analyzing the track, defaults to 95
    :param heartrate_zones: Optional heartrate Zones, defaults to None
    :param power_zones: Optional power Zones, defaults to None
    :param cadence_zones: Optional cadence Zones, defaults to None
    """

    super().__init__(
        stopped_speed_threshold=stopped_speed_threshold,
        max_speed_percentile=max_speed_percentile,
        heartrate_zones=heartrate_zones,
        require_data_extensions=require_data_extensions,
        power_zones=power_zones,
        cadence_zones=cadence_zones,
    )

    logger.info("Loading gpx track from file %s", gpx_file)

    gpx = self._get_gpx(gpx_file)

    self._track = gpx.tracks[n_track]
    self._update_extensions()

add_segmeent(segment)

Add a new segment ot the track

Parameters:

Name Type Description Default
segment GPXTrackSegment

GPXTracksegment to be added

required
Source code in geo_track_analyzer/track.py
114
115
116
117
118
119
120
def add_segmeent(self, segment: GPXTrackSegment) -> None:
    """Add a new segment ot the track

    :param segment: GPXTracksegment to be added
    """
    self.track.segments.append(segment)
    logger.info("Added segment with postition: %s", len(self.track.segments))

find_overlap_with_segment(n_segment, match_track, match_track_segment=0, width=50, overlap_threshold=0.75, max_queue_normalize=5, merge_subsegments=5, extensions_interpolation='copy-forward')

Find overlap of a segment of the track with a segment in another track.

Parameters:

Name Type Description Default
n_segment int

Segment in the track that sould be used as base for the comparison

required
match_track Track

Track object containing the segment to be matched

required
match_track_segment int

Segment on the passed track that should be matched to the segment in this track, defaults to 0

0
width float

Width (in meters) of the grid that will be filled to estimate the overalp , defaults to 50

50
overlap_threshold float

Minimum overlap (as fracrtion) required to return the overlap data, defaults to 0.75

0.75
max_queue_normalize int

Minimum number of successive points in the segment between two points falling into same plate bin, defaults to 5

5
merge_subsegments int

Number of points between sub segments allowed for merging the segments, defaults to 5

5
extensions_interpolation Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points, defaults to copy-forward

'copy-forward'

Returns:

Type Description
Sequence[tuple[Track, float, bool]]

Tuple containing a Track with the overlapping points, the overlap in percent, and the direction of the overlap

Source code in geo_track_analyzer/track.py
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
def find_overlap_with_segment(
    self,
    n_segment: int,
    match_track: Track,
    match_track_segment: int = 0,
    width: float = 50,
    overlap_threshold: float = 0.75,
    max_queue_normalize: int = 5,
    merge_subsegments: int = 5,
    extensions_interpolation: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> Sequence[tuple[Track, float, bool]]:
    """Find overlap of a segment of the track with a segment in another track.

    :param n_segment: Segment in the track that sould be used as base for the
        comparison
    :param match_track: Track object containing the segment to be matched
    :param match_track_segment: Segment on the passed track that should be matched
        to the segment in this track, defaults to 0
    :param width: Width (in meters) of the grid that will be filled to estimate
        the overalp , defaults to 50
    :param overlap_threshold: Minimum overlap (as fracrtion) required to return the
        overlap data, defaults to 0.75
    :param max_queue_normalize: Minimum number of successive points in the segment
        between two points falling into same plate bin, defaults to 5
    :param merge_subsegments: Number of points between sub segments allowed
        for merging the segments, defaults to 5
    :param extensions_interpolation: How should the extenstion (if present) be
        defined in the interpolated points, defaults to copy-forward

    :return: Tuple containing a Track with the overlapping points, the overlap in
        percent, and the direction of the overlap
    """
    max_distance_self = self.get_max_pp_distance_in_segment(n_segment)

    segment_self = self.track.segments[n_segment]
    if max_distance_self > width:
        segment_self = interpolate_segment(
            segment_self, width / 2, copy_extensions=extensions_interpolation
        )

    max_distance_match = match_track.get_max_pp_distance_in_segment(
        match_track_segment
    )
    segment_match = match_track.track.segments[match_track_segment]
    if max_distance_match > width:
        segment_match = interpolate_segment(
            segment_match, width / 2, copy_extensions=extensions_interpolation
        )

    logger.info("Looking for overlapping segments")
    segment_overlaps = get_segment_overlap(
        segment_self,
        segment_match,
        width,
        max_queue_normalize,
        merge_subsegments,
        overlap_threshold,
    )

    matched_tracks: list[tuple[Track, float, bool]] = []
    for overlap in segment_overlaps:
        logger.info("Found: %s", overlap)
        matched_segment = GPXTrackSegment()
        # TODO: Might need to go up to overlap.end_idx + 1?
        matched_segment.points = self.track.segments[n_segment].points[
            overlap.start_idx : overlap.end_idx
        ]
        matched_tracks.append(
            (
                SegmentTrack(
                    matched_segment,
                    stopped_speed_threshold=self.stopped_speed_threshold,
                    max_speed_percentile=self.max_speed_percentile,
                ),
                overlap.overlap,
                overlap.inverse,
            )
        )
    return matched_tracks

get_avg_pp_distance(threshold=10)

Get average distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
379
380
381
382
383
384
385
386
387
388
def get_avg_pp_distance(self, threshold: float = 10) -> float:
    """
    Get average distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance("average", threshold)

get_avg_pp_distance_in_segment(n_segment=0, threshold=10)

Get average distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def get_avg_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get average distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance_in_segment(
        "average", n_segment, threshold
    )

get_closest_point(n_segment, latitude, longitude)

Get closest point in a segment or track to the passed latitude and longitude corrdinate

Parameters:

Name Type Description Default
n_segment None | int

Index of the segment. If None is passed the whole track is considered

required
latitude float

Latitude to check

required
longitude float

Longitude to check

required

Returns:

Type Description
PointDistance

Tuple containg the point as GPXTrackPoint, the distance from the passed coordinates and the index in the segment

Source code in geo_track_analyzer/track.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def get_closest_point(
    self, n_segment: None | int, latitude: float, longitude: float
) -> PointDistance:
    """
    Get closest point in a segment or track to the passed latitude and longitude
    corrdinate

    :param n_segment: Index of the segment. If None is passed the whole track is
        considered
    :param latitude: Latitude to check
    :param longitude: Longitude to check

    :return: Tuple containg the point as GPXTrackPoint, the distance from
        the passed coordinates and the index in the segment
    """
    return get_point_distance(self.track, n_segment, latitude, longitude)

get_max_pp_distance(threshold=10)

Get maximum distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
406
407
408
409
410
411
412
413
414
415
def get_max_pp_distance(self, threshold: float = 10) -> float:
    """
    Get maximum distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance("max", threshold)

get_max_pp_distance_in_segment(n_segment=0, threshold=10)

Get maximum distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
417
418
419
420
421
422
423
424
425
426
427
428
429
def get_max_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get maximum distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance_in_segment("max", n_segment, threshold)

get_point_data_in_segmnet(n_segment=0)

Get raw coordinates (latitude, longitude), times and elevations for the segement with the passed index.

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]

tuple with coordinates (latitude, longitude), times and elevations

Source code in geo_track_analyzer/track.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def get_point_data_in_segmnet(
    self, n_segment: int = 0
) -> tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]:
    """Get raw coordinates (latitude, longitude), times and elevations for the
    segement with the passed index.

    :param n_segment: Index of the segement, defaults to 0

    :return: tuple with coordinates (latitude, longitude), times and elevations
    """
    coords = []
    elevations = []
    times = []

    for point in self.track.segments[n_segment].points:
        coords.append((point.latitude, point.longitude))
        if point.elevation is not None:
            elevations.append(point.elevation)
        if point.time is not None:
            times.append(point.time)

    if not elevations:
        elevations = None  # type: ignore
    elif len(coords) != len(elevations):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )
    if not times:
        times = None  # type: ignore
    elif len(coords) != len(times):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )

    return coords, elevations, times

get_segment_data(n_segment=0)

Get processed data for the segmeent with passed index as DataFrame

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
DataFrame

DataFrame with segmenet data

Source code in geo_track_analyzer/track.py
513
514
515
516
517
518
519
520
521
522
def get_segment_data(self, n_segment: int = 0) -> pd.DataFrame:
    """Get processed data for the segmeent with passed index as DataFrame

    :param n_segment: Index of the segement, defaults to 0

    :return: DataFrame with segmenet data
    """
    _, _, _, _, data = self._get_processed_segment_data(n_segment)

    return data

get_segment_overview(n_segment=0)

Get overall metrics for a segment

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for, default to 0

0

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics moving time and distance, total time and distance, maximum and average speed and elevation and cummulated uphill, downholl elevation

Source code in geo_track_analyzer/track.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def get_segment_overview(self, n_segment: int = 0) -> SegmentOverview:
    """
    Get overall metrics for a segment

    :param n_segment: Index of the segment the overview should be generated for,
        default to 0

    :returns: A SegmentOverview object containing the metrics moving time and
        distance, total time and distance, maximum and average speed and elevation
        and cummulated uphill, downholl elevation
    """
    (
        time,
        distance,
        stopped_time,
        stopped_distance,
        data,
    ) = self._get_processed_segment_data(n_segment)

    max_speed = None
    avg_speed = None

    if self.track.segments[n_segment].has_times():
        max_speed = data.speed[data.in_speed_percentile].max()
        avg_speed = data.speed[data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=time,
        distance=distance,
        stopped_time=stopped_time,
        stopped_distance=stopped_distance,
        max_speed=max_speed,
        avg_speed=avg_speed,
        data=data,
    )

get_track_data(connect_segments='forward')

Get processed data for the track as DataFrame. Segment are indicated via the segment column.

Returns:

Type Description
DataFrame

DataFrame with track data

Source code in geo_track_analyzer/track.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
def get_track_data(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> pd.DataFrame:
    """
    Get processed data for the track as DataFrame. Segment are indicated
    via the segment column.

    :return: DataFrame with track data
    """
    track_data: None | pd.DataFrame = None

    _, _, _, _, track_data = self._get_processed_track_data(
        connect_segments=connect_segments
    )

    return track_data

get_track_overview(connect_segments='forward')

Get overall metrics for the track. Equivalent to the sum of all segments

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics

Source code in geo_track_analyzer/track.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def get_track_overview(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> SegmentOverview:
    """
    Get overall metrics for the track. Equivalent to the sum of all segments

    :return: A SegmentOverview object containing the metrics
    """
    (
        track_time,
        track_distance,
        track_stopped_time,
        track_stopped_distance,
        track_data,
    ) = self._get_processed_track_data(connect_segments=connect_segments)

    track_max_speed = None
    track_avg_speed = None

    if (
        all(seg.has_times() for seg in self.track.segments)
        and not track_data.speed.isna().all()
    ):
        track_max_speed = track_data.speed[track_data.in_speed_percentile].max()
        track_avg_speed = track_data.speed[track_data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=track_time,
        distance=track_distance,
        stopped_time=track_stopped_time,
        stopped_distance=track_stopped_distance,
        max_speed=track_max_speed,
        avg_speed=track_avg_speed,
        data=track_data,  # type: ignore
    )

get_xml(name=None, email=None)

Get track as .gpx file data

Parameters:

Name Type Description Default
name None | str

Optional author name to be added to gpx file, defaults to None

None
email None | str

Optional auther e-mail address to be added to the gpx file, defaults to None

None

Returns:

Type Description
str

Content of a gpx file

Source code in geo_track_analyzer/track.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def get_xml(self, name: None | str = None, email: None | str = None) -> str:
    """Get track as .gpx file data

    :param name: Optional author name to be added to gpx file, defaults to None
    :param email: Optional auther e-mail address to be added to the gpx file,
        defaults to None

    :return: Content of a gpx file
    """
    gpx = GPX()

    gpx.tracks = [self.track]
    gpx.author_name = name
    gpx.author_email = email

    return gpx.to_xml()

interpolate_points_in_segment(spacing, n_segment=0, copy_extensions='copy-forward')

Add additdion points to a segment by interpolating along the direct line between each point pair according to the passed spacing parameter. If present, elevation and time will be linearly interpolated. Extensions (Heartrate, Cadence, Power) will be interpolated according to value of copy_extensions. Optionas are:

  • copy the value from the start point of the interpolation (copy-forward)
  • Use value of start point for first half and last point for second half (meet-center)
  • Linear interpolation (linear)

Parameters:

Name Type Description Default
spacing float

Minimum distance between points added by the interpolation

required
n_segment int

segment in the track to use, defaults to 0

0
copy_extensions Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points.

'copy-forward'
Source code in geo_track_analyzer/track.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def interpolate_points_in_segment(
    self,
    spacing: float,
    n_segment: int = 0,
    copy_extensions: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> None:
    """
    Add additdion points to a segment by interpolating along the direct line
    between each point pair according to the passed spacing parameter. If present,
    elevation and time will be linearly interpolated. Extensions (Heartrate,
    Cadence, Power) will be interpolated according to value of copy_extensions.
    Optionas are:

    - copy the value from the start point of the interpolation (copy-forward)
    - Use value of start point for first half and last point for second half
      (meet-center)
    - Linear interpolation (linear)


    :param spacing: Minimum distance between points added by the interpolation
    :param n_segment: segment in the track to use, defaults to 0
    :param copy_extensions: How should the extenstion (if present) be defined in the
        interpolated points.
    """
    self.track.segments[n_segment] = interpolate_segment(
        self.track.segments[n_segment], spacing, copy_extensions=copy_extensions
    )

    # Reset saved processed data
    for key in self._processed_track_data:
        self._processed_track_data.pop(key)
    if n_segment in self._processed_segment_data:
        logger.debug(
            "Deleting saved processed segment data for segment %s", n_segment
        )
        self._processed_segment_data.pop(n_segment)

plot(kind, *, segment=None, reduce_pp_intervals=None, use_distance_segments=None, **kwargs)

Visualize the full track or a single segment.

Parameters:

Name Type Description Default
kind Literal['profile', 'profile-slope', 'map-line', 'map-line-enhanced', 'map-segments', 'zone-summary', 'segment-zone-summary', 'segment-box', 'segment-summary', 'metrics']

Kind of plot to be generated - profile: Elevation profile of the track. May be enhanced with additional information like Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_2d - profile-slope: Elevation profile with slopes between points. Use the reduce_pp_intervals argument to reduce the number of slope intervals. Pass keyword args for plot_track_with_slope - map-line: Visualize coordinates on the map. Pass keyword args for plot_track_line_on_map - map-line-enhanced: Visualize coordinates on the map. Enhance with additional information like Elevation, Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_enriched_on_map - map-segments: Visualize coordinates on the map split into segments. Pass keyword args for plot_segments_on_map - zone_summary : Visualize an aggregate (time, distance, speed) value for a metric (heartrate, power, cadence) with defined zones. Pass keyword args for plot_track_zones, aggregate and metric are required. - segment_zone_summary : Same as "zone-summary" but split aggregate per segment plot_segment_zones - segment_box : Box plot of a metric (heartrate, power, cadence, speed, elevation) per segment. Pass keyword args for plot_segments_on_map metric is required. - segment_summary : Visualize a aggregate (total_time, total_distance, avg_speed, max_speed) per segment. Pass keyword args for plot_segment_summary aggregate is required. - metrics: Visualize the progression of of a metric (elevation, heartrate, power, cadence, power) over the course of a track. Can be plotted over distance and duration.

required
segment None | int | list[int]

Select a specific segment, multiple segments or all segmenets, defaults to None

None
reduce_pp_intervals None | int

Optionally pass a distance in m which is used to reduce the points in a track, defaults to None

None
use_distance_segments None | float

Ignore all segments in data and split full track into segments with passed cummulated distance in meters. If passed, segment arg must be None. Defaults to None.

None

Returns:

Type Description
Figure

Figure (plotly)

Raises:

Type Description
VisualizationSetupError

If the plot prequisites are not met

Source code in geo_track_analyzer/track.py
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ],
    *,
    segment: None | int | list[int] = None,
    reduce_pp_intervals: None | int = None,
    use_distance_segments: None | float = None,
    **kwargs,
) -> Figure:
    """
    Visualize the full track or a single segment.

    :param kind: Kind of plot to be generated

        - profile: Elevation profile of the track. May be enhanced with additional
          information like Velocity, Heartrate, Cadence, and Power. Pass keyword
          args for [`plot_track_2d`][geo_track_analyzer.visualize.plot_track_2d]
        - profile-slope: Elevation profile with slopes between points. Use the
          reduce_pp_intervals argument to reduce the number of slope intervals.
          Pass keyword args for
          [`plot_track_with_slope`][geo_track_analyzer.visualize.plot_track_with_slope]
        - map-line: Visualize coordinates on the map. Pass keyword args for
          [`plot_track_line_on_map`][geo_track_analyzer.visualize.plot_track_line_on_map]
        - map-line-enhanced: Visualize coordinates on the map. Enhance with
          additional information like Elevation, Velocity, Heartrate, Cadence, and
          Power. Pass keyword args for [`plot_track_enriched_on_map`][geo_track_analyzer.visualize.plot_track_enriched_on_map]
        - map-segments: Visualize coordinates on the map split into segments.
          Pass keyword args for
          [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
        - zone_summary : Visualize an aggregate (time, distance, speed) value for a
            metric (heartrate, power, cadence) with defined zones. Pass keyword args
            for [`plot_track_zones`][geo_track_analyzer.visualize.plot_track_zones],
            `aggregate` and `metric` are required.
        - segment_zone_summary : Same as "zone-summary" but split aggregate per
            segment [`plot_segment_zones`][geo_track_analyzer.visualize.plot_segment_zones]
        - segment_box : Box plot of a metric (heartrate, power, cadence, speed,
            elevation) per segment. Pass keyword args for [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
            `metric` is required.
        - segment_summary : Visualize a aggregate (total_time, total_distance,
            avg_speed, max_speed) per segment. Pass keyword args for [`plot_segment_summary`][geo_track_analyzer.visualize.plot_segment_summary]
            `aggregate` is required.
        - metrics: Visualize the progression of of a metric (elevation, heartrate,
            power, cadence, power) over the course of a track. Can be plotted over distance
            and duration.
    :param segment: Select a specific segment, multiple segments or all segmenets,
        defaults to None
    :param reduce_pp_intervals: Optionally pass a distance in m which is used to
        reduce the points in a track, defaults to None
    :param use_distance_segments: Ignore all segments in data and split full track
        into segments with passed cummulated distance in meters. If passed, segment
        arg must be None. Defaults to None.
    :raises VisualizationSetupError: If the plot prequisites are not met

    :return: Figure (plotly)
    """
    from geo_track_analyzer.utils.track import generate_distance_segments

    if use_distance_segments is not None and segment is not None:
        raise VisualizationSetupError(
            f"use_distance_segments {use_distance_segments} cannot be passed with "
            f"segment {segment}."
        )

    valid_kinds = [
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ]

    if "_" in kind and kind.replace("_", "-") in valid_kinds:
        warnings.warn(
            "Found %s but in versions >=2 only %s will be supported"
            % (kind, kind.replace("_", "-")),
            DeprecationWarning,
        )
        kind = kind.replace("_", "-")  # type: ignore

    require_elevation = ["profile", "profile-slope"]
    connect_segment_full = ["map-segments"]
    if kind not in valid_kinds:
        raise VisualizationSetupError(
            f"Kind {kind} is not valid. Pass on of {','.join(valid_kinds)}"
        )

    if kind in ["zone-summary", "segment-zone-summary"] and not all(
        key in kwargs for key in ["metric", "aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** and **aggregate** need to be passed"
        )
    if kind in ["segment-box"] and not all(key in kwargs for key in ["metric"]):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )
    if kind in ["segment-summary"] and not all(
        key in kwargs for key in ["aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )

    require_extensions = (
        set()
        if self.require_data_extensions is None
        else copy(self.require_data_extensions)
    )
    require_extensions.update({"heartrate", "power", "cadence"})

    if segment is None:
        from geo_track_analyzer.utils.track import extract_track_data_for_plot

        data = extract_track_data_for_plot(
            track=self,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            connect_segments="full" if kind in connect_segment_full else "forward",
            extensions=self.extensions,
        )
    elif isinstance(segment, int):
        from geo_track_analyzer.utils.track import extract_segment_data_for_plot

        data = extract_segment_data_for_plot(
            track=self,
            segment=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )
    else:
        from geo_track_analyzer.utils.track import (
            extract_multiple_segment_data_for_plot,
        )

        data = extract_multiple_segment_data_for_plot(
            track=self,
            segments=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )

    # Special case if all points are stopped. Since we still want
    # a plot, we interpret it as all moving. This is expected behaviour
    # for tracks that all have the same lat/lon coordinates
    if (data.moving == False).all():  # noqa: E712
        data.moving = True
        data.cum_time_moving = data.cum_time_stopped

    if use_distance_segments is not None:
        data = generate_distance_segments(data, use_distance_segments)

    fig: Figure
    if kind == "profile":
        fig = plot_track_2d(data=data, **kwargs)
    elif kind == "profile-slope":
        fig = plot_track_with_slope(data=data, **kwargs)
    elif kind == "map-line":
        fig = plot_track_line_on_map(data=data, **kwargs)
    elif kind == "map-line-enhanced":
        fig = plot_track_enriched_on_map(data=data, **kwargs)
    elif kind == "map-segments":
        fig = plot_segments_on_map(data=data, **kwargs)
    elif kind == "zone-summary":
        fig = plot_track_zones(data=data, **kwargs)
    elif kind == "segment-zone-summary":
        fig = plot_segment_zones(data=data, **kwargs)
    elif kind == "segment-summary":
        fig = plot_segment_summary(data=data, **kwargs)
    elif kind == "segment-box":
        fig = plot_segment_box_summary(data=data, **kwargs)
    elif kind == "metrics":
        fig = plot_metrics(data=data, **kwargs)

    return fig

remove_segement(n_segment, merge='before')

Remove a given segment from the track and merge it with the previous or next segment. Will return False, of the passed parameters lead to not possible operation.

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for,

required
merge Literal['before', 'after']

Direction of the merge. Possible values of "before" and "after".

'before'

Returns:

Type Description
bool

Boolean value reflecting if a segment was removed

Source code in geo_track_analyzer/track.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def remove_segement(
    self, n_segment: int, merge: Literal["before", "after"] = "before"
) -> bool:
    """
    Remove a given segment from the track and merge it with the previous or next
    segment. Will return False, of the passed parameters lead to not possible
    operation.

    :param n_segment: Index of the segment the overview should be generated for,
    :param merge: Direction of the merge. Possible values of "before" and "after".

    :return: Boolean value reflecting if a segment was removed
    """
    assert merge in ["before", "after"]
    if n_segment == 0 and merge != "after":
        logger.error("First segement can only be merged with the after method")
        return False
    if merge == "after" and n_segment == len(self.track.segments) - 1:
        logger.error("Last segment can only be merged with the before method")
        return False
    try:
        self.track.segments[n_segment]
    except IndexError:
        logger.error(
            "Cannot remove segment %s. No valid key in segments", n_segment
        )
        return False

    if merge == "after":
        idx_end = None
        if _points_eq(
            self.track.segments[n_segment].points[-1],
            self.track.segments[n_segment + 1].points[0],
        ):
            idx_end = -1
        self.track.segments[n_segment + 1].points = (
            self.track.segments[n_segment].points[0:idx_end]
            + self.track.segments[n_segment + 1].points
        )
        self.track.segments.pop(n_segment)
        return True
    else:
        idx_start = 0
        if _points_eq(
            self.track.segments[n_segment].points[0],
            self.track.segments[n_segment - 1].points[-1],
        ):
            idx_start = 1
        self.track.segments[n_segment - 1].points = (
            self.track.segments[n_segment - 1].points
            + self.track.segments[n_segment].points[idx_start:]
        )
        self.track.segments.pop(n_segment)
        return True

split(coords, distance_threshold=20)

Split the track at the passed coordinates. The distance_threshold parameter defines the maximum distance between the passed coordingates and the closest point in the track.

Parameters:

Name Type Description Default
coords tuple[float, float]

Latitude, Longitude point at which the split should be made

required
distance_threshold float

Maximum distance between coords and closest point, defaults to 20

20

Raises:

Type Description
TrackTransformationError

If distance exceeds threshold

Source code in geo_track_analyzer/track.py
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
def split(
    self, coords: tuple[float, float], distance_threshold: float = 20
) -> None:
    """
    Split the track at the passed coordinates. The distance_threshold parameter
    defines the maximum distance between the passed coordingates and the closest
    point in the track.

    :param coords: Latitude, Longitude point at which the split should be made
    :param distance_threshold: Maximum distance between coords and closest point,
        defaults to 20

    :raises TrackTransformationError: If distance exceeds threshold
    """
    lat, long = coords
    point_distance = get_point_distance(
        self.track, None, latitude=lat, longitude=long
    )

    if point_distance.distance > distance_threshold:
        raise TrackTransformationError(
            f"Closes point in track has distance {point_distance.distance:.2f}m "
            "from passed coordingates"
        )
    # Split the segment. The closest point should be the first
    # point of the second segment
    pre_segment, post_segment = self.track.segments[
        point_distance.segment_idx
    ].split(point_distance.segment_point_idx - 1)

    self.track.segments[point_distance.segment_idx] = pre_segment
    self.track.segments.insert(point_distance.segment_idx + 1, post_segment)

    self._processed_segment_data = {}
    self._processed_track_data = {}

strip_segements()

Strip all segments from the track. Duplicate points at the segmentment boardes will be dropped.

Source code in geo_track_analyzer/track.py
122
123
124
125
126
127
128
129
130
131
def strip_segements(self) -> bool:
    """
    Strip all segments from the track. Duplicate points at the segmentment boardes
    will be dropped.
    """
    while len(self.track.segments) != 1:
        if not self.remove_segement(len(self.track.segments) - 1, "before"):
            return False

    return True

FITTrack(source, stopped_speed_threshold=1, max_speed_percentile=95, strict_elevation_loading=False, require_data_extensions=None, heartrate_zones=None, power_zones=None, cadence_zones=None)

Bases: Track

Track that should be initialized by loading a .fit file

Load a .fit file and extract the data into a Track object. NOTE: Tested with Wahoo devices only

Parameters:

Name Type Description Default
source str | bytes

Path to fit file or byte representation of fit file

required
stopped_speed_threshold float

Minium speed required for a point to be count as moving, defaults to 1

1
max_speed_percentile int

Points with speed outside of the percentile are not counted when analyzing the track, defaults to 95

95
strict_elevation_loading bool

If set, only points are added to the track that have a valid elevation,defaults to False

False
heartrate_zones None | Zones

Optional heartrate Zones, defaults to None

None
power_zones None | Zones

Optional power Zones, defaults to None

None
cadence_zones None | Zones

Optional cadence Zones, defaults to None

None
Source code in geo_track_analyzer/track.py
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
def __init__(
    self,
    source: str | bytes,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    strict_elevation_loading: bool = False,
    require_data_extensions: set[str] | None = None,
    heartrate_zones: None | Zones = None,
    power_zones: None | Zones = None,
    cadence_zones: None | Zones = None,
) -> None:
    """Load a .fit file and extract the data into a Track object.
    NOTE: Tested with Wahoo devices only

    :param source: Path to fit file or byte representation of fit file
    :param stopped_speed_threshold: Minium speed required for a point to be count
        as moving, defaults to 1
    :param max_speed_percentile: Points with speed outside of the percentile are not
        counted when analyzing the track, defaults to 95
    :param strict_elevation_loading: If set, only points are added to the track that
        have a valid elevation,defaults to False
    :param heartrate_zones: Optional heartrate Zones, defaults to None
    :param power_zones: Optional power Zones, defaults to None
    :param cadence_zones: Optional cadence Zones, defaults to None
    """
    super().__init__(
        stopped_speed_threshold=stopped_speed_threshold,
        max_speed_percentile=max_speed_percentile,
        require_data_extensions=require_data_extensions,
        heartrate_zones=heartrate_zones,
        power_zones=power_zones,
        cadence_zones=cadence_zones,
    )

    if isinstance(source, str):
        logger.info("Loading fit track from file %s", source)
    else:
        logger.info("Using passed bytes data as fit track")

    fit_data = FitFile(
        source,
        data_processor=StandardUnitsDataProcessor(),
    )

    points, elevations, times = [], [], []

    rename_keys = {
        "heart_rate": "heartrate",
        "distance": "raw_distance",
        "speed": "raw_speed",
        "calories": "cum_calories",
    }
    alias_keys = {"enhanced_speed": ["speed"]}
    alias_values = set()
    for value in alias_keys.values():
        alias_values.update(value)

    split_at = set([0])
    extensions = BackFillExtensionDict()
    for record in fit_data.get_messages(("record", "lap")):  # type: ignore
        record: DataMessage  # type: ignore
        if record.mesg_type.name == "lap":
            split_at.add(len(points))
        lat = record.get_value("position_lat")
        long = record.get_value("position_long")
        ele = record.get_value("enhanced_altitude")
        if ele is None and (alt := record.get_value("altitude")) is not None:
            ele = alt
        ts = record.get_value("timestamp")

        check_vals = [lat, long, ts]
        if strict_elevation_loading:
            check_vals.append(ele)

        if any([v is None for v in check_vals]):
            logger.debug(
                "Found records with None value in lat/long/elevation/timestamp "
                " - %s/%s/%s/%s",
                lat,
                long,
                ele,
                ts,
            )
            continue

        record_extensions = {}
        extension_names = []
        for field in record.fields:
            if field.name in [
                "position_long",
                "position_lat",
                "enhanced_altitude",
                "altitude",
                "timestamp",
            ]:
                continue
            extension_names.append(field.name)

        for name in extension_names:
            if name in alias_values:
                continue
            value = record.get_value(name)
            if name in alias_keys and value is None:
                for alias in alias_keys[name]:
                    value = record.get_value(alias)
                    if value is not None:
                        break
            record_extensions[rename_keys.get(name, name)] = value

        extensions.fill(record_extensions)

        points.append((lat, long))
        elevations.append(ele)
        times.append(ts)

    if not strict_elevation_loading and set(elevations) != {None}:
        elevations = fill_list(elevations)

    try:
        session_data: DataMessage = list(fit_data.get_messages("session"))[-1]  # type: ignore
    except IndexError:
        logger.debug("Could not load session data from fit file")
    else:
        self.session_data = {  # type: ignore
            "start_time": session_data.get_value("start_time"),
            "ride_time": session_data.get_value("total_timer_time"),
            "total_time": session_data.get_value("total_elapsed_time"),
            "distance": session_data.get_value("total_distance"),
            "ascent": session_data.get_value("total_ascent"),
            "descent": session_data.get_value("total_descent"),
            "avg_velocity": session_data.get_value("avg_speed"),
            "max_velocity": session_data.get_value("max_speed"),
        }

    split_at = sorted(split_at)
    if len(split_at) == 1:
        split_at.append(len(points))

    gpx = GPX()

    gpx_track = GPXTrack()
    gpx.tracks.append(gpx_track)

    for start_idx, end_idx in pairwise(split_at):
        gpx_segment = GPXTrackSegment()

        _points = points[start_idx:end_idx]
        _elevations = elevations[start_idx:end_idx]
        _times = times[start_idx:end_idx]
        _extensions = {
            key: value[start_idx:end_idx] for key, value in extensions.items()
        }

        for i in range(len(_points)):
            this_extensions = {key: _extensions[key][i] for key in _extensions}
            lat, lng = _points[i]
            this_point = get_extended_track_point(
                lat, lng, _elevations[i], _times[i], this_extensions
            )
            gpx_segment.points.append(this_point)

        gpx_track.segments.append(gpx_segment)

    self._track = gpx.tracks[0]
    self._update_extensions()

add_segmeent(segment)

Add a new segment ot the track

Parameters:

Name Type Description Default
segment GPXTrackSegment

GPXTracksegment to be added

required
Source code in geo_track_analyzer/track.py
114
115
116
117
118
119
120
def add_segmeent(self, segment: GPXTrackSegment) -> None:
    """Add a new segment ot the track

    :param segment: GPXTracksegment to be added
    """
    self.track.segments.append(segment)
    logger.info("Added segment with postition: %s", len(self.track.segments))

find_overlap_with_segment(n_segment, match_track, match_track_segment=0, width=50, overlap_threshold=0.75, max_queue_normalize=5, merge_subsegments=5, extensions_interpolation='copy-forward')

Find overlap of a segment of the track with a segment in another track.

Parameters:

Name Type Description Default
n_segment int

Segment in the track that sould be used as base for the comparison

required
match_track Track

Track object containing the segment to be matched

required
match_track_segment int

Segment on the passed track that should be matched to the segment in this track, defaults to 0

0
width float

Width (in meters) of the grid that will be filled to estimate the overalp , defaults to 50

50
overlap_threshold float

Minimum overlap (as fracrtion) required to return the overlap data, defaults to 0.75

0.75
max_queue_normalize int

Minimum number of successive points in the segment between two points falling into same plate bin, defaults to 5

5
merge_subsegments int

Number of points between sub segments allowed for merging the segments, defaults to 5

5
extensions_interpolation Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points, defaults to copy-forward

'copy-forward'

Returns:

Type Description
Sequence[tuple[Track, float, bool]]

Tuple containing a Track with the overlapping points, the overlap in percent, and the direction of the overlap

Source code in geo_track_analyzer/track.py
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
def find_overlap_with_segment(
    self,
    n_segment: int,
    match_track: Track,
    match_track_segment: int = 0,
    width: float = 50,
    overlap_threshold: float = 0.75,
    max_queue_normalize: int = 5,
    merge_subsegments: int = 5,
    extensions_interpolation: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> Sequence[tuple[Track, float, bool]]:
    """Find overlap of a segment of the track with a segment in another track.

    :param n_segment: Segment in the track that sould be used as base for the
        comparison
    :param match_track: Track object containing the segment to be matched
    :param match_track_segment: Segment on the passed track that should be matched
        to the segment in this track, defaults to 0
    :param width: Width (in meters) of the grid that will be filled to estimate
        the overalp , defaults to 50
    :param overlap_threshold: Minimum overlap (as fracrtion) required to return the
        overlap data, defaults to 0.75
    :param max_queue_normalize: Minimum number of successive points in the segment
        between two points falling into same plate bin, defaults to 5
    :param merge_subsegments: Number of points between sub segments allowed
        for merging the segments, defaults to 5
    :param extensions_interpolation: How should the extenstion (if present) be
        defined in the interpolated points, defaults to copy-forward

    :return: Tuple containing a Track with the overlapping points, the overlap in
        percent, and the direction of the overlap
    """
    max_distance_self = self.get_max_pp_distance_in_segment(n_segment)

    segment_self = self.track.segments[n_segment]
    if max_distance_self > width:
        segment_self = interpolate_segment(
            segment_self, width / 2, copy_extensions=extensions_interpolation
        )

    max_distance_match = match_track.get_max_pp_distance_in_segment(
        match_track_segment
    )
    segment_match = match_track.track.segments[match_track_segment]
    if max_distance_match > width:
        segment_match = interpolate_segment(
            segment_match, width / 2, copy_extensions=extensions_interpolation
        )

    logger.info("Looking for overlapping segments")
    segment_overlaps = get_segment_overlap(
        segment_self,
        segment_match,
        width,
        max_queue_normalize,
        merge_subsegments,
        overlap_threshold,
    )

    matched_tracks: list[tuple[Track, float, bool]] = []
    for overlap in segment_overlaps:
        logger.info("Found: %s", overlap)
        matched_segment = GPXTrackSegment()
        # TODO: Might need to go up to overlap.end_idx + 1?
        matched_segment.points = self.track.segments[n_segment].points[
            overlap.start_idx : overlap.end_idx
        ]
        matched_tracks.append(
            (
                SegmentTrack(
                    matched_segment,
                    stopped_speed_threshold=self.stopped_speed_threshold,
                    max_speed_percentile=self.max_speed_percentile,
                ),
                overlap.overlap,
                overlap.inverse,
            )
        )
    return matched_tracks

get_avg_pp_distance(threshold=10)

Get average distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
379
380
381
382
383
384
385
386
387
388
def get_avg_pp_distance(self, threshold: float = 10) -> float:
    """
    Get average distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance("average", threshold)

get_avg_pp_distance_in_segment(n_segment=0, threshold=10)

Get average distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def get_avg_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get average distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance_in_segment(
        "average", n_segment, threshold
    )

get_closest_point(n_segment, latitude, longitude)

Get closest point in a segment or track to the passed latitude and longitude corrdinate

Parameters:

Name Type Description Default
n_segment None | int

Index of the segment. If None is passed the whole track is considered

required
latitude float

Latitude to check

required
longitude float

Longitude to check

required

Returns:

Type Description
PointDistance

Tuple containg the point as GPXTrackPoint, the distance from the passed coordinates and the index in the segment

Source code in geo_track_analyzer/track.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def get_closest_point(
    self, n_segment: None | int, latitude: float, longitude: float
) -> PointDistance:
    """
    Get closest point in a segment or track to the passed latitude and longitude
    corrdinate

    :param n_segment: Index of the segment. If None is passed the whole track is
        considered
    :param latitude: Latitude to check
    :param longitude: Longitude to check

    :return: Tuple containg the point as GPXTrackPoint, the distance from
        the passed coordinates and the index in the segment
    """
    return get_point_distance(self.track, n_segment, latitude, longitude)

get_max_pp_distance(threshold=10)

Get maximum distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
406
407
408
409
410
411
412
413
414
415
def get_max_pp_distance(self, threshold: float = 10) -> float:
    """
    Get maximum distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance("max", threshold)

get_max_pp_distance_in_segment(n_segment=0, threshold=10)

Get maximum distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
417
418
419
420
421
422
423
424
425
426
427
428
429
def get_max_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get maximum distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance_in_segment("max", n_segment, threshold)

get_point_data_in_segmnet(n_segment=0)

Get raw coordinates (latitude, longitude), times and elevations for the segement with the passed index.

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]

tuple with coordinates (latitude, longitude), times and elevations

Source code in geo_track_analyzer/track.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def get_point_data_in_segmnet(
    self, n_segment: int = 0
) -> tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]:
    """Get raw coordinates (latitude, longitude), times and elevations for the
    segement with the passed index.

    :param n_segment: Index of the segement, defaults to 0

    :return: tuple with coordinates (latitude, longitude), times and elevations
    """
    coords = []
    elevations = []
    times = []

    for point in self.track.segments[n_segment].points:
        coords.append((point.latitude, point.longitude))
        if point.elevation is not None:
            elevations.append(point.elevation)
        if point.time is not None:
            times.append(point.time)

    if not elevations:
        elevations = None  # type: ignore
    elif len(coords) != len(elevations):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )
    if not times:
        times = None  # type: ignore
    elif len(coords) != len(times):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )

    return coords, elevations, times

get_segment_data(n_segment=0)

Get processed data for the segmeent with passed index as DataFrame

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
DataFrame

DataFrame with segmenet data

Source code in geo_track_analyzer/track.py
513
514
515
516
517
518
519
520
521
522
def get_segment_data(self, n_segment: int = 0) -> pd.DataFrame:
    """Get processed data for the segmeent with passed index as DataFrame

    :param n_segment: Index of the segement, defaults to 0

    :return: DataFrame with segmenet data
    """
    _, _, _, _, data = self._get_processed_segment_data(n_segment)

    return data

get_segment_overview(n_segment=0)

Get overall metrics for a segment

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for, default to 0

0

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics moving time and distance, total time and distance, maximum and average speed and elevation and cummulated uphill, downholl elevation

Source code in geo_track_analyzer/track.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def get_segment_overview(self, n_segment: int = 0) -> SegmentOverview:
    """
    Get overall metrics for a segment

    :param n_segment: Index of the segment the overview should be generated for,
        default to 0

    :returns: A SegmentOverview object containing the metrics moving time and
        distance, total time and distance, maximum and average speed and elevation
        and cummulated uphill, downholl elevation
    """
    (
        time,
        distance,
        stopped_time,
        stopped_distance,
        data,
    ) = self._get_processed_segment_data(n_segment)

    max_speed = None
    avg_speed = None

    if self.track.segments[n_segment].has_times():
        max_speed = data.speed[data.in_speed_percentile].max()
        avg_speed = data.speed[data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=time,
        distance=distance,
        stopped_time=stopped_time,
        stopped_distance=stopped_distance,
        max_speed=max_speed,
        avg_speed=avg_speed,
        data=data,
    )

get_track_data(connect_segments='forward')

Get processed data for the track as DataFrame. Segment are indicated via the segment column.

Returns:

Type Description
DataFrame

DataFrame with track data

Source code in geo_track_analyzer/track.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
def get_track_data(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> pd.DataFrame:
    """
    Get processed data for the track as DataFrame. Segment are indicated
    via the segment column.

    :return: DataFrame with track data
    """
    track_data: None | pd.DataFrame = None

    _, _, _, _, track_data = self._get_processed_track_data(
        connect_segments=connect_segments
    )

    return track_data

get_track_overview(connect_segments='forward')

Get overall metrics for the track. Equivalent to the sum of all segments

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics

Source code in geo_track_analyzer/track.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def get_track_overview(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> SegmentOverview:
    """
    Get overall metrics for the track. Equivalent to the sum of all segments

    :return: A SegmentOverview object containing the metrics
    """
    (
        track_time,
        track_distance,
        track_stopped_time,
        track_stopped_distance,
        track_data,
    ) = self._get_processed_track_data(connect_segments=connect_segments)

    track_max_speed = None
    track_avg_speed = None

    if (
        all(seg.has_times() for seg in self.track.segments)
        and not track_data.speed.isna().all()
    ):
        track_max_speed = track_data.speed[track_data.in_speed_percentile].max()
        track_avg_speed = track_data.speed[track_data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=track_time,
        distance=track_distance,
        stopped_time=track_stopped_time,
        stopped_distance=track_stopped_distance,
        max_speed=track_max_speed,
        avg_speed=track_avg_speed,
        data=track_data,  # type: ignore
    )

get_xml(name=None, email=None)

Get track as .gpx file data

Parameters:

Name Type Description Default
name None | str

Optional author name to be added to gpx file, defaults to None

None
email None | str

Optional auther e-mail address to be added to the gpx file, defaults to None

None

Returns:

Type Description
str

Content of a gpx file

Source code in geo_track_analyzer/track.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def get_xml(self, name: None | str = None, email: None | str = None) -> str:
    """Get track as .gpx file data

    :param name: Optional author name to be added to gpx file, defaults to None
    :param email: Optional auther e-mail address to be added to the gpx file,
        defaults to None

    :return: Content of a gpx file
    """
    gpx = GPX()

    gpx.tracks = [self.track]
    gpx.author_name = name
    gpx.author_email = email

    return gpx.to_xml()

interpolate_points_in_segment(spacing, n_segment=0, copy_extensions='copy-forward')

Add additdion points to a segment by interpolating along the direct line between each point pair according to the passed spacing parameter. If present, elevation and time will be linearly interpolated. Extensions (Heartrate, Cadence, Power) will be interpolated according to value of copy_extensions. Optionas are:

  • copy the value from the start point of the interpolation (copy-forward)
  • Use value of start point for first half and last point for second half (meet-center)
  • Linear interpolation (linear)

Parameters:

Name Type Description Default
spacing float

Minimum distance between points added by the interpolation

required
n_segment int

segment in the track to use, defaults to 0

0
copy_extensions Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points.

'copy-forward'
Source code in geo_track_analyzer/track.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def interpolate_points_in_segment(
    self,
    spacing: float,
    n_segment: int = 0,
    copy_extensions: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> None:
    """
    Add additdion points to a segment by interpolating along the direct line
    between each point pair according to the passed spacing parameter. If present,
    elevation and time will be linearly interpolated. Extensions (Heartrate,
    Cadence, Power) will be interpolated according to value of copy_extensions.
    Optionas are:

    - copy the value from the start point of the interpolation (copy-forward)
    - Use value of start point for first half and last point for second half
      (meet-center)
    - Linear interpolation (linear)


    :param spacing: Minimum distance between points added by the interpolation
    :param n_segment: segment in the track to use, defaults to 0
    :param copy_extensions: How should the extenstion (if present) be defined in the
        interpolated points.
    """
    self.track.segments[n_segment] = interpolate_segment(
        self.track.segments[n_segment], spacing, copy_extensions=copy_extensions
    )

    # Reset saved processed data
    for key in self._processed_track_data:
        self._processed_track_data.pop(key)
    if n_segment in self._processed_segment_data:
        logger.debug(
            "Deleting saved processed segment data for segment %s", n_segment
        )
        self._processed_segment_data.pop(n_segment)

plot(kind, *, segment=None, reduce_pp_intervals=None, use_distance_segments=None, **kwargs)

Visualize the full track or a single segment.

Parameters:

Name Type Description Default
kind Literal['profile', 'profile-slope', 'map-line', 'map-line-enhanced', 'map-segments', 'zone-summary', 'segment-zone-summary', 'segment-box', 'segment-summary', 'metrics']

Kind of plot to be generated - profile: Elevation profile of the track. May be enhanced with additional information like Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_2d - profile-slope: Elevation profile with slopes between points. Use the reduce_pp_intervals argument to reduce the number of slope intervals. Pass keyword args for plot_track_with_slope - map-line: Visualize coordinates on the map. Pass keyword args for plot_track_line_on_map - map-line-enhanced: Visualize coordinates on the map. Enhance with additional information like Elevation, Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_enriched_on_map - map-segments: Visualize coordinates on the map split into segments. Pass keyword args for plot_segments_on_map - zone_summary : Visualize an aggregate (time, distance, speed) value for a metric (heartrate, power, cadence) with defined zones. Pass keyword args for plot_track_zones, aggregate and metric are required. - segment_zone_summary : Same as "zone-summary" but split aggregate per segment plot_segment_zones - segment_box : Box plot of a metric (heartrate, power, cadence, speed, elevation) per segment. Pass keyword args for plot_segments_on_map metric is required. - segment_summary : Visualize a aggregate (total_time, total_distance, avg_speed, max_speed) per segment. Pass keyword args for plot_segment_summary aggregate is required. - metrics: Visualize the progression of of a metric (elevation, heartrate, power, cadence, power) over the course of a track. Can be plotted over distance and duration.

required
segment None | int | list[int]

Select a specific segment, multiple segments or all segmenets, defaults to None

None
reduce_pp_intervals None | int

Optionally pass a distance in m which is used to reduce the points in a track, defaults to None

None
use_distance_segments None | float

Ignore all segments in data and split full track into segments with passed cummulated distance in meters. If passed, segment arg must be None. Defaults to None.

None

Returns:

Type Description
Figure

Figure (plotly)

Raises:

Type Description
VisualizationSetupError

If the plot prequisites are not met

Source code in geo_track_analyzer/track.py
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ],
    *,
    segment: None | int | list[int] = None,
    reduce_pp_intervals: None | int = None,
    use_distance_segments: None | float = None,
    **kwargs,
) -> Figure:
    """
    Visualize the full track or a single segment.

    :param kind: Kind of plot to be generated

        - profile: Elevation profile of the track. May be enhanced with additional
          information like Velocity, Heartrate, Cadence, and Power. Pass keyword
          args for [`plot_track_2d`][geo_track_analyzer.visualize.plot_track_2d]
        - profile-slope: Elevation profile with slopes between points. Use the
          reduce_pp_intervals argument to reduce the number of slope intervals.
          Pass keyword args for
          [`plot_track_with_slope`][geo_track_analyzer.visualize.plot_track_with_slope]
        - map-line: Visualize coordinates on the map. Pass keyword args for
          [`plot_track_line_on_map`][geo_track_analyzer.visualize.plot_track_line_on_map]
        - map-line-enhanced: Visualize coordinates on the map. Enhance with
          additional information like Elevation, Velocity, Heartrate, Cadence, and
          Power. Pass keyword args for [`plot_track_enriched_on_map`][geo_track_analyzer.visualize.plot_track_enriched_on_map]
        - map-segments: Visualize coordinates on the map split into segments.
          Pass keyword args for
          [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
        - zone_summary : Visualize an aggregate (time, distance, speed) value for a
            metric (heartrate, power, cadence) with defined zones. Pass keyword args
            for [`plot_track_zones`][geo_track_analyzer.visualize.plot_track_zones],
            `aggregate` and `metric` are required.
        - segment_zone_summary : Same as "zone-summary" but split aggregate per
            segment [`plot_segment_zones`][geo_track_analyzer.visualize.plot_segment_zones]
        - segment_box : Box plot of a metric (heartrate, power, cadence, speed,
            elevation) per segment. Pass keyword args for [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
            `metric` is required.
        - segment_summary : Visualize a aggregate (total_time, total_distance,
            avg_speed, max_speed) per segment. Pass keyword args for [`plot_segment_summary`][geo_track_analyzer.visualize.plot_segment_summary]
            `aggregate` is required.
        - metrics: Visualize the progression of of a metric (elevation, heartrate,
            power, cadence, power) over the course of a track. Can be plotted over distance
            and duration.
    :param segment: Select a specific segment, multiple segments or all segmenets,
        defaults to None
    :param reduce_pp_intervals: Optionally pass a distance in m which is used to
        reduce the points in a track, defaults to None
    :param use_distance_segments: Ignore all segments in data and split full track
        into segments with passed cummulated distance in meters. If passed, segment
        arg must be None. Defaults to None.
    :raises VisualizationSetupError: If the plot prequisites are not met

    :return: Figure (plotly)
    """
    from geo_track_analyzer.utils.track import generate_distance_segments

    if use_distance_segments is not None and segment is not None:
        raise VisualizationSetupError(
            f"use_distance_segments {use_distance_segments} cannot be passed with "
            f"segment {segment}."
        )

    valid_kinds = [
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ]

    if "_" in kind and kind.replace("_", "-") in valid_kinds:
        warnings.warn(
            "Found %s but in versions >=2 only %s will be supported"
            % (kind, kind.replace("_", "-")),
            DeprecationWarning,
        )
        kind = kind.replace("_", "-")  # type: ignore

    require_elevation = ["profile", "profile-slope"]
    connect_segment_full = ["map-segments"]
    if kind not in valid_kinds:
        raise VisualizationSetupError(
            f"Kind {kind} is not valid. Pass on of {','.join(valid_kinds)}"
        )

    if kind in ["zone-summary", "segment-zone-summary"] and not all(
        key in kwargs for key in ["metric", "aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** and **aggregate** need to be passed"
        )
    if kind in ["segment-box"] and not all(key in kwargs for key in ["metric"]):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )
    if kind in ["segment-summary"] and not all(
        key in kwargs for key in ["aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )

    require_extensions = (
        set()
        if self.require_data_extensions is None
        else copy(self.require_data_extensions)
    )
    require_extensions.update({"heartrate", "power", "cadence"})

    if segment is None:
        from geo_track_analyzer.utils.track import extract_track_data_for_plot

        data = extract_track_data_for_plot(
            track=self,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            connect_segments="full" if kind in connect_segment_full else "forward",
            extensions=self.extensions,
        )
    elif isinstance(segment, int):
        from geo_track_analyzer.utils.track import extract_segment_data_for_plot

        data = extract_segment_data_for_plot(
            track=self,
            segment=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )
    else:
        from geo_track_analyzer.utils.track import (
            extract_multiple_segment_data_for_plot,
        )

        data = extract_multiple_segment_data_for_plot(
            track=self,
            segments=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )

    # Special case if all points are stopped. Since we still want
    # a plot, we interpret it as all moving. This is expected behaviour
    # for tracks that all have the same lat/lon coordinates
    if (data.moving == False).all():  # noqa: E712
        data.moving = True
        data.cum_time_moving = data.cum_time_stopped

    if use_distance_segments is not None:
        data = generate_distance_segments(data, use_distance_segments)

    fig: Figure
    if kind == "profile":
        fig = plot_track_2d(data=data, **kwargs)
    elif kind == "profile-slope":
        fig = plot_track_with_slope(data=data, **kwargs)
    elif kind == "map-line":
        fig = plot_track_line_on_map(data=data, **kwargs)
    elif kind == "map-line-enhanced":
        fig = plot_track_enriched_on_map(data=data, **kwargs)
    elif kind == "map-segments":
        fig = plot_segments_on_map(data=data, **kwargs)
    elif kind == "zone-summary":
        fig = plot_track_zones(data=data, **kwargs)
    elif kind == "segment-zone-summary":
        fig = plot_segment_zones(data=data, **kwargs)
    elif kind == "segment-summary":
        fig = plot_segment_summary(data=data, **kwargs)
    elif kind == "segment-box":
        fig = plot_segment_box_summary(data=data, **kwargs)
    elif kind == "metrics":
        fig = plot_metrics(data=data, **kwargs)

    return fig

remove_segement(n_segment, merge='before')

Remove a given segment from the track and merge it with the previous or next segment. Will return False, of the passed parameters lead to not possible operation.

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for,

required
merge Literal['before', 'after']

Direction of the merge. Possible values of "before" and "after".

'before'

Returns:

Type Description
bool

Boolean value reflecting if a segment was removed

Source code in geo_track_analyzer/track.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def remove_segement(
    self, n_segment: int, merge: Literal["before", "after"] = "before"
) -> bool:
    """
    Remove a given segment from the track and merge it with the previous or next
    segment. Will return False, of the passed parameters lead to not possible
    operation.

    :param n_segment: Index of the segment the overview should be generated for,
    :param merge: Direction of the merge. Possible values of "before" and "after".

    :return: Boolean value reflecting if a segment was removed
    """
    assert merge in ["before", "after"]
    if n_segment == 0 and merge != "after":
        logger.error("First segement can only be merged with the after method")
        return False
    if merge == "after" and n_segment == len(self.track.segments) - 1:
        logger.error("Last segment can only be merged with the before method")
        return False
    try:
        self.track.segments[n_segment]
    except IndexError:
        logger.error(
            "Cannot remove segment %s. No valid key in segments", n_segment
        )
        return False

    if merge == "after":
        idx_end = None
        if _points_eq(
            self.track.segments[n_segment].points[-1],
            self.track.segments[n_segment + 1].points[0],
        ):
            idx_end = -1
        self.track.segments[n_segment + 1].points = (
            self.track.segments[n_segment].points[0:idx_end]
            + self.track.segments[n_segment + 1].points
        )
        self.track.segments.pop(n_segment)
        return True
    else:
        idx_start = 0
        if _points_eq(
            self.track.segments[n_segment].points[0],
            self.track.segments[n_segment - 1].points[-1],
        ):
            idx_start = 1
        self.track.segments[n_segment - 1].points = (
            self.track.segments[n_segment - 1].points
            + self.track.segments[n_segment].points[idx_start:]
        )
        self.track.segments.pop(n_segment)
        return True

split(coords, distance_threshold=20)

Split the track at the passed coordinates. The distance_threshold parameter defines the maximum distance between the passed coordingates and the closest point in the track.

Parameters:

Name Type Description Default
coords tuple[float, float]

Latitude, Longitude point at which the split should be made

required
distance_threshold float

Maximum distance between coords and closest point, defaults to 20

20

Raises:

Type Description
TrackTransformationError

If distance exceeds threshold

Source code in geo_track_analyzer/track.py
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
def split(
    self, coords: tuple[float, float], distance_threshold: float = 20
) -> None:
    """
    Split the track at the passed coordinates. The distance_threshold parameter
    defines the maximum distance between the passed coordingates and the closest
    point in the track.

    :param coords: Latitude, Longitude point at which the split should be made
    :param distance_threshold: Maximum distance between coords and closest point,
        defaults to 20

    :raises TrackTransformationError: If distance exceeds threshold
    """
    lat, long = coords
    point_distance = get_point_distance(
        self.track, None, latitude=lat, longitude=long
    )

    if point_distance.distance > distance_threshold:
        raise TrackTransformationError(
            f"Closes point in track has distance {point_distance.distance:.2f}m "
            "from passed coordingates"
        )
    # Split the segment. The closest point should be the first
    # point of the second segment
    pre_segment, post_segment = self.track.segments[
        point_distance.segment_idx
    ].split(point_distance.segment_point_idx - 1)

    self.track.segments[point_distance.segment_idx] = pre_segment
    self.track.segments.insert(point_distance.segment_idx + 1, post_segment)

    self._processed_segment_data = {}
    self._processed_track_data = {}

strip_segements()

Strip all segments from the track. Duplicate points at the segmentment boardes will be dropped.

Source code in geo_track_analyzer/track.py
122
123
124
125
126
127
128
129
130
131
def strip_segements(self) -> bool:
    """
    Strip all segments from the track. Duplicate points at the segmentment boardes
    will be dropped.
    """
    while len(self.track.segments) != 1:
        if not self.remove_segement(len(self.track.segments) - 1, "before"):
            return False

    return True

ByteTrack(bytefile, n_track=0, stopped_speed_threshold=1, max_speed_percentile=95, require_data_extensions=None, heartrate_zones=None, power_zones=None, cadence_zones=None)

Bases: Track

Track that should be initialized from a byte stream

Initialize a Track object from a gpx file

Parameters:

Name Type Description Default
bytefile bytes

Bytestring of a gpx file

required
n_track int

Index of track in the gpx file, defaults to 0

0
stopped_speed_threshold float

Minium speed required for a point to be count as moving, defaults to 1

1
max_speed_percentile int

Points with speed outside of the percentile are not counted when analyzing the track, defaults to 95

95
heartrate_zones None | Zones

Optional heartrate Zones, defaults to None

None
power_zones None | Zones

Optional power Zones, defaults to None

None
cadence_zones None | Zones

Optional cadence Zones, defaults to None

None
Source code in geo_track_analyzer/track.py
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
def __init__(
    self,
    bytefile: bytes,
    n_track: int = 0,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    require_data_extensions: set[str] | None = None,
    heartrate_zones: None | Zones = None,
    power_zones: None | Zones = None,
    cadence_zones: None | Zones = None,
) -> None:
    """Initialize a Track object from a gpx file

    :param bytefile: Bytestring of a gpx file
    :param n_track: Index of track in the gpx file, defaults to 0
    :param stopped_speed_threshold: Minium speed required for a point to be count
        as moving, defaults to 1
    :param max_speed_percentile: Points with speed outside of the percentile are not
        counted when analyzing the track, defaults to 95
    :param heartrate_zones: Optional heartrate Zones, defaults to None
    :param power_zones: Optional power Zones, defaults to None
    :param cadence_zones: Optional cadence Zones, defaults to None
    """
    super().__init__(
        stopped_speed_threshold=stopped_speed_threshold,
        max_speed_percentile=max_speed_percentile,
        require_data_extensions=require_data_extensions,
        heartrate_zones=heartrate_zones,
        power_zones=power_zones,
        cadence_zones=cadence_zones,
    )

    gpx = gpxpy.parse(bytefile)

    self._track = gpx.tracks[n_track]
    self._update_extensions()

add_segmeent(segment)

Add a new segment ot the track

Parameters:

Name Type Description Default
segment GPXTrackSegment

GPXTracksegment to be added

required
Source code in geo_track_analyzer/track.py
114
115
116
117
118
119
120
def add_segmeent(self, segment: GPXTrackSegment) -> None:
    """Add a new segment ot the track

    :param segment: GPXTracksegment to be added
    """
    self.track.segments.append(segment)
    logger.info("Added segment with postition: %s", len(self.track.segments))

find_overlap_with_segment(n_segment, match_track, match_track_segment=0, width=50, overlap_threshold=0.75, max_queue_normalize=5, merge_subsegments=5, extensions_interpolation='copy-forward')

Find overlap of a segment of the track with a segment in another track.

Parameters:

Name Type Description Default
n_segment int

Segment in the track that sould be used as base for the comparison

required
match_track Track

Track object containing the segment to be matched

required
match_track_segment int

Segment on the passed track that should be matched to the segment in this track, defaults to 0

0
width float

Width (in meters) of the grid that will be filled to estimate the overalp , defaults to 50

50
overlap_threshold float

Minimum overlap (as fracrtion) required to return the overlap data, defaults to 0.75

0.75
max_queue_normalize int

Minimum number of successive points in the segment between two points falling into same plate bin, defaults to 5

5
merge_subsegments int

Number of points between sub segments allowed for merging the segments, defaults to 5

5
extensions_interpolation Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points, defaults to copy-forward

'copy-forward'

Returns:

Type Description
Sequence[tuple[Track, float, bool]]

Tuple containing a Track with the overlapping points, the overlap in percent, and the direction of the overlap

Source code in geo_track_analyzer/track.py
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
def find_overlap_with_segment(
    self,
    n_segment: int,
    match_track: Track,
    match_track_segment: int = 0,
    width: float = 50,
    overlap_threshold: float = 0.75,
    max_queue_normalize: int = 5,
    merge_subsegments: int = 5,
    extensions_interpolation: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> Sequence[tuple[Track, float, bool]]:
    """Find overlap of a segment of the track with a segment in another track.

    :param n_segment: Segment in the track that sould be used as base for the
        comparison
    :param match_track: Track object containing the segment to be matched
    :param match_track_segment: Segment on the passed track that should be matched
        to the segment in this track, defaults to 0
    :param width: Width (in meters) of the grid that will be filled to estimate
        the overalp , defaults to 50
    :param overlap_threshold: Minimum overlap (as fracrtion) required to return the
        overlap data, defaults to 0.75
    :param max_queue_normalize: Minimum number of successive points in the segment
        between two points falling into same plate bin, defaults to 5
    :param merge_subsegments: Number of points between sub segments allowed
        for merging the segments, defaults to 5
    :param extensions_interpolation: How should the extenstion (if present) be
        defined in the interpolated points, defaults to copy-forward

    :return: Tuple containing a Track with the overlapping points, the overlap in
        percent, and the direction of the overlap
    """
    max_distance_self = self.get_max_pp_distance_in_segment(n_segment)

    segment_self = self.track.segments[n_segment]
    if max_distance_self > width:
        segment_self = interpolate_segment(
            segment_self, width / 2, copy_extensions=extensions_interpolation
        )

    max_distance_match = match_track.get_max_pp_distance_in_segment(
        match_track_segment
    )
    segment_match = match_track.track.segments[match_track_segment]
    if max_distance_match > width:
        segment_match = interpolate_segment(
            segment_match, width / 2, copy_extensions=extensions_interpolation
        )

    logger.info("Looking for overlapping segments")
    segment_overlaps = get_segment_overlap(
        segment_self,
        segment_match,
        width,
        max_queue_normalize,
        merge_subsegments,
        overlap_threshold,
    )

    matched_tracks: list[tuple[Track, float, bool]] = []
    for overlap in segment_overlaps:
        logger.info("Found: %s", overlap)
        matched_segment = GPXTrackSegment()
        # TODO: Might need to go up to overlap.end_idx + 1?
        matched_segment.points = self.track.segments[n_segment].points[
            overlap.start_idx : overlap.end_idx
        ]
        matched_tracks.append(
            (
                SegmentTrack(
                    matched_segment,
                    stopped_speed_threshold=self.stopped_speed_threshold,
                    max_speed_percentile=self.max_speed_percentile,
                ),
                overlap.overlap,
                overlap.inverse,
            )
        )
    return matched_tracks

get_avg_pp_distance(threshold=10)

Get average distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
379
380
381
382
383
384
385
386
387
388
def get_avg_pp_distance(self, threshold: float = 10) -> float:
    """
    Get average distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance("average", threshold)

get_avg_pp_distance_in_segment(n_segment=0, threshold=10)

Get average distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def get_avg_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get average distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance_in_segment(
        "average", n_segment, threshold
    )

get_closest_point(n_segment, latitude, longitude)

Get closest point in a segment or track to the passed latitude and longitude corrdinate

Parameters:

Name Type Description Default
n_segment None | int

Index of the segment. If None is passed the whole track is considered

required
latitude float

Latitude to check

required
longitude float

Longitude to check

required

Returns:

Type Description
PointDistance

Tuple containg the point as GPXTrackPoint, the distance from the passed coordinates and the index in the segment

Source code in geo_track_analyzer/track.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def get_closest_point(
    self, n_segment: None | int, latitude: float, longitude: float
) -> PointDistance:
    """
    Get closest point in a segment or track to the passed latitude and longitude
    corrdinate

    :param n_segment: Index of the segment. If None is passed the whole track is
        considered
    :param latitude: Latitude to check
    :param longitude: Longitude to check

    :return: Tuple containg the point as GPXTrackPoint, the distance from
        the passed coordinates and the index in the segment
    """
    return get_point_distance(self.track, n_segment, latitude, longitude)

get_max_pp_distance(threshold=10)

Get maximum distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
406
407
408
409
410
411
412
413
414
415
def get_max_pp_distance(self, threshold: float = 10) -> float:
    """
    Get maximum distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance("max", threshold)

get_max_pp_distance_in_segment(n_segment=0, threshold=10)

Get maximum distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
417
418
419
420
421
422
423
424
425
426
427
428
429
def get_max_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get maximum distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance_in_segment("max", n_segment, threshold)

get_point_data_in_segmnet(n_segment=0)

Get raw coordinates (latitude, longitude), times and elevations for the segement with the passed index.

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]

tuple with coordinates (latitude, longitude), times and elevations

Source code in geo_track_analyzer/track.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def get_point_data_in_segmnet(
    self, n_segment: int = 0
) -> tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]:
    """Get raw coordinates (latitude, longitude), times and elevations for the
    segement with the passed index.

    :param n_segment: Index of the segement, defaults to 0

    :return: tuple with coordinates (latitude, longitude), times and elevations
    """
    coords = []
    elevations = []
    times = []

    for point in self.track.segments[n_segment].points:
        coords.append((point.latitude, point.longitude))
        if point.elevation is not None:
            elevations.append(point.elevation)
        if point.time is not None:
            times.append(point.time)

    if not elevations:
        elevations = None  # type: ignore
    elif len(coords) != len(elevations):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )
    if not times:
        times = None  # type: ignore
    elif len(coords) != len(times):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )

    return coords, elevations, times

get_segment_data(n_segment=0)

Get processed data for the segmeent with passed index as DataFrame

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
DataFrame

DataFrame with segmenet data

Source code in geo_track_analyzer/track.py
513
514
515
516
517
518
519
520
521
522
def get_segment_data(self, n_segment: int = 0) -> pd.DataFrame:
    """Get processed data for the segmeent with passed index as DataFrame

    :param n_segment: Index of the segement, defaults to 0

    :return: DataFrame with segmenet data
    """
    _, _, _, _, data = self._get_processed_segment_data(n_segment)

    return data

get_segment_overview(n_segment=0)

Get overall metrics for a segment

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for, default to 0

0

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics moving time and distance, total time and distance, maximum and average speed and elevation and cummulated uphill, downholl elevation

Source code in geo_track_analyzer/track.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def get_segment_overview(self, n_segment: int = 0) -> SegmentOverview:
    """
    Get overall metrics for a segment

    :param n_segment: Index of the segment the overview should be generated for,
        default to 0

    :returns: A SegmentOverview object containing the metrics moving time and
        distance, total time and distance, maximum and average speed and elevation
        and cummulated uphill, downholl elevation
    """
    (
        time,
        distance,
        stopped_time,
        stopped_distance,
        data,
    ) = self._get_processed_segment_data(n_segment)

    max_speed = None
    avg_speed = None

    if self.track.segments[n_segment].has_times():
        max_speed = data.speed[data.in_speed_percentile].max()
        avg_speed = data.speed[data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=time,
        distance=distance,
        stopped_time=stopped_time,
        stopped_distance=stopped_distance,
        max_speed=max_speed,
        avg_speed=avg_speed,
        data=data,
    )

get_track_data(connect_segments='forward')

Get processed data for the track as DataFrame. Segment are indicated via the segment column.

Returns:

Type Description
DataFrame

DataFrame with track data

Source code in geo_track_analyzer/track.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
def get_track_data(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> pd.DataFrame:
    """
    Get processed data for the track as DataFrame. Segment are indicated
    via the segment column.

    :return: DataFrame with track data
    """
    track_data: None | pd.DataFrame = None

    _, _, _, _, track_data = self._get_processed_track_data(
        connect_segments=connect_segments
    )

    return track_data

get_track_overview(connect_segments='forward')

Get overall metrics for the track. Equivalent to the sum of all segments

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics

Source code in geo_track_analyzer/track.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def get_track_overview(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> SegmentOverview:
    """
    Get overall metrics for the track. Equivalent to the sum of all segments

    :return: A SegmentOverview object containing the metrics
    """
    (
        track_time,
        track_distance,
        track_stopped_time,
        track_stopped_distance,
        track_data,
    ) = self._get_processed_track_data(connect_segments=connect_segments)

    track_max_speed = None
    track_avg_speed = None

    if (
        all(seg.has_times() for seg in self.track.segments)
        and not track_data.speed.isna().all()
    ):
        track_max_speed = track_data.speed[track_data.in_speed_percentile].max()
        track_avg_speed = track_data.speed[track_data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=track_time,
        distance=track_distance,
        stopped_time=track_stopped_time,
        stopped_distance=track_stopped_distance,
        max_speed=track_max_speed,
        avg_speed=track_avg_speed,
        data=track_data,  # type: ignore
    )

get_xml(name=None, email=None)

Get track as .gpx file data

Parameters:

Name Type Description Default
name None | str

Optional author name to be added to gpx file, defaults to None

None
email None | str

Optional auther e-mail address to be added to the gpx file, defaults to None

None

Returns:

Type Description
str

Content of a gpx file

Source code in geo_track_analyzer/track.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def get_xml(self, name: None | str = None, email: None | str = None) -> str:
    """Get track as .gpx file data

    :param name: Optional author name to be added to gpx file, defaults to None
    :param email: Optional auther e-mail address to be added to the gpx file,
        defaults to None

    :return: Content of a gpx file
    """
    gpx = GPX()

    gpx.tracks = [self.track]
    gpx.author_name = name
    gpx.author_email = email

    return gpx.to_xml()

interpolate_points_in_segment(spacing, n_segment=0, copy_extensions='copy-forward')

Add additdion points to a segment by interpolating along the direct line between each point pair according to the passed spacing parameter. If present, elevation and time will be linearly interpolated. Extensions (Heartrate, Cadence, Power) will be interpolated according to value of copy_extensions. Optionas are:

  • copy the value from the start point of the interpolation (copy-forward)
  • Use value of start point for first half and last point for second half (meet-center)
  • Linear interpolation (linear)

Parameters:

Name Type Description Default
spacing float

Minimum distance between points added by the interpolation

required
n_segment int

segment in the track to use, defaults to 0

0
copy_extensions Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points.

'copy-forward'
Source code in geo_track_analyzer/track.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def interpolate_points_in_segment(
    self,
    spacing: float,
    n_segment: int = 0,
    copy_extensions: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> None:
    """
    Add additdion points to a segment by interpolating along the direct line
    between each point pair according to the passed spacing parameter. If present,
    elevation and time will be linearly interpolated. Extensions (Heartrate,
    Cadence, Power) will be interpolated according to value of copy_extensions.
    Optionas are:

    - copy the value from the start point of the interpolation (copy-forward)
    - Use value of start point for first half and last point for second half
      (meet-center)
    - Linear interpolation (linear)


    :param spacing: Minimum distance between points added by the interpolation
    :param n_segment: segment in the track to use, defaults to 0
    :param copy_extensions: How should the extenstion (if present) be defined in the
        interpolated points.
    """
    self.track.segments[n_segment] = interpolate_segment(
        self.track.segments[n_segment], spacing, copy_extensions=copy_extensions
    )

    # Reset saved processed data
    for key in self._processed_track_data:
        self._processed_track_data.pop(key)
    if n_segment in self._processed_segment_data:
        logger.debug(
            "Deleting saved processed segment data for segment %s", n_segment
        )
        self._processed_segment_data.pop(n_segment)

plot(kind, *, segment=None, reduce_pp_intervals=None, use_distance_segments=None, **kwargs)

Visualize the full track or a single segment.

Parameters:

Name Type Description Default
kind Literal['profile', 'profile-slope', 'map-line', 'map-line-enhanced', 'map-segments', 'zone-summary', 'segment-zone-summary', 'segment-box', 'segment-summary', 'metrics']

Kind of plot to be generated - profile: Elevation profile of the track. May be enhanced with additional information like Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_2d - profile-slope: Elevation profile with slopes between points. Use the reduce_pp_intervals argument to reduce the number of slope intervals. Pass keyword args for plot_track_with_slope - map-line: Visualize coordinates on the map. Pass keyword args for plot_track_line_on_map - map-line-enhanced: Visualize coordinates on the map. Enhance with additional information like Elevation, Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_enriched_on_map - map-segments: Visualize coordinates on the map split into segments. Pass keyword args for plot_segments_on_map - zone_summary : Visualize an aggregate (time, distance, speed) value for a metric (heartrate, power, cadence) with defined zones. Pass keyword args for plot_track_zones, aggregate and metric are required. - segment_zone_summary : Same as "zone-summary" but split aggregate per segment plot_segment_zones - segment_box : Box plot of a metric (heartrate, power, cadence, speed, elevation) per segment. Pass keyword args for plot_segments_on_map metric is required. - segment_summary : Visualize a aggregate (total_time, total_distance, avg_speed, max_speed) per segment. Pass keyword args for plot_segment_summary aggregate is required. - metrics: Visualize the progression of of a metric (elevation, heartrate, power, cadence, power) over the course of a track. Can be plotted over distance and duration.

required
segment None | int | list[int]

Select a specific segment, multiple segments or all segmenets, defaults to None

None
reduce_pp_intervals None | int

Optionally pass a distance in m which is used to reduce the points in a track, defaults to None

None
use_distance_segments None | float

Ignore all segments in data and split full track into segments with passed cummulated distance in meters. If passed, segment arg must be None. Defaults to None.

None

Returns:

Type Description
Figure

Figure (plotly)

Raises:

Type Description
VisualizationSetupError

If the plot prequisites are not met

Source code in geo_track_analyzer/track.py
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ],
    *,
    segment: None | int | list[int] = None,
    reduce_pp_intervals: None | int = None,
    use_distance_segments: None | float = None,
    **kwargs,
) -> Figure:
    """
    Visualize the full track or a single segment.

    :param kind: Kind of plot to be generated

        - profile: Elevation profile of the track. May be enhanced with additional
          information like Velocity, Heartrate, Cadence, and Power. Pass keyword
          args for [`plot_track_2d`][geo_track_analyzer.visualize.plot_track_2d]
        - profile-slope: Elevation profile with slopes between points. Use the
          reduce_pp_intervals argument to reduce the number of slope intervals.
          Pass keyword args for
          [`plot_track_with_slope`][geo_track_analyzer.visualize.plot_track_with_slope]
        - map-line: Visualize coordinates on the map. Pass keyword args for
          [`plot_track_line_on_map`][geo_track_analyzer.visualize.plot_track_line_on_map]
        - map-line-enhanced: Visualize coordinates on the map. Enhance with
          additional information like Elevation, Velocity, Heartrate, Cadence, and
          Power. Pass keyword args for [`plot_track_enriched_on_map`][geo_track_analyzer.visualize.plot_track_enriched_on_map]
        - map-segments: Visualize coordinates on the map split into segments.
          Pass keyword args for
          [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
        - zone_summary : Visualize an aggregate (time, distance, speed) value for a
            metric (heartrate, power, cadence) with defined zones. Pass keyword args
            for [`plot_track_zones`][geo_track_analyzer.visualize.plot_track_zones],
            `aggregate` and `metric` are required.
        - segment_zone_summary : Same as "zone-summary" but split aggregate per
            segment [`plot_segment_zones`][geo_track_analyzer.visualize.plot_segment_zones]
        - segment_box : Box plot of a metric (heartrate, power, cadence, speed,
            elevation) per segment. Pass keyword args for [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
            `metric` is required.
        - segment_summary : Visualize a aggregate (total_time, total_distance,
            avg_speed, max_speed) per segment. Pass keyword args for [`plot_segment_summary`][geo_track_analyzer.visualize.plot_segment_summary]
            `aggregate` is required.
        - metrics: Visualize the progression of of a metric (elevation, heartrate,
            power, cadence, power) over the course of a track. Can be plotted over distance
            and duration.
    :param segment: Select a specific segment, multiple segments or all segmenets,
        defaults to None
    :param reduce_pp_intervals: Optionally pass a distance in m which is used to
        reduce the points in a track, defaults to None
    :param use_distance_segments: Ignore all segments in data and split full track
        into segments with passed cummulated distance in meters. If passed, segment
        arg must be None. Defaults to None.
    :raises VisualizationSetupError: If the plot prequisites are not met

    :return: Figure (plotly)
    """
    from geo_track_analyzer.utils.track import generate_distance_segments

    if use_distance_segments is not None and segment is not None:
        raise VisualizationSetupError(
            f"use_distance_segments {use_distance_segments} cannot be passed with "
            f"segment {segment}."
        )

    valid_kinds = [
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ]

    if "_" in kind and kind.replace("_", "-") in valid_kinds:
        warnings.warn(
            "Found %s but in versions >=2 only %s will be supported"
            % (kind, kind.replace("_", "-")),
            DeprecationWarning,
        )
        kind = kind.replace("_", "-")  # type: ignore

    require_elevation = ["profile", "profile-slope"]
    connect_segment_full = ["map-segments"]
    if kind not in valid_kinds:
        raise VisualizationSetupError(
            f"Kind {kind} is not valid. Pass on of {','.join(valid_kinds)}"
        )

    if kind in ["zone-summary", "segment-zone-summary"] and not all(
        key in kwargs for key in ["metric", "aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** and **aggregate** need to be passed"
        )
    if kind in ["segment-box"] and not all(key in kwargs for key in ["metric"]):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )
    if kind in ["segment-summary"] and not all(
        key in kwargs for key in ["aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )

    require_extensions = (
        set()
        if self.require_data_extensions is None
        else copy(self.require_data_extensions)
    )
    require_extensions.update({"heartrate", "power", "cadence"})

    if segment is None:
        from geo_track_analyzer.utils.track import extract_track_data_for_plot

        data = extract_track_data_for_plot(
            track=self,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            connect_segments="full" if kind in connect_segment_full else "forward",
            extensions=self.extensions,
        )
    elif isinstance(segment, int):
        from geo_track_analyzer.utils.track import extract_segment_data_for_plot

        data = extract_segment_data_for_plot(
            track=self,
            segment=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )
    else:
        from geo_track_analyzer.utils.track import (
            extract_multiple_segment_data_for_plot,
        )

        data = extract_multiple_segment_data_for_plot(
            track=self,
            segments=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )

    # Special case if all points are stopped. Since we still want
    # a plot, we interpret it as all moving. This is expected behaviour
    # for tracks that all have the same lat/lon coordinates
    if (data.moving == False).all():  # noqa: E712
        data.moving = True
        data.cum_time_moving = data.cum_time_stopped

    if use_distance_segments is not None:
        data = generate_distance_segments(data, use_distance_segments)

    fig: Figure
    if kind == "profile":
        fig = plot_track_2d(data=data, **kwargs)
    elif kind == "profile-slope":
        fig = plot_track_with_slope(data=data, **kwargs)
    elif kind == "map-line":
        fig = plot_track_line_on_map(data=data, **kwargs)
    elif kind == "map-line-enhanced":
        fig = plot_track_enriched_on_map(data=data, **kwargs)
    elif kind == "map-segments":
        fig = plot_segments_on_map(data=data, **kwargs)
    elif kind == "zone-summary":
        fig = plot_track_zones(data=data, **kwargs)
    elif kind == "segment-zone-summary":
        fig = plot_segment_zones(data=data, **kwargs)
    elif kind == "segment-summary":
        fig = plot_segment_summary(data=data, **kwargs)
    elif kind == "segment-box":
        fig = plot_segment_box_summary(data=data, **kwargs)
    elif kind == "metrics":
        fig = plot_metrics(data=data, **kwargs)

    return fig

remove_segement(n_segment, merge='before')

Remove a given segment from the track and merge it with the previous or next segment. Will return False, of the passed parameters lead to not possible operation.

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for,

required
merge Literal['before', 'after']

Direction of the merge. Possible values of "before" and "after".

'before'

Returns:

Type Description
bool

Boolean value reflecting if a segment was removed

Source code in geo_track_analyzer/track.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def remove_segement(
    self, n_segment: int, merge: Literal["before", "after"] = "before"
) -> bool:
    """
    Remove a given segment from the track and merge it with the previous or next
    segment. Will return False, of the passed parameters lead to not possible
    operation.

    :param n_segment: Index of the segment the overview should be generated for,
    :param merge: Direction of the merge. Possible values of "before" and "after".

    :return: Boolean value reflecting if a segment was removed
    """
    assert merge in ["before", "after"]
    if n_segment == 0 and merge != "after":
        logger.error("First segement can only be merged with the after method")
        return False
    if merge == "after" and n_segment == len(self.track.segments) - 1:
        logger.error("Last segment can only be merged with the before method")
        return False
    try:
        self.track.segments[n_segment]
    except IndexError:
        logger.error(
            "Cannot remove segment %s. No valid key in segments", n_segment
        )
        return False

    if merge == "after":
        idx_end = None
        if _points_eq(
            self.track.segments[n_segment].points[-1],
            self.track.segments[n_segment + 1].points[0],
        ):
            idx_end = -1
        self.track.segments[n_segment + 1].points = (
            self.track.segments[n_segment].points[0:idx_end]
            + self.track.segments[n_segment + 1].points
        )
        self.track.segments.pop(n_segment)
        return True
    else:
        idx_start = 0
        if _points_eq(
            self.track.segments[n_segment].points[0],
            self.track.segments[n_segment - 1].points[-1],
        ):
            idx_start = 1
        self.track.segments[n_segment - 1].points = (
            self.track.segments[n_segment - 1].points
            + self.track.segments[n_segment].points[idx_start:]
        )
        self.track.segments.pop(n_segment)
        return True

split(coords, distance_threshold=20)

Split the track at the passed coordinates. The distance_threshold parameter defines the maximum distance between the passed coordingates and the closest point in the track.

Parameters:

Name Type Description Default
coords tuple[float, float]

Latitude, Longitude point at which the split should be made

required
distance_threshold float

Maximum distance between coords and closest point, defaults to 20

20

Raises:

Type Description
TrackTransformationError

If distance exceeds threshold

Source code in geo_track_analyzer/track.py
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
def split(
    self, coords: tuple[float, float], distance_threshold: float = 20
) -> None:
    """
    Split the track at the passed coordinates. The distance_threshold parameter
    defines the maximum distance between the passed coordingates and the closest
    point in the track.

    :param coords: Latitude, Longitude point at which the split should be made
    :param distance_threshold: Maximum distance between coords and closest point,
        defaults to 20

    :raises TrackTransformationError: If distance exceeds threshold
    """
    lat, long = coords
    point_distance = get_point_distance(
        self.track, None, latitude=lat, longitude=long
    )

    if point_distance.distance > distance_threshold:
        raise TrackTransformationError(
            f"Closes point in track has distance {point_distance.distance:.2f}m "
            "from passed coordingates"
        )
    # Split the segment. The closest point should be the first
    # point of the second segment
    pre_segment, post_segment = self.track.segments[
        point_distance.segment_idx
    ].split(point_distance.segment_point_idx - 1)

    self.track.segments[point_distance.segment_idx] = pre_segment
    self.track.segments.insert(point_distance.segment_idx + 1, post_segment)

    self._processed_segment_data = {}
    self._processed_track_data = {}

strip_segements()

Strip all segments from the track. Duplicate points at the segmentment boardes will be dropped.

Source code in geo_track_analyzer/track.py
122
123
124
125
126
127
128
129
130
131
def strip_segements(self) -> bool:
    """
    Strip all segments from the track. Duplicate points at the segmentment boardes
    will be dropped.
    """
    while len(self.track.segments) != 1:
        if not self.remove_segement(len(self.track.segments) - 1, "before"):
            return False

    return True

PyTrack(points, elevations, times, extensions=None, stopped_speed_threshold=1, max_speed_percentile=95, require_data_extensions=None, heartrate_zones=None, power_zones=None, cadence_zones=None)

Bases: Track

Track that should be initialized from python objects

A geospacial data track initialized from python objects

Parameters:

Name Type Description Default
points list[tuple[float, float]]

List of Latitude/Longitude tuples

required
elevations None | list[float]

Optional list of elevation for each point

required
times None | list[datetime]

Optional list of times for each point

required
heartrate

Optional list of heartrate values for each point

required
cadence

Optional list of cadence values for each point

required
power

Optional list of power values for each point

required
stopped_speed_threshold float

Minium speed required for a point to be count as moving, defaults to 1

1
max_speed_percentile int

Points with speed outside of the percentile are not counted when analyzing the track, defaults to 95

95
heartrate_zones None | Zones

Optional heartrate Zones, defaults to None

None
power_zones None | Zones

Optional power Zones, defaults to None

None
cadence_zones None | Zones

Optional cadence Zones, defaults to None

None

Raises:

Type Description
TrackInitializationError

Raised if number of elevation, time, heatrate, or cadence values do not match passed points

Source code in geo_track_analyzer/track.py
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
def __init__(
    self,
    points: list[tuple[float, float]],
    elevations: None | list[float],
    times: None | list[datetime],
    extensions: dict[str, list[N | None] | None] | None = None,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    require_data_extensions: set[str] | None = None,
    heartrate_zones: None | Zones = None,
    power_zones: None | Zones = None,
    cadence_zones: None | Zones = None,
) -> None:
    """A geospacial data track initialized from python objects

    :param points: List of Latitude/Longitude tuples
    :param elevations: Optional list of elevation for each point
    :param times: Optional list of times for each point
    :param heartrate: Optional list of heartrate values for each point
    :param cadence: Optional list of cadence values for each point
    :param power: Optional list of power values for each point
    :param stopped_speed_threshold: Minium speed required for a point to be count
        as moving, defaults to 1
    :param max_speed_percentile: Points with speed outside of the percentile are not
        counted when analyzing the track, defaults to 95
    :param heartrate_zones: Optional heartrate Zones, defaults to None
    :param power_zones: Optional power Zones, defaults to None
    :param cadence_zones: Optional cadence Zones, defaults to None
    :raises TrackInitializationError: Raised if number of elevation, time, heatrate,
        or cadence values do not match passed points
    """
    if extensions is None:
        extensions = dict()

    super().__init__(
        stopped_speed_threshold=stopped_speed_threshold,
        max_speed_percentile=max_speed_percentile,
        require_data_extensions=require_data_extensions,
        heartrate_zones=heartrate_zones,
        power_zones=power_zones,
        cadence_zones=cadence_zones,
        extensions=set(extensions.keys()),
    )

    gpx = GPX()

    gpx_track = GPXTrack()
    gpx.tracks.append(gpx_track)

    gpx_segment = self._create_segmeent(
        points=points,
        elevations=elevations,
        times=times,
        extensions=extensions,
    )

    gpx_track.segments.append(gpx_segment)

    self._track = gpx.tracks[0]

find_overlap_with_segment(n_segment, match_track, match_track_segment=0, width=50, overlap_threshold=0.75, max_queue_normalize=5, merge_subsegments=5, extensions_interpolation='copy-forward')

Find overlap of a segment of the track with a segment in another track.

Parameters:

Name Type Description Default
n_segment int

Segment in the track that sould be used as base for the comparison

required
match_track Track

Track object containing the segment to be matched

required
match_track_segment int

Segment on the passed track that should be matched to the segment in this track, defaults to 0

0
width float

Width (in meters) of the grid that will be filled to estimate the overalp , defaults to 50

50
overlap_threshold float

Minimum overlap (as fracrtion) required to return the overlap data, defaults to 0.75

0.75
max_queue_normalize int

Minimum number of successive points in the segment between two points falling into same plate bin, defaults to 5

5
merge_subsegments int

Number of points between sub segments allowed for merging the segments, defaults to 5

5
extensions_interpolation Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points, defaults to copy-forward

'copy-forward'

Returns:

Type Description
Sequence[tuple[Track, float, bool]]

Tuple containing a Track with the overlapping points, the overlap in percent, and the direction of the overlap

Source code in geo_track_analyzer/track.py
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
def find_overlap_with_segment(
    self,
    n_segment: int,
    match_track: Track,
    match_track_segment: int = 0,
    width: float = 50,
    overlap_threshold: float = 0.75,
    max_queue_normalize: int = 5,
    merge_subsegments: int = 5,
    extensions_interpolation: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> Sequence[tuple[Track, float, bool]]:
    """Find overlap of a segment of the track with a segment in another track.

    :param n_segment: Segment in the track that sould be used as base for the
        comparison
    :param match_track: Track object containing the segment to be matched
    :param match_track_segment: Segment on the passed track that should be matched
        to the segment in this track, defaults to 0
    :param width: Width (in meters) of the grid that will be filled to estimate
        the overalp , defaults to 50
    :param overlap_threshold: Minimum overlap (as fracrtion) required to return the
        overlap data, defaults to 0.75
    :param max_queue_normalize: Minimum number of successive points in the segment
        between two points falling into same plate bin, defaults to 5
    :param merge_subsegments: Number of points between sub segments allowed
        for merging the segments, defaults to 5
    :param extensions_interpolation: How should the extenstion (if present) be
        defined in the interpolated points, defaults to copy-forward

    :return: Tuple containing a Track with the overlapping points, the overlap in
        percent, and the direction of the overlap
    """
    max_distance_self = self.get_max_pp_distance_in_segment(n_segment)

    segment_self = self.track.segments[n_segment]
    if max_distance_self > width:
        segment_self = interpolate_segment(
            segment_self, width / 2, copy_extensions=extensions_interpolation
        )

    max_distance_match = match_track.get_max_pp_distance_in_segment(
        match_track_segment
    )
    segment_match = match_track.track.segments[match_track_segment]
    if max_distance_match > width:
        segment_match = interpolate_segment(
            segment_match, width / 2, copy_extensions=extensions_interpolation
        )

    logger.info("Looking for overlapping segments")
    segment_overlaps = get_segment_overlap(
        segment_self,
        segment_match,
        width,
        max_queue_normalize,
        merge_subsegments,
        overlap_threshold,
    )

    matched_tracks: list[tuple[Track, float, bool]] = []
    for overlap in segment_overlaps:
        logger.info("Found: %s", overlap)
        matched_segment = GPXTrackSegment()
        # TODO: Might need to go up to overlap.end_idx + 1?
        matched_segment.points = self.track.segments[n_segment].points[
            overlap.start_idx : overlap.end_idx
        ]
        matched_tracks.append(
            (
                SegmentTrack(
                    matched_segment,
                    stopped_speed_threshold=self.stopped_speed_threshold,
                    max_speed_percentile=self.max_speed_percentile,
                ),
                overlap.overlap,
                overlap.inverse,
            )
        )
    return matched_tracks

get_avg_pp_distance(threshold=10)

Get average distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
379
380
381
382
383
384
385
386
387
388
def get_avg_pp_distance(self, threshold: float = 10) -> float:
    """
    Get average distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance("average", threshold)

get_avg_pp_distance_in_segment(n_segment=0, threshold=10)

Get average distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def get_avg_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get average distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance_in_segment(
        "average", n_segment, threshold
    )

get_closest_point(n_segment, latitude, longitude)

Get closest point in a segment or track to the passed latitude and longitude corrdinate

Parameters:

Name Type Description Default
n_segment None | int

Index of the segment. If None is passed the whole track is considered

required
latitude float

Latitude to check

required
longitude float

Longitude to check

required

Returns:

Type Description
PointDistance

Tuple containg the point as GPXTrackPoint, the distance from the passed coordinates and the index in the segment

Source code in geo_track_analyzer/track.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def get_closest_point(
    self, n_segment: None | int, latitude: float, longitude: float
) -> PointDistance:
    """
    Get closest point in a segment or track to the passed latitude and longitude
    corrdinate

    :param n_segment: Index of the segment. If None is passed the whole track is
        considered
    :param latitude: Latitude to check
    :param longitude: Longitude to check

    :return: Tuple containg the point as GPXTrackPoint, the distance from
        the passed coordinates and the index in the segment
    """
    return get_point_distance(self.track, n_segment, latitude, longitude)

get_max_pp_distance(threshold=10)

Get maximum distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
406
407
408
409
410
411
412
413
414
415
def get_max_pp_distance(self, threshold: float = 10) -> float:
    """
    Get maximum distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance("max", threshold)

get_max_pp_distance_in_segment(n_segment=0, threshold=10)

Get maximum distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
417
418
419
420
421
422
423
424
425
426
427
428
429
def get_max_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get maximum distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance_in_segment("max", n_segment, threshold)

get_point_data_in_segmnet(n_segment=0)

Get raw coordinates (latitude, longitude), times and elevations for the segement with the passed index.

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]

tuple with coordinates (latitude, longitude), times and elevations

Source code in geo_track_analyzer/track.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def get_point_data_in_segmnet(
    self, n_segment: int = 0
) -> tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]:
    """Get raw coordinates (latitude, longitude), times and elevations for the
    segement with the passed index.

    :param n_segment: Index of the segement, defaults to 0

    :return: tuple with coordinates (latitude, longitude), times and elevations
    """
    coords = []
    elevations = []
    times = []

    for point in self.track.segments[n_segment].points:
        coords.append((point.latitude, point.longitude))
        if point.elevation is not None:
            elevations.append(point.elevation)
        if point.time is not None:
            times.append(point.time)

    if not elevations:
        elevations = None  # type: ignore
    elif len(coords) != len(elevations):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )
    if not times:
        times = None  # type: ignore
    elif len(coords) != len(times):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )

    return coords, elevations, times

get_segment_data(n_segment=0)

Get processed data for the segmeent with passed index as DataFrame

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
DataFrame

DataFrame with segmenet data

Source code in geo_track_analyzer/track.py
513
514
515
516
517
518
519
520
521
522
def get_segment_data(self, n_segment: int = 0) -> pd.DataFrame:
    """Get processed data for the segmeent with passed index as DataFrame

    :param n_segment: Index of the segement, defaults to 0

    :return: DataFrame with segmenet data
    """
    _, _, _, _, data = self._get_processed_segment_data(n_segment)

    return data

get_segment_overview(n_segment=0)

Get overall metrics for a segment

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for, default to 0

0

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics moving time and distance, total time and distance, maximum and average speed and elevation and cummulated uphill, downholl elevation

Source code in geo_track_analyzer/track.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def get_segment_overview(self, n_segment: int = 0) -> SegmentOverview:
    """
    Get overall metrics for a segment

    :param n_segment: Index of the segment the overview should be generated for,
        default to 0

    :returns: A SegmentOverview object containing the metrics moving time and
        distance, total time and distance, maximum and average speed and elevation
        and cummulated uphill, downholl elevation
    """
    (
        time,
        distance,
        stopped_time,
        stopped_distance,
        data,
    ) = self._get_processed_segment_data(n_segment)

    max_speed = None
    avg_speed = None

    if self.track.segments[n_segment].has_times():
        max_speed = data.speed[data.in_speed_percentile].max()
        avg_speed = data.speed[data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=time,
        distance=distance,
        stopped_time=stopped_time,
        stopped_distance=stopped_distance,
        max_speed=max_speed,
        avg_speed=avg_speed,
        data=data,
    )

get_track_data(connect_segments='forward')

Get processed data for the track as DataFrame. Segment are indicated via the segment column.

Returns:

Type Description
DataFrame

DataFrame with track data

Source code in geo_track_analyzer/track.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
def get_track_data(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> pd.DataFrame:
    """
    Get processed data for the track as DataFrame. Segment are indicated
    via the segment column.

    :return: DataFrame with track data
    """
    track_data: None | pd.DataFrame = None

    _, _, _, _, track_data = self._get_processed_track_data(
        connect_segments=connect_segments
    )

    return track_data

get_track_overview(connect_segments='forward')

Get overall metrics for the track. Equivalent to the sum of all segments

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics

Source code in geo_track_analyzer/track.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def get_track_overview(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> SegmentOverview:
    """
    Get overall metrics for the track. Equivalent to the sum of all segments

    :return: A SegmentOverview object containing the metrics
    """
    (
        track_time,
        track_distance,
        track_stopped_time,
        track_stopped_distance,
        track_data,
    ) = self._get_processed_track_data(connect_segments=connect_segments)

    track_max_speed = None
    track_avg_speed = None

    if (
        all(seg.has_times() for seg in self.track.segments)
        and not track_data.speed.isna().all()
    ):
        track_max_speed = track_data.speed[track_data.in_speed_percentile].max()
        track_avg_speed = track_data.speed[track_data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=track_time,
        distance=track_distance,
        stopped_time=track_stopped_time,
        stopped_distance=track_stopped_distance,
        max_speed=track_max_speed,
        avg_speed=track_avg_speed,
        data=track_data,  # type: ignore
    )

get_xml(name=None, email=None)

Get track as .gpx file data

Parameters:

Name Type Description Default
name None | str

Optional author name to be added to gpx file, defaults to None

None
email None | str

Optional auther e-mail address to be added to the gpx file, defaults to None

None

Returns:

Type Description
str

Content of a gpx file

Source code in geo_track_analyzer/track.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def get_xml(self, name: None | str = None, email: None | str = None) -> str:
    """Get track as .gpx file data

    :param name: Optional author name to be added to gpx file, defaults to None
    :param email: Optional auther e-mail address to be added to the gpx file,
        defaults to None

    :return: Content of a gpx file
    """
    gpx = GPX()

    gpx.tracks = [self.track]
    gpx.author_name = name
    gpx.author_email = email

    return gpx.to_xml()

interpolate_points_in_segment(spacing, n_segment=0, copy_extensions='copy-forward')

Add additdion points to a segment by interpolating along the direct line between each point pair according to the passed spacing parameter. If present, elevation and time will be linearly interpolated. Extensions (Heartrate, Cadence, Power) will be interpolated according to value of copy_extensions. Optionas are:

  • copy the value from the start point of the interpolation (copy-forward)
  • Use value of start point for first half and last point for second half (meet-center)
  • Linear interpolation (linear)

Parameters:

Name Type Description Default
spacing float

Minimum distance between points added by the interpolation

required
n_segment int

segment in the track to use, defaults to 0

0
copy_extensions Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points.

'copy-forward'
Source code in geo_track_analyzer/track.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def interpolate_points_in_segment(
    self,
    spacing: float,
    n_segment: int = 0,
    copy_extensions: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> None:
    """
    Add additdion points to a segment by interpolating along the direct line
    between each point pair according to the passed spacing parameter. If present,
    elevation and time will be linearly interpolated. Extensions (Heartrate,
    Cadence, Power) will be interpolated according to value of copy_extensions.
    Optionas are:

    - copy the value from the start point of the interpolation (copy-forward)
    - Use value of start point for first half and last point for second half
      (meet-center)
    - Linear interpolation (linear)


    :param spacing: Minimum distance between points added by the interpolation
    :param n_segment: segment in the track to use, defaults to 0
    :param copy_extensions: How should the extenstion (if present) be defined in the
        interpolated points.
    """
    self.track.segments[n_segment] = interpolate_segment(
        self.track.segments[n_segment], spacing, copy_extensions=copy_extensions
    )

    # Reset saved processed data
    for key in self._processed_track_data:
        self._processed_track_data.pop(key)
    if n_segment in self._processed_segment_data:
        logger.debug(
            "Deleting saved processed segment data for segment %s", n_segment
        )
        self._processed_segment_data.pop(n_segment)

plot(kind, *, segment=None, reduce_pp_intervals=None, use_distance_segments=None, **kwargs)

Visualize the full track or a single segment.

Parameters:

Name Type Description Default
kind Literal['profile', 'profile-slope', 'map-line', 'map-line-enhanced', 'map-segments', 'zone-summary', 'segment-zone-summary', 'segment-box', 'segment-summary', 'metrics']

Kind of plot to be generated - profile: Elevation profile of the track. May be enhanced with additional information like Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_2d - profile-slope: Elevation profile with slopes between points. Use the reduce_pp_intervals argument to reduce the number of slope intervals. Pass keyword args for plot_track_with_slope - map-line: Visualize coordinates on the map. Pass keyword args for plot_track_line_on_map - map-line-enhanced: Visualize coordinates on the map. Enhance with additional information like Elevation, Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_enriched_on_map - map-segments: Visualize coordinates on the map split into segments. Pass keyword args for plot_segments_on_map - zone_summary : Visualize an aggregate (time, distance, speed) value for a metric (heartrate, power, cadence) with defined zones. Pass keyword args for plot_track_zones, aggregate and metric are required. - segment_zone_summary : Same as "zone-summary" but split aggregate per segment plot_segment_zones - segment_box : Box plot of a metric (heartrate, power, cadence, speed, elevation) per segment. Pass keyword args for plot_segments_on_map metric is required. - segment_summary : Visualize a aggregate (total_time, total_distance, avg_speed, max_speed) per segment. Pass keyword args for plot_segment_summary aggregate is required. - metrics: Visualize the progression of of a metric (elevation, heartrate, power, cadence, power) over the course of a track. Can be plotted over distance and duration.

required
segment None | int | list[int]

Select a specific segment, multiple segments or all segmenets, defaults to None

None
reduce_pp_intervals None | int

Optionally pass a distance in m which is used to reduce the points in a track, defaults to None

None
use_distance_segments None | float

Ignore all segments in data and split full track into segments with passed cummulated distance in meters. If passed, segment arg must be None. Defaults to None.

None

Returns:

Type Description
Figure

Figure (plotly)

Raises:

Type Description
VisualizationSetupError

If the plot prequisites are not met

Source code in geo_track_analyzer/track.py
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ],
    *,
    segment: None | int | list[int] = None,
    reduce_pp_intervals: None | int = None,
    use_distance_segments: None | float = None,
    **kwargs,
) -> Figure:
    """
    Visualize the full track or a single segment.

    :param kind: Kind of plot to be generated

        - profile: Elevation profile of the track. May be enhanced with additional
          information like Velocity, Heartrate, Cadence, and Power. Pass keyword
          args for [`plot_track_2d`][geo_track_analyzer.visualize.plot_track_2d]
        - profile-slope: Elevation profile with slopes between points. Use the
          reduce_pp_intervals argument to reduce the number of slope intervals.
          Pass keyword args for
          [`plot_track_with_slope`][geo_track_analyzer.visualize.plot_track_with_slope]
        - map-line: Visualize coordinates on the map. Pass keyword args for
          [`plot_track_line_on_map`][geo_track_analyzer.visualize.plot_track_line_on_map]
        - map-line-enhanced: Visualize coordinates on the map. Enhance with
          additional information like Elevation, Velocity, Heartrate, Cadence, and
          Power. Pass keyword args for [`plot_track_enriched_on_map`][geo_track_analyzer.visualize.plot_track_enriched_on_map]
        - map-segments: Visualize coordinates on the map split into segments.
          Pass keyword args for
          [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
        - zone_summary : Visualize an aggregate (time, distance, speed) value for a
            metric (heartrate, power, cadence) with defined zones. Pass keyword args
            for [`plot_track_zones`][geo_track_analyzer.visualize.plot_track_zones],
            `aggregate` and `metric` are required.
        - segment_zone_summary : Same as "zone-summary" but split aggregate per
            segment [`plot_segment_zones`][geo_track_analyzer.visualize.plot_segment_zones]
        - segment_box : Box plot of a metric (heartrate, power, cadence, speed,
            elevation) per segment. Pass keyword args for [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
            `metric` is required.
        - segment_summary : Visualize a aggregate (total_time, total_distance,
            avg_speed, max_speed) per segment. Pass keyword args for [`plot_segment_summary`][geo_track_analyzer.visualize.plot_segment_summary]
            `aggregate` is required.
        - metrics: Visualize the progression of of a metric (elevation, heartrate,
            power, cadence, power) over the course of a track. Can be plotted over distance
            and duration.
    :param segment: Select a specific segment, multiple segments or all segmenets,
        defaults to None
    :param reduce_pp_intervals: Optionally pass a distance in m which is used to
        reduce the points in a track, defaults to None
    :param use_distance_segments: Ignore all segments in data and split full track
        into segments with passed cummulated distance in meters. If passed, segment
        arg must be None. Defaults to None.
    :raises VisualizationSetupError: If the plot prequisites are not met

    :return: Figure (plotly)
    """
    from geo_track_analyzer.utils.track import generate_distance_segments

    if use_distance_segments is not None and segment is not None:
        raise VisualizationSetupError(
            f"use_distance_segments {use_distance_segments} cannot be passed with "
            f"segment {segment}."
        )

    valid_kinds = [
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ]

    if "_" in kind and kind.replace("_", "-") in valid_kinds:
        warnings.warn(
            "Found %s but in versions >=2 only %s will be supported"
            % (kind, kind.replace("_", "-")),
            DeprecationWarning,
        )
        kind = kind.replace("_", "-")  # type: ignore

    require_elevation = ["profile", "profile-slope"]
    connect_segment_full = ["map-segments"]
    if kind not in valid_kinds:
        raise VisualizationSetupError(
            f"Kind {kind} is not valid. Pass on of {','.join(valid_kinds)}"
        )

    if kind in ["zone-summary", "segment-zone-summary"] and not all(
        key in kwargs for key in ["metric", "aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** and **aggregate** need to be passed"
        )
    if kind in ["segment-box"] and not all(key in kwargs for key in ["metric"]):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )
    if kind in ["segment-summary"] and not all(
        key in kwargs for key in ["aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )

    require_extensions = (
        set()
        if self.require_data_extensions is None
        else copy(self.require_data_extensions)
    )
    require_extensions.update({"heartrate", "power", "cadence"})

    if segment is None:
        from geo_track_analyzer.utils.track import extract_track_data_for_plot

        data = extract_track_data_for_plot(
            track=self,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            connect_segments="full" if kind in connect_segment_full else "forward",
            extensions=self.extensions,
        )
    elif isinstance(segment, int):
        from geo_track_analyzer.utils.track import extract_segment_data_for_plot

        data = extract_segment_data_for_plot(
            track=self,
            segment=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )
    else:
        from geo_track_analyzer.utils.track import (
            extract_multiple_segment_data_for_plot,
        )

        data = extract_multiple_segment_data_for_plot(
            track=self,
            segments=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )

    # Special case if all points are stopped. Since we still want
    # a plot, we interpret it as all moving. This is expected behaviour
    # for tracks that all have the same lat/lon coordinates
    if (data.moving == False).all():  # noqa: E712
        data.moving = True
        data.cum_time_moving = data.cum_time_stopped

    if use_distance_segments is not None:
        data = generate_distance_segments(data, use_distance_segments)

    fig: Figure
    if kind == "profile":
        fig = plot_track_2d(data=data, **kwargs)
    elif kind == "profile-slope":
        fig = plot_track_with_slope(data=data, **kwargs)
    elif kind == "map-line":
        fig = plot_track_line_on_map(data=data, **kwargs)
    elif kind == "map-line-enhanced":
        fig = plot_track_enriched_on_map(data=data, **kwargs)
    elif kind == "map-segments":
        fig = plot_segments_on_map(data=data, **kwargs)
    elif kind == "zone-summary":
        fig = plot_track_zones(data=data, **kwargs)
    elif kind == "segment-zone-summary":
        fig = plot_segment_zones(data=data, **kwargs)
    elif kind == "segment-summary":
        fig = plot_segment_summary(data=data, **kwargs)
    elif kind == "segment-box":
        fig = plot_segment_box_summary(data=data, **kwargs)
    elif kind == "metrics":
        fig = plot_metrics(data=data, **kwargs)

    return fig

remove_segement(n_segment, merge='before')

Remove a given segment from the track and merge it with the previous or next segment. Will return False, of the passed parameters lead to not possible operation.

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for,

required
merge Literal['before', 'after']

Direction of the merge. Possible values of "before" and "after".

'before'

Returns:

Type Description
bool

Boolean value reflecting if a segment was removed

Source code in geo_track_analyzer/track.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def remove_segement(
    self, n_segment: int, merge: Literal["before", "after"] = "before"
) -> bool:
    """
    Remove a given segment from the track and merge it with the previous or next
    segment. Will return False, of the passed parameters lead to not possible
    operation.

    :param n_segment: Index of the segment the overview should be generated for,
    :param merge: Direction of the merge. Possible values of "before" and "after".

    :return: Boolean value reflecting if a segment was removed
    """
    assert merge in ["before", "after"]
    if n_segment == 0 and merge != "after":
        logger.error("First segement can only be merged with the after method")
        return False
    if merge == "after" and n_segment == len(self.track.segments) - 1:
        logger.error("Last segment can only be merged with the before method")
        return False
    try:
        self.track.segments[n_segment]
    except IndexError:
        logger.error(
            "Cannot remove segment %s. No valid key in segments", n_segment
        )
        return False

    if merge == "after":
        idx_end = None
        if _points_eq(
            self.track.segments[n_segment].points[-1],
            self.track.segments[n_segment + 1].points[0],
        ):
            idx_end = -1
        self.track.segments[n_segment + 1].points = (
            self.track.segments[n_segment].points[0:idx_end]
            + self.track.segments[n_segment + 1].points
        )
        self.track.segments.pop(n_segment)
        return True
    else:
        idx_start = 0
        if _points_eq(
            self.track.segments[n_segment].points[0],
            self.track.segments[n_segment - 1].points[-1],
        ):
            idx_start = 1
        self.track.segments[n_segment - 1].points = (
            self.track.segments[n_segment - 1].points
            + self.track.segments[n_segment].points[idx_start:]
        )
        self.track.segments.pop(n_segment)
        return True

split(coords, distance_threshold=20)

Split the track at the passed coordinates. The distance_threshold parameter defines the maximum distance between the passed coordingates and the closest point in the track.

Parameters:

Name Type Description Default
coords tuple[float, float]

Latitude, Longitude point at which the split should be made

required
distance_threshold float

Maximum distance between coords and closest point, defaults to 20

20

Raises:

Type Description
TrackTransformationError

If distance exceeds threshold

Source code in geo_track_analyzer/track.py
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
def split(
    self, coords: tuple[float, float], distance_threshold: float = 20
) -> None:
    """
    Split the track at the passed coordinates. The distance_threshold parameter
    defines the maximum distance between the passed coordingates and the closest
    point in the track.

    :param coords: Latitude, Longitude point at which the split should be made
    :param distance_threshold: Maximum distance between coords and closest point,
        defaults to 20

    :raises TrackTransformationError: If distance exceeds threshold
    """
    lat, long = coords
    point_distance = get_point_distance(
        self.track, None, latitude=lat, longitude=long
    )

    if point_distance.distance > distance_threshold:
        raise TrackTransformationError(
            f"Closes point in track has distance {point_distance.distance:.2f}m "
            "from passed coordingates"
        )
    # Split the segment. The closest point should be the first
    # point of the second segment
    pre_segment, post_segment = self.track.segments[
        point_distance.segment_idx
    ].split(point_distance.segment_point_idx - 1)

    self.track.segments[point_distance.segment_idx] = pre_segment
    self.track.segments.insert(point_distance.segment_idx + 1, post_segment)

    self._processed_segment_data = {}
    self._processed_track_data = {}

strip_segements()

Strip all segments from the track. Duplicate points at the segmentment boardes will be dropped.

Source code in geo_track_analyzer/track.py
122
123
124
125
126
127
128
129
130
131
def strip_segements(self) -> bool:
    """
    Strip all segments from the track. Duplicate points at the segmentment boardes
    will be dropped.
    """
    while len(self.track.segments) != 1:
        if not self.remove_segement(len(self.track.segments) - 1, "before"):
            return False

    return True

SegmentTrack(segment, stopped_speed_threshold=1, max_speed_percentile=95, require_data_extensions=None, heartrate_zones=None, power_zones=None, cadence_zones=None)

Bases: Track

Track that should be initialized by loading a PGXTrackSegment object

Wrap a GPXTrackSegment into a Track object

Parameters:

Name Type Description Default
segment GPXTrackSegment

GPXTrackSegment

required
stopped_speed_threshold float

Minium speed required for a point to be count as moving, defaults to 1

1
max_speed_percentile int

Points with speed outside of the percentile are not counted when analyzing the track, defaults to 95

95
heartrate_zones None | Zones

Optional heartrate Zones, defaults to None

None
power_zones None | Zones

Optional power Zones, defaults to None

None
cadence_zones None | Zones

Optional cadence Zones, defaults to None

None
Source code in geo_track_analyzer/track.py
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
def __init__(
    self,
    segment: GPXTrackSegment,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    require_data_extensions: set[str] | None = None,
    heartrate_zones: None | Zones = None,
    power_zones: None | Zones = None,
    cadence_zones: None | Zones = None,
) -> None:
    """Wrap a GPXTrackSegment into a Track object

    :param segment: GPXTrackSegment
    :param stopped_speed_threshold: Minium speed required for a point to be count
        as moving, defaults to 1
    :param max_speed_percentile: Points with speed outside of the percentile are not
        counted when analyzing the track, defaults to 95
    :param heartrate_zones: Optional heartrate Zones, defaults to None
    :param power_zones: Optional power Zones, defaults to None
    :param cadence_zones: Optional cadence Zones, defaults to None
    """
    gpx = GPX()

    gpx_track = GPXTrack()
    gpx.tracks.append(gpx_track)

    gpx_track.segments.append(segment)

    super().__init__(
        stopped_speed_threshold=stopped_speed_threshold,
        max_speed_percentile=max_speed_percentile,
        require_data_extensions=require_data_extensions,
        heartrate_zones=heartrate_zones,
        power_zones=power_zones,
        cadence_zones=cadence_zones,
        extensions=get_extensions_in_points(segment.points),
    )
    self._track = gpx.tracks[0]

add_segmeent(segment)

Add a new segment ot the track

Parameters:

Name Type Description Default
segment GPXTrackSegment

GPXTracksegment to be added

required
Source code in geo_track_analyzer/track.py
114
115
116
117
118
119
120
def add_segmeent(self, segment: GPXTrackSegment) -> None:
    """Add a new segment ot the track

    :param segment: GPXTracksegment to be added
    """
    self.track.segments.append(segment)
    logger.info("Added segment with postition: %s", len(self.track.segments))

find_overlap_with_segment(n_segment, match_track, match_track_segment=0, width=50, overlap_threshold=0.75, max_queue_normalize=5, merge_subsegments=5, extensions_interpolation='copy-forward')

Find overlap of a segment of the track with a segment in another track.

Parameters:

Name Type Description Default
n_segment int

Segment in the track that sould be used as base for the comparison

required
match_track Track

Track object containing the segment to be matched

required
match_track_segment int

Segment on the passed track that should be matched to the segment in this track, defaults to 0

0
width float

Width (in meters) of the grid that will be filled to estimate the overalp , defaults to 50

50
overlap_threshold float

Minimum overlap (as fracrtion) required to return the overlap data, defaults to 0.75

0.75
max_queue_normalize int

Minimum number of successive points in the segment between two points falling into same plate bin, defaults to 5

5
merge_subsegments int

Number of points between sub segments allowed for merging the segments, defaults to 5

5
extensions_interpolation Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points, defaults to copy-forward

'copy-forward'

Returns:

Type Description
Sequence[tuple[Track, float, bool]]

Tuple containing a Track with the overlapping points, the overlap in percent, and the direction of the overlap

Source code in geo_track_analyzer/track.py
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
def find_overlap_with_segment(
    self,
    n_segment: int,
    match_track: Track,
    match_track_segment: int = 0,
    width: float = 50,
    overlap_threshold: float = 0.75,
    max_queue_normalize: int = 5,
    merge_subsegments: int = 5,
    extensions_interpolation: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> Sequence[tuple[Track, float, bool]]:
    """Find overlap of a segment of the track with a segment in another track.

    :param n_segment: Segment in the track that sould be used as base for the
        comparison
    :param match_track: Track object containing the segment to be matched
    :param match_track_segment: Segment on the passed track that should be matched
        to the segment in this track, defaults to 0
    :param width: Width (in meters) of the grid that will be filled to estimate
        the overalp , defaults to 50
    :param overlap_threshold: Minimum overlap (as fracrtion) required to return the
        overlap data, defaults to 0.75
    :param max_queue_normalize: Minimum number of successive points in the segment
        between two points falling into same plate bin, defaults to 5
    :param merge_subsegments: Number of points between sub segments allowed
        for merging the segments, defaults to 5
    :param extensions_interpolation: How should the extenstion (if present) be
        defined in the interpolated points, defaults to copy-forward

    :return: Tuple containing a Track with the overlapping points, the overlap in
        percent, and the direction of the overlap
    """
    max_distance_self = self.get_max_pp_distance_in_segment(n_segment)

    segment_self = self.track.segments[n_segment]
    if max_distance_self > width:
        segment_self = interpolate_segment(
            segment_self, width / 2, copy_extensions=extensions_interpolation
        )

    max_distance_match = match_track.get_max_pp_distance_in_segment(
        match_track_segment
    )
    segment_match = match_track.track.segments[match_track_segment]
    if max_distance_match > width:
        segment_match = interpolate_segment(
            segment_match, width / 2, copy_extensions=extensions_interpolation
        )

    logger.info("Looking for overlapping segments")
    segment_overlaps = get_segment_overlap(
        segment_self,
        segment_match,
        width,
        max_queue_normalize,
        merge_subsegments,
        overlap_threshold,
    )

    matched_tracks: list[tuple[Track, float, bool]] = []
    for overlap in segment_overlaps:
        logger.info("Found: %s", overlap)
        matched_segment = GPXTrackSegment()
        # TODO: Might need to go up to overlap.end_idx + 1?
        matched_segment.points = self.track.segments[n_segment].points[
            overlap.start_idx : overlap.end_idx
        ]
        matched_tracks.append(
            (
                SegmentTrack(
                    matched_segment,
                    stopped_speed_threshold=self.stopped_speed_threshold,
                    max_speed_percentile=self.max_speed_percentile,
                ),
                overlap.overlap,
                overlap.inverse,
            )
        )
    return matched_tracks

get_avg_pp_distance(threshold=10)

Get average distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
379
380
381
382
383
384
385
386
387
388
def get_avg_pp_distance(self, threshold: float = 10) -> float:
    """
    Get average distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance("average", threshold)

get_avg_pp_distance_in_segment(n_segment=0, threshold=10)

Get average distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def get_avg_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get average distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance_in_segment(
        "average", n_segment, threshold
    )

get_closest_point(n_segment, latitude, longitude)

Get closest point in a segment or track to the passed latitude and longitude corrdinate

Parameters:

Name Type Description Default
n_segment None | int

Index of the segment. If None is passed the whole track is considered

required
latitude float

Latitude to check

required
longitude float

Longitude to check

required

Returns:

Type Description
PointDistance

Tuple containg the point as GPXTrackPoint, the distance from the passed coordinates and the index in the segment

Source code in geo_track_analyzer/track.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def get_closest_point(
    self, n_segment: None | int, latitude: float, longitude: float
) -> PointDistance:
    """
    Get closest point in a segment or track to the passed latitude and longitude
    corrdinate

    :param n_segment: Index of the segment. If None is passed the whole track is
        considered
    :param latitude: Latitude to check
    :param longitude: Longitude to check

    :return: Tuple containg the point as GPXTrackPoint, the distance from
        the passed coordinates and the index in the segment
    """
    return get_point_distance(self.track, n_segment, latitude, longitude)

get_max_pp_distance(threshold=10)

Get maximum distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
406
407
408
409
410
411
412
413
414
415
def get_max_pp_distance(self, threshold: float = 10) -> float:
    """
    Get maximum distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance("max", threshold)

get_max_pp_distance_in_segment(n_segment=0, threshold=10)

Get maximum distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
417
418
419
420
421
422
423
424
425
426
427
428
429
def get_max_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get maximum distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance_in_segment("max", n_segment, threshold)

get_point_data_in_segmnet(n_segment=0)

Get raw coordinates (latitude, longitude), times and elevations for the segement with the passed index.

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]

tuple with coordinates (latitude, longitude), times and elevations

Source code in geo_track_analyzer/track.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def get_point_data_in_segmnet(
    self, n_segment: int = 0
) -> tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]:
    """Get raw coordinates (latitude, longitude), times and elevations for the
    segement with the passed index.

    :param n_segment: Index of the segement, defaults to 0

    :return: tuple with coordinates (latitude, longitude), times and elevations
    """
    coords = []
    elevations = []
    times = []

    for point in self.track.segments[n_segment].points:
        coords.append((point.latitude, point.longitude))
        if point.elevation is not None:
            elevations.append(point.elevation)
        if point.time is not None:
            times.append(point.time)

    if not elevations:
        elevations = None  # type: ignore
    elif len(coords) != len(elevations):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )
    if not times:
        times = None  # type: ignore
    elif len(coords) != len(times):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )

    return coords, elevations, times

get_segment_data(n_segment=0)

Get processed data for the segmeent with passed index as DataFrame

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
DataFrame

DataFrame with segmenet data

Source code in geo_track_analyzer/track.py
513
514
515
516
517
518
519
520
521
522
def get_segment_data(self, n_segment: int = 0) -> pd.DataFrame:
    """Get processed data for the segmeent with passed index as DataFrame

    :param n_segment: Index of the segement, defaults to 0

    :return: DataFrame with segmenet data
    """
    _, _, _, _, data = self._get_processed_segment_data(n_segment)

    return data

get_segment_overview(n_segment=0)

Get overall metrics for a segment

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for, default to 0

0

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics moving time and distance, total time and distance, maximum and average speed and elevation and cummulated uphill, downholl elevation

Source code in geo_track_analyzer/track.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def get_segment_overview(self, n_segment: int = 0) -> SegmentOverview:
    """
    Get overall metrics for a segment

    :param n_segment: Index of the segment the overview should be generated for,
        default to 0

    :returns: A SegmentOverview object containing the metrics moving time and
        distance, total time and distance, maximum and average speed and elevation
        and cummulated uphill, downholl elevation
    """
    (
        time,
        distance,
        stopped_time,
        stopped_distance,
        data,
    ) = self._get_processed_segment_data(n_segment)

    max_speed = None
    avg_speed = None

    if self.track.segments[n_segment].has_times():
        max_speed = data.speed[data.in_speed_percentile].max()
        avg_speed = data.speed[data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=time,
        distance=distance,
        stopped_time=stopped_time,
        stopped_distance=stopped_distance,
        max_speed=max_speed,
        avg_speed=avg_speed,
        data=data,
    )

get_track_data(connect_segments='forward')

Get processed data for the track as DataFrame. Segment are indicated via the segment column.

Returns:

Type Description
DataFrame

DataFrame with track data

Source code in geo_track_analyzer/track.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
def get_track_data(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> pd.DataFrame:
    """
    Get processed data for the track as DataFrame. Segment are indicated
    via the segment column.

    :return: DataFrame with track data
    """
    track_data: None | pd.DataFrame = None

    _, _, _, _, track_data = self._get_processed_track_data(
        connect_segments=connect_segments
    )

    return track_data

get_track_overview(connect_segments='forward')

Get overall metrics for the track. Equivalent to the sum of all segments

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics

Source code in geo_track_analyzer/track.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def get_track_overview(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> SegmentOverview:
    """
    Get overall metrics for the track. Equivalent to the sum of all segments

    :return: A SegmentOverview object containing the metrics
    """
    (
        track_time,
        track_distance,
        track_stopped_time,
        track_stopped_distance,
        track_data,
    ) = self._get_processed_track_data(connect_segments=connect_segments)

    track_max_speed = None
    track_avg_speed = None

    if (
        all(seg.has_times() for seg in self.track.segments)
        and not track_data.speed.isna().all()
    ):
        track_max_speed = track_data.speed[track_data.in_speed_percentile].max()
        track_avg_speed = track_data.speed[track_data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=track_time,
        distance=track_distance,
        stopped_time=track_stopped_time,
        stopped_distance=track_stopped_distance,
        max_speed=track_max_speed,
        avg_speed=track_avg_speed,
        data=track_data,  # type: ignore
    )

get_xml(name=None, email=None)

Get track as .gpx file data

Parameters:

Name Type Description Default
name None | str

Optional author name to be added to gpx file, defaults to None

None
email None | str

Optional auther e-mail address to be added to the gpx file, defaults to None

None

Returns:

Type Description
str

Content of a gpx file

Source code in geo_track_analyzer/track.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def get_xml(self, name: None | str = None, email: None | str = None) -> str:
    """Get track as .gpx file data

    :param name: Optional author name to be added to gpx file, defaults to None
    :param email: Optional auther e-mail address to be added to the gpx file,
        defaults to None

    :return: Content of a gpx file
    """
    gpx = GPX()

    gpx.tracks = [self.track]
    gpx.author_name = name
    gpx.author_email = email

    return gpx.to_xml()

interpolate_points_in_segment(spacing, n_segment=0, copy_extensions='copy-forward')

Add additdion points to a segment by interpolating along the direct line between each point pair according to the passed spacing parameter. If present, elevation and time will be linearly interpolated. Extensions (Heartrate, Cadence, Power) will be interpolated according to value of copy_extensions. Optionas are:

  • copy the value from the start point of the interpolation (copy-forward)
  • Use value of start point for first half and last point for second half (meet-center)
  • Linear interpolation (linear)

Parameters:

Name Type Description Default
spacing float

Minimum distance between points added by the interpolation

required
n_segment int

segment in the track to use, defaults to 0

0
copy_extensions Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points.

'copy-forward'
Source code in geo_track_analyzer/track.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def interpolate_points_in_segment(
    self,
    spacing: float,
    n_segment: int = 0,
    copy_extensions: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> None:
    """
    Add additdion points to a segment by interpolating along the direct line
    between each point pair according to the passed spacing parameter. If present,
    elevation and time will be linearly interpolated. Extensions (Heartrate,
    Cadence, Power) will be interpolated according to value of copy_extensions.
    Optionas are:

    - copy the value from the start point of the interpolation (copy-forward)
    - Use value of start point for first half and last point for second half
      (meet-center)
    - Linear interpolation (linear)


    :param spacing: Minimum distance between points added by the interpolation
    :param n_segment: segment in the track to use, defaults to 0
    :param copy_extensions: How should the extenstion (if present) be defined in the
        interpolated points.
    """
    self.track.segments[n_segment] = interpolate_segment(
        self.track.segments[n_segment], spacing, copy_extensions=copy_extensions
    )

    # Reset saved processed data
    for key in self._processed_track_data:
        self._processed_track_data.pop(key)
    if n_segment in self._processed_segment_data:
        logger.debug(
            "Deleting saved processed segment data for segment %s", n_segment
        )
        self._processed_segment_data.pop(n_segment)

plot(kind, *, segment=None, reduce_pp_intervals=None, use_distance_segments=None, **kwargs)

Visualize the full track or a single segment.

Parameters:

Name Type Description Default
kind Literal['profile', 'profile-slope', 'map-line', 'map-line-enhanced', 'map-segments', 'zone-summary', 'segment-zone-summary', 'segment-box', 'segment-summary', 'metrics']

Kind of plot to be generated - profile: Elevation profile of the track. May be enhanced with additional information like Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_2d - profile-slope: Elevation profile with slopes between points. Use the reduce_pp_intervals argument to reduce the number of slope intervals. Pass keyword args for plot_track_with_slope - map-line: Visualize coordinates on the map. Pass keyword args for plot_track_line_on_map - map-line-enhanced: Visualize coordinates on the map. Enhance with additional information like Elevation, Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_enriched_on_map - map-segments: Visualize coordinates on the map split into segments. Pass keyword args for plot_segments_on_map - zone_summary : Visualize an aggregate (time, distance, speed) value for a metric (heartrate, power, cadence) with defined zones. Pass keyword args for plot_track_zones, aggregate and metric are required. - segment_zone_summary : Same as "zone-summary" but split aggregate per segment plot_segment_zones - segment_box : Box plot of a metric (heartrate, power, cadence, speed, elevation) per segment. Pass keyword args for plot_segments_on_map metric is required. - segment_summary : Visualize a aggregate (total_time, total_distance, avg_speed, max_speed) per segment. Pass keyword args for plot_segment_summary aggregate is required. - metrics: Visualize the progression of of a metric (elevation, heartrate, power, cadence, power) over the course of a track. Can be plotted over distance and duration.

required
segment None | int | list[int]

Select a specific segment, multiple segments or all segmenets, defaults to None

None
reduce_pp_intervals None | int

Optionally pass a distance in m which is used to reduce the points in a track, defaults to None

None
use_distance_segments None | float

Ignore all segments in data and split full track into segments with passed cummulated distance in meters. If passed, segment arg must be None. Defaults to None.

None

Returns:

Type Description
Figure

Figure (plotly)

Raises:

Type Description
VisualizationSetupError

If the plot prequisites are not met

Source code in geo_track_analyzer/track.py
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ],
    *,
    segment: None | int | list[int] = None,
    reduce_pp_intervals: None | int = None,
    use_distance_segments: None | float = None,
    **kwargs,
) -> Figure:
    """
    Visualize the full track or a single segment.

    :param kind: Kind of plot to be generated

        - profile: Elevation profile of the track. May be enhanced with additional
          information like Velocity, Heartrate, Cadence, and Power. Pass keyword
          args for [`plot_track_2d`][geo_track_analyzer.visualize.plot_track_2d]
        - profile-slope: Elevation profile with slopes between points. Use the
          reduce_pp_intervals argument to reduce the number of slope intervals.
          Pass keyword args for
          [`plot_track_with_slope`][geo_track_analyzer.visualize.plot_track_with_slope]
        - map-line: Visualize coordinates on the map. Pass keyword args for
          [`plot_track_line_on_map`][geo_track_analyzer.visualize.plot_track_line_on_map]
        - map-line-enhanced: Visualize coordinates on the map. Enhance with
          additional information like Elevation, Velocity, Heartrate, Cadence, and
          Power. Pass keyword args for [`plot_track_enriched_on_map`][geo_track_analyzer.visualize.plot_track_enriched_on_map]
        - map-segments: Visualize coordinates on the map split into segments.
          Pass keyword args for
          [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
        - zone_summary : Visualize an aggregate (time, distance, speed) value for a
            metric (heartrate, power, cadence) with defined zones. Pass keyword args
            for [`plot_track_zones`][geo_track_analyzer.visualize.plot_track_zones],
            `aggregate` and `metric` are required.
        - segment_zone_summary : Same as "zone-summary" but split aggregate per
            segment [`plot_segment_zones`][geo_track_analyzer.visualize.plot_segment_zones]
        - segment_box : Box plot of a metric (heartrate, power, cadence, speed,
            elevation) per segment. Pass keyword args for [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
            `metric` is required.
        - segment_summary : Visualize a aggregate (total_time, total_distance,
            avg_speed, max_speed) per segment. Pass keyword args for [`plot_segment_summary`][geo_track_analyzer.visualize.plot_segment_summary]
            `aggregate` is required.
        - metrics: Visualize the progression of of a metric (elevation, heartrate,
            power, cadence, power) over the course of a track. Can be plotted over distance
            and duration.
    :param segment: Select a specific segment, multiple segments or all segmenets,
        defaults to None
    :param reduce_pp_intervals: Optionally pass a distance in m which is used to
        reduce the points in a track, defaults to None
    :param use_distance_segments: Ignore all segments in data and split full track
        into segments with passed cummulated distance in meters. If passed, segment
        arg must be None. Defaults to None.
    :raises VisualizationSetupError: If the plot prequisites are not met

    :return: Figure (plotly)
    """
    from geo_track_analyzer.utils.track import generate_distance_segments

    if use_distance_segments is not None and segment is not None:
        raise VisualizationSetupError(
            f"use_distance_segments {use_distance_segments} cannot be passed with "
            f"segment {segment}."
        )

    valid_kinds = [
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ]

    if "_" in kind and kind.replace("_", "-") in valid_kinds:
        warnings.warn(
            "Found %s but in versions >=2 only %s will be supported"
            % (kind, kind.replace("_", "-")),
            DeprecationWarning,
        )
        kind = kind.replace("_", "-")  # type: ignore

    require_elevation = ["profile", "profile-slope"]
    connect_segment_full = ["map-segments"]
    if kind not in valid_kinds:
        raise VisualizationSetupError(
            f"Kind {kind} is not valid. Pass on of {','.join(valid_kinds)}"
        )

    if kind in ["zone-summary", "segment-zone-summary"] and not all(
        key in kwargs for key in ["metric", "aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** and **aggregate** need to be passed"
        )
    if kind in ["segment-box"] and not all(key in kwargs for key in ["metric"]):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )
    if kind in ["segment-summary"] and not all(
        key in kwargs for key in ["aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )

    require_extensions = (
        set()
        if self.require_data_extensions is None
        else copy(self.require_data_extensions)
    )
    require_extensions.update({"heartrate", "power", "cadence"})

    if segment is None:
        from geo_track_analyzer.utils.track import extract_track_data_for_plot

        data = extract_track_data_for_plot(
            track=self,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            connect_segments="full" if kind in connect_segment_full else "forward",
            extensions=self.extensions,
        )
    elif isinstance(segment, int):
        from geo_track_analyzer.utils.track import extract_segment_data_for_plot

        data = extract_segment_data_for_plot(
            track=self,
            segment=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )
    else:
        from geo_track_analyzer.utils.track import (
            extract_multiple_segment_data_for_plot,
        )

        data = extract_multiple_segment_data_for_plot(
            track=self,
            segments=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )

    # Special case if all points are stopped. Since we still want
    # a plot, we interpret it as all moving. This is expected behaviour
    # for tracks that all have the same lat/lon coordinates
    if (data.moving == False).all():  # noqa: E712
        data.moving = True
        data.cum_time_moving = data.cum_time_stopped

    if use_distance_segments is not None:
        data = generate_distance_segments(data, use_distance_segments)

    fig: Figure
    if kind == "profile":
        fig = plot_track_2d(data=data, **kwargs)
    elif kind == "profile-slope":
        fig = plot_track_with_slope(data=data, **kwargs)
    elif kind == "map-line":
        fig = plot_track_line_on_map(data=data, **kwargs)
    elif kind == "map-line-enhanced":
        fig = plot_track_enriched_on_map(data=data, **kwargs)
    elif kind == "map-segments":
        fig = plot_segments_on_map(data=data, **kwargs)
    elif kind == "zone-summary":
        fig = plot_track_zones(data=data, **kwargs)
    elif kind == "segment-zone-summary":
        fig = plot_segment_zones(data=data, **kwargs)
    elif kind == "segment-summary":
        fig = plot_segment_summary(data=data, **kwargs)
    elif kind == "segment-box":
        fig = plot_segment_box_summary(data=data, **kwargs)
    elif kind == "metrics":
        fig = plot_metrics(data=data, **kwargs)

    return fig

remove_segement(n_segment, merge='before')

Remove a given segment from the track and merge it with the previous or next segment. Will return False, of the passed parameters lead to not possible operation.

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for,

required
merge Literal['before', 'after']

Direction of the merge. Possible values of "before" and "after".

'before'

Returns:

Type Description
bool

Boolean value reflecting if a segment was removed

Source code in geo_track_analyzer/track.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def remove_segement(
    self, n_segment: int, merge: Literal["before", "after"] = "before"
) -> bool:
    """
    Remove a given segment from the track and merge it with the previous or next
    segment. Will return False, of the passed parameters lead to not possible
    operation.

    :param n_segment: Index of the segment the overview should be generated for,
    :param merge: Direction of the merge. Possible values of "before" and "after".

    :return: Boolean value reflecting if a segment was removed
    """
    assert merge in ["before", "after"]
    if n_segment == 0 and merge != "after":
        logger.error("First segement can only be merged with the after method")
        return False
    if merge == "after" and n_segment == len(self.track.segments) - 1:
        logger.error("Last segment can only be merged with the before method")
        return False
    try:
        self.track.segments[n_segment]
    except IndexError:
        logger.error(
            "Cannot remove segment %s. No valid key in segments", n_segment
        )
        return False

    if merge == "after":
        idx_end = None
        if _points_eq(
            self.track.segments[n_segment].points[-1],
            self.track.segments[n_segment + 1].points[0],
        ):
            idx_end = -1
        self.track.segments[n_segment + 1].points = (
            self.track.segments[n_segment].points[0:idx_end]
            + self.track.segments[n_segment + 1].points
        )
        self.track.segments.pop(n_segment)
        return True
    else:
        idx_start = 0
        if _points_eq(
            self.track.segments[n_segment].points[0],
            self.track.segments[n_segment - 1].points[-1],
        ):
            idx_start = 1
        self.track.segments[n_segment - 1].points = (
            self.track.segments[n_segment - 1].points
            + self.track.segments[n_segment].points[idx_start:]
        )
        self.track.segments.pop(n_segment)
        return True

split(coords, distance_threshold=20)

Split the track at the passed coordinates. The distance_threshold parameter defines the maximum distance between the passed coordingates and the closest point in the track.

Parameters:

Name Type Description Default
coords tuple[float, float]

Latitude, Longitude point at which the split should be made

required
distance_threshold float

Maximum distance between coords and closest point, defaults to 20

20

Raises:

Type Description
TrackTransformationError

If distance exceeds threshold

Source code in geo_track_analyzer/track.py
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
def split(
    self, coords: tuple[float, float], distance_threshold: float = 20
) -> None:
    """
    Split the track at the passed coordinates. The distance_threshold parameter
    defines the maximum distance between the passed coordingates and the closest
    point in the track.

    :param coords: Latitude, Longitude point at which the split should be made
    :param distance_threshold: Maximum distance between coords and closest point,
        defaults to 20

    :raises TrackTransformationError: If distance exceeds threshold
    """
    lat, long = coords
    point_distance = get_point_distance(
        self.track, None, latitude=lat, longitude=long
    )

    if point_distance.distance > distance_threshold:
        raise TrackTransformationError(
            f"Closes point in track has distance {point_distance.distance:.2f}m "
            "from passed coordingates"
        )
    # Split the segment. The closest point should be the first
    # point of the second segment
    pre_segment, post_segment = self.track.segments[
        point_distance.segment_idx
    ].split(point_distance.segment_point_idx - 1)

    self.track.segments[point_distance.segment_idx] = pre_segment
    self.track.segments.insert(point_distance.segment_idx + 1, post_segment)

    self._processed_segment_data = {}
    self._processed_track_data = {}

strip_segements()

Strip all segments from the track. Duplicate points at the segmentment boardes will be dropped.

Source code in geo_track_analyzer/track.py
122
123
124
125
126
127
128
129
130
131
def strip_segements(self) -> bool:
    """
    Strip all segments from the track. Duplicate points at the segmentment boardes
    will be dropped.
    """
    while len(self.track.segments) != 1:
        if not self.remove_segement(len(self.track.segments) - 1, "before"):
            return False

    return True

GeoJsonTrack(source, stopped_speed_threshold=1, max_speed_percentile=95, allow_empty_spatial=False, fallback_coordinates=(0.0, 0.0), require_data_extensions=None, heartrate_zones=None, power_zones=None, cadence_zones=None)

Bases: Track

Track that should be initialized by loading a .json file

Load a .json file that conforms to a supported geojson format. Currently the GeoJsonTrack supports: 1. LineString + Arrays 2. Collection of Points

Parameters:

Name Type Description Default
source str | bytes | dict

Path or byte representation of the json file

required
stopped_speed_threshold float

Minium speed required for a point to be count as moving, defaults to 1

1
max_speed_percentile int

Points with speed outside of the percentile are not counted when analyzing the track, defaults to 95

95
allow_empty_spatial bool

Allow geojson files without spatial values. If true, will generate the track with a fixed location (see. fallback_coordinates)

False
fallback_coordinates tuple[float, float]

Coordinates to be used if the geojson file does not contain spatial data, defaults to (0.0, 0.0)

(0.0, 0.0)
heartrate_zones None | Zones

Optional heartrate Zones, defaults to None

None
power_zones None | Zones

Optional power Zones, defaults to None

None
cadence_zones None | Zones

Optional cadence Zones, defaults to None

None
Source code in geo_track_analyzer/track.py
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
def __init__(
    self,
    source: str | bytes | dict,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    allow_empty_spatial: bool = False,
    fallback_coordinates: tuple[float, float] = (0.0, 0.0),
    require_data_extensions: set[str] | None = None,
    heartrate_zones: None | Zones = None,
    power_zones: None | Zones = None,
    cadence_zones: None | Zones = None,
) -> None:
    """Load a .json file that conforms to a supported geojson format. Currently
    the GeoJsonTrack supports:
    1. LineString + Arrays
    2. Collection of Points

    :param source: Path or byte representation of the json file
    :param stopped_speed_threshold: Minium speed required for a point to be count
        as moving, defaults to 1
    :param max_speed_percentile: Points with speed outside of the percentile are not
        counted when analyzing the track, defaults to 95
    :param allow_empty_spatial: Allow geojson files without spatial values. If true,
        will generate the track with a fixed location (see. fallback_coordinates)
    :param fallback_coordinates: Coordinates to be used if the geojson file
        does not contain spatial data, defaults to (0.0, 0.0)
    :param heartrate_zones: Optional heartrate Zones, defaults to None
    :param power_zones: Optional power Zones, defaults to None
    :param cadence_zones: Optional cadence Zones, defaults to None
    """
    from geo_track_analyzer.utils.geojson import read_raw_data

    super().__init__(
        stopped_speed_threshold=stopped_speed_threshold,
        max_speed_percentile=max_speed_percentile,
        require_data_extensions=require_data_extensions,
        heartrate_zones=heartrate_zones,
        power_zones=power_zones,
        cadence_zones=cadence_zones,
    )

    if isinstance(source, dict):
        raw_data = source
    elif isinstance(source, str):
        with open(source, "r") as f:
            raw_data = json.load(f)
    else:
        with StringIO(source.decode("utf-8")) as f:
            raw_data = json.load(f)

    _track = read_raw_data(
        data=raw_data,
        allow_empty_spatial=allow_empty_spatial,
        fallback_coordinates=fallback_coordinates,
    )

    self._track = _track.track
    self._update_extensions()

add_segmeent(segment)

Add a new segment ot the track

Parameters:

Name Type Description Default
segment GPXTrackSegment

GPXTracksegment to be added

required
Source code in geo_track_analyzer/track.py
114
115
116
117
118
119
120
def add_segmeent(self, segment: GPXTrackSegment) -> None:
    """Add a new segment ot the track

    :param segment: GPXTracksegment to be added
    """
    self.track.segments.append(segment)
    logger.info("Added segment with postition: %s", len(self.track.segments))

find_overlap_with_segment(n_segment, match_track, match_track_segment=0, width=50, overlap_threshold=0.75, max_queue_normalize=5, merge_subsegments=5, extensions_interpolation='copy-forward')

Find overlap of a segment of the track with a segment in another track.

Parameters:

Name Type Description Default
n_segment int

Segment in the track that sould be used as base for the comparison

required
match_track Track

Track object containing the segment to be matched

required
match_track_segment int

Segment on the passed track that should be matched to the segment in this track, defaults to 0

0
width float

Width (in meters) of the grid that will be filled to estimate the overalp , defaults to 50

50
overlap_threshold float

Minimum overlap (as fracrtion) required to return the overlap data, defaults to 0.75

0.75
max_queue_normalize int

Minimum number of successive points in the segment between two points falling into same plate bin, defaults to 5

5
merge_subsegments int

Number of points between sub segments allowed for merging the segments, defaults to 5

5
extensions_interpolation Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points, defaults to copy-forward

'copy-forward'

Returns:

Type Description
Sequence[tuple[Track, float, bool]]

Tuple containing a Track with the overlapping points, the overlap in percent, and the direction of the overlap

Source code in geo_track_analyzer/track.py
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
def find_overlap_with_segment(
    self,
    n_segment: int,
    match_track: Track,
    match_track_segment: int = 0,
    width: float = 50,
    overlap_threshold: float = 0.75,
    max_queue_normalize: int = 5,
    merge_subsegments: int = 5,
    extensions_interpolation: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> Sequence[tuple[Track, float, bool]]:
    """Find overlap of a segment of the track with a segment in another track.

    :param n_segment: Segment in the track that sould be used as base for the
        comparison
    :param match_track: Track object containing the segment to be matched
    :param match_track_segment: Segment on the passed track that should be matched
        to the segment in this track, defaults to 0
    :param width: Width (in meters) of the grid that will be filled to estimate
        the overalp , defaults to 50
    :param overlap_threshold: Minimum overlap (as fracrtion) required to return the
        overlap data, defaults to 0.75
    :param max_queue_normalize: Minimum number of successive points in the segment
        between two points falling into same plate bin, defaults to 5
    :param merge_subsegments: Number of points between sub segments allowed
        for merging the segments, defaults to 5
    :param extensions_interpolation: How should the extenstion (if present) be
        defined in the interpolated points, defaults to copy-forward

    :return: Tuple containing a Track with the overlapping points, the overlap in
        percent, and the direction of the overlap
    """
    max_distance_self = self.get_max_pp_distance_in_segment(n_segment)

    segment_self = self.track.segments[n_segment]
    if max_distance_self > width:
        segment_self = interpolate_segment(
            segment_self, width / 2, copy_extensions=extensions_interpolation
        )

    max_distance_match = match_track.get_max_pp_distance_in_segment(
        match_track_segment
    )
    segment_match = match_track.track.segments[match_track_segment]
    if max_distance_match > width:
        segment_match = interpolate_segment(
            segment_match, width / 2, copy_extensions=extensions_interpolation
        )

    logger.info("Looking for overlapping segments")
    segment_overlaps = get_segment_overlap(
        segment_self,
        segment_match,
        width,
        max_queue_normalize,
        merge_subsegments,
        overlap_threshold,
    )

    matched_tracks: list[tuple[Track, float, bool]] = []
    for overlap in segment_overlaps:
        logger.info("Found: %s", overlap)
        matched_segment = GPXTrackSegment()
        # TODO: Might need to go up to overlap.end_idx + 1?
        matched_segment.points = self.track.segments[n_segment].points[
            overlap.start_idx : overlap.end_idx
        ]
        matched_tracks.append(
            (
                SegmentTrack(
                    matched_segment,
                    stopped_speed_threshold=self.stopped_speed_threshold,
                    max_speed_percentile=self.max_speed_percentile,
                ),
                overlap.overlap,
                overlap.inverse,
            )
        )
    return matched_tracks

get_avg_pp_distance(threshold=10)

Get average distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
379
380
381
382
383
384
385
386
387
388
def get_avg_pp_distance(self, threshold: float = 10) -> float:
    """
    Get average distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance("average", threshold)

get_avg_pp_distance_in_segment(n_segment=0, threshold=10)

Get average distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the average, defaults to 10

10

Returns:

Type Description
float

Average distance

Source code in geo_track_analyzer/track.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def get_avg_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get average distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        average, defaults to 10

    :return: Average distance
    """
    return self._get_aggregated_pp_distance_in_segment(
        "average", n_segment, threshold
    )

get_closest_point(n_segment, latitude, longitude)

Get closest point in a segment or track to the passed latitude and longitude corrdinate

Parameters:

Name Type Description Default
n_segment None | int

Index of the segment. If None is passed the whole track is considered

required
latitude float

Latitude to check

required
longitude float

Longitude to check

required

Returns:

Type Description
PointDistance

Tuple containg the point as GPXTrackPoint, the distance from the passed coordinates and the index in the segment

Source code in geo_track_analyzer/track.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def get_closest_point(
    self, n_segment: None | int, latitude: float, longitude: float
) -> PointDistance:
    """
    Get closest point in a segment or track to the passed latitude and longitude
    corrdinate

    :param n_segment: Index of the segment. If None is passed the whole track is
        considered
    :param latitude: Latitude to check
    :param longitude: Longitude to check

    :return: Tuple containg the point as GPXTrackPoint, the distance from
        the passed coordinates and the index in the segment
    """
    return get_point_distance(self.track, n_segment, latitude, longitude)

get_max_pp_distance(threshold=10)

Get maximum distance between points in the track.

Parameters:

Name Type Description Default
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
406
407
408
409
410
411
412
413
414
415
def get_max_pp_distance(self, threshold: float = 10) -> float:
    """
    Get maximum distance between points in the track.

    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance("max", threshold)

get_max_pp_distance_in_segment(n_segment=0, threshold=10)

Get maximum distance between points in the segment with index n_segment.

Parameters:

Name Type Description Default
n_segment int

Index of the segement to process, defaults to 0

0
threshold float

Minimum distance between points required to be used for the maximum, defaults to 10

10

Returns:

Type Description
float

Maximum distance

Source code in geo_track_analyzer/track.py
417
418
419
420
421
422
423
424
425
426
427
428
429
def get_max_pp_distance_in_segment(
    self, n_segment: int = 0, threshold: float = 10
) -> float:
    """
    Get maximum distance between points in the segment with index n_segment.

    :param n_segment: Index of the segement to process, defaults to 0
    :param threshold: Minimum distance between points required to  be used for the
        maximum, defaults to 10

    :return: Maximum distance
    """
    return self._get_aggregated_pp_distance_in_segment("max", n_segment, threshold)

get_point_data_in_segmnet(n_segment=0)

Get raw coordinates (latitude, longitude), times and elevations for the segement with the passed index.

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]

tuple with coordinates (latitude, longitude), times and elevations

Source code in geo_track_analyzer/track.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def get_point_data_in_segmnet(
    self, n_segment: int = 0
) -> tuple[list[tuple[float, float]], None | list[float], None | list[datetime]]:
    """Get raw coordinates (latitude, longitude), times and elevations for the
    segement with the passed index.

    :param n_segment: Index of the segement, defaults to 0

    :return: tuple with coordinates (latitude, longitude), times and elevations
    """
    coords = []
    elevations = []
    times = []

    for point in self.track.segments[n_segment].points:
        coords.append((point.latitude, point.longitude))
        if point.elevation is not None:
            elevations.append(point.elevation)
        if point.time is not None:
            times.append(point.time)

    if not elevations:
        elevations = None  # type: ignore
    elif len(coords) != len(elevations):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )
    if not times:
        times = None  # type: ignore
    elif len(coords) != len(times):
        raise TrackTransformationError(
            "Elevation is not set for all points. This is not supported"
        )

    return coords, elevations, times

get_segment_data(n_segment=0)

Get processed data for the segmeent with passed index as DataFrame

Parameters:

Name Type Description Default
n_segment int

Index of the segement, defaults to 0

0

Returns:

Type Description
DataFrame

DataFrame with segmenet data

Source code in geo_track_analyzer/track.py
513
514
515
516
517
518
519
520
521
522
def get_segment_data(self, n_segment: int = 0) -> pd.DataFrame:
    """Get processed data for the segmeent with passed index as DataFrame

    :param n_segment: Index of the segement, defaults to 0

    :return: DataFrame with segmenet data
    """
    _, _, _, _, data = self._get_processed_segment_data(n_segment)

    return data

get_segment_overview(n_segment=0)

Get overall metrics for a segment

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for, default to 0

0

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics moving time and distance, total time and distance, maximum and average speed and elevation and cummulated uphill, downholl elevation

Source code in geo_track_analyzer/track.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def get_segment_overview(self, n_segment: int = 0) -> SegmentOverview:
    """
    Get overall metrics for a segment

    :param n_segment: Index of the segment the overview should be generated for,
        default to 0

    :returns: A SegmentOverview object containing the metrics moving time and
        distance, total time and distance, maximum and average speed and elevation
        and cummulated uphill, downholl elevation
    """
    (
        time,
        distance,
        stopped_time,
        stopped_distance,
        data,
    ) = self._get_processed_segment_data(n_segment)

    max_speed = None
    avg_speed = None

    if self.track.segments[n_segment].has_times():
        max_speed = data.speed[data.in_speed_percentile].max()
        avg_speed = data.speed[data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=time,
        distance=distance,
        stopped_time=stopped_time,
        stopped_distance=stopped_distance,
        max_speed=max_speed,
        avg_speed=avg_speed,
        data=data,
    )

get_track_data(connect_segments='forward')

Get processed data for the track as DataFrame. Segment are indicated via the segment column.

Returns:

Type Description
DataFrame

DataFrame with track data

Source code in geo_track_analyzer/track.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
def get_track_data(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> pd.DataFrame:
    """
    Get processed data for the track as DataFrame. Segment are indicated
    via the segment column.

    :return: DataFrame with track data
    """
    track_data: None | pd.DataFrame = None

    _, _, _, _, track_data = self._get_processed_track_data(
        connect_segments=connect_segments
    )

    return track_data

get_track_overview(connect_segments='forward')

Get overall metrics for the track. Equivalent to the sum of all segments

Returns:

Type Description
SegmentOverview

A SegmentOverview object containing the metrics

Source code in geo_track_analyzer/track.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def get_track_overview(
    self, connect_segments: Literal["full", "forward"] = "forward"
) -> SegmentOverview:
    """
    Get overall metrics for the track. Equivalent to the sum of all segments

    :return: A SegmentOverview object containing the metrics
    """
    (
        track_time,
        track_distance,
        track_stopped_time,
        track_stopped_distance,
        track_data,
    ) = self._get_processed_track_data(connect_segments=connect_segments)

    track_max_speed = None
    track_avg_speed = None

    if (
        all(seg.has_times() for seg in self.track.segments)
        and not track_data.speed.isna().all()
    ):
        track_max_speed = track_data.speed[track_data.in_speed_percentile].max()
        track_avg_speed = track_data.speed[track_data.in_speed_percentile].mean()

    return self._create_segment_overview(
        time=track_time,
        distance=track_distance,
        stopped_time=track_stopped_time,
        stopped_distance=track_stopped_distance,
        max_speed=track_max_speed,
        avg_speed=track_avg_speed,
        data=track_data,  # type: ignore
    )

get_xml(name=None, email=None)

Get track as .gpx file data

Parameters:

Name Type Description Default
name None | str

Optional author name to be added to gpx file, defaults to None

None
email None | str

Optional auther e-mail address to be added to the gpx file, defaults to None

None

Returns:

Type Description
str

Content of a gpx file

Source code in geo_track_analyzer/track.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def get_xml(self, name: None | str = None, email: None | str = None) -> str:
    """Get track as .gpx file data

    :param name: Optional author name to be added to gpx file, defaults to None
    :param email: Optional auther e-mail address to be added to the gpx file,
        defaults to None

    :return: Content of a gpx file
    """
    gpx = GPX()

    gpx.tracks = [self.track]
    gpx.author_name = name
    gpx.author_email = email

    return gpx.to_xml()

interpolate_points_in_segment(spacing, n_segment=0, copy_extensions='copy-forward')

Add additdion points to a segment by interpolating along the direct line between each point pair according to the passed spacing parameter. If present, elevation and time will be linearly interpolated. Extensions (Heartrate, Cadence, Power) will be interpolated according to value of copy_extensions. Optionas are:

  • copy the value from the start point of the interpolation (copy-forward)
  • Use value of start point for first half and last point for second half (meet-center)
  • Linear interpolation (linear)

Parameters:

Name Type Description Default
spacing float

Minimum distance between points added by the interpolation

required
n_segment int

segment in the track to use, defaults to 0

0
copy_extensions Literal['copy-forward', 'meet-center', 'linear']

How should the extenstion (if present) be defined in the interpolated points.

'copy-forward'
Source code in geo_track_analyzer/track.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def interpolate_points_in_segment(
    self,
    spacing: float,
    n_segment: int = 0,
    copy_extensions: Literal[
        "copy-forward", "meet-center", "linear"
    ] = "copy-forward",
) -> None:
    """
    Add additdion points to a segment by interpolating along the direct line
    between each point pair according to the passed spacing parameter. If present,
    elevation and time will be linearly interpolated. Extensions (Heartrate,
    Cadence, Power) will be interpolated according to value of copy_extensions.
    Optionas are:

    - copy the value from the start point of the interpolation (copy-forward)
    - Use value of start point for first half and last point for second half
      (meet-center)
    - Linear interpolation (linear)


    :param spacing: Minimum distance between points added by the interpolation
    :param n_segment: segment in the track to use, defaults to 0
    :param copy_extensions: How should the extenstion (if present) be defined in the
        interpolated points.
    """
    self.track.segments[n_segment] = interpolate_segment(
        self.track.segments[n_segment], spacing, copy_extensions=copy_extensions
    )

    # Reset saved processed data
    for key in self._processed_track_data:
        self._processed_track_data.pop(key)
    if n_segment in self._processed_segment_data:
        logger.debug(
            "Deleting saved processed segment data for segment %s", n_segment
        )
        self._processed_segment_data.pop(n_segment)

plot(kind, *, segment=None, reduce_pp_intervals=None, use_distance_segments=None, **kwargs)

Visualize the full track or a single segment.

Parameters:

Name Type Description Default
kind Literal['profile', 'profile-slope', 'map-line', 'map-line-enhanced', 'map-segments', 'zone-summary', 'segment-zone-summary', 'segment-box', 'segment-summary', 'metrics']

Kind of plot to be generated - profile: Elevation profile of the track. May be enhanced with additional information like Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_2d - profile-slope: Elevation profile with slopes between points. Use the reduce_pp_intervals argument to reduce the number of slope intervals. Pass keyword args for plot_track_with_slope - map-line: Visualize coordinates on the map. Pass keyword args for plot_track_line_on_map - map-line-enhanced: Visualize coordinates on the map. Enhance with additional information like Elevation, Velocity, Heartrate, Cadence, and Power. Pass keyword args for plot_track_enriched_on_map - map-segments: Visualize coordinates on the map split into segments. Pass keyword args for plot_segments_on_map - zone_summary : Visualize an aggregate (time, distance, speed) value for a metric (heartrate, power, cadence) with defined zones. Pass keyword args for plot_track_zones, aggregate and metric are required. - segment_zone_summary : Same as "zone-summary" but split aggregate per segment plot_segment_zones - segment_box : Box plot of a metric (heartrate, power, cadence, speed, elevation) per segment. Pass keyword args for plot_segments_on_map metric is required. - segment_summary : Visualize a aggregate (total_time, total_distance, avg_speed, max_speed) per segment. Pass keyword args for plot_segment_summary aggregate is required. - metrics: Visualize the progression of of a metric (elevation, heartrate, power, cadence, power) over the course of a track. Can be plotted over distance and duration.

required
segment None | int | list[int]

Select a specific segment, multiple segments or all segmenets, defaults to None

None
reduce_pp_intervals None | int

Optionally pass a distance in m which is used to reduce the points in a track, defaults to None

None
use_distance_segments None | float

Ignore all segments in data and split full track into segments with passed cummulated distance in meters. If passed, segment arg must be None. Defaults to None.

None

Returns:

Type Description
Figure

Figure (plotly)

Raises:

Type Description
VisualizationSetupError

If the plot prequisites are not met

Source code in geo_track_analyzer/track.py
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ],
    *,
    segment: None | int | list[int] = None,
    reduce_pp_intervals: None | int = None,
    use_distance_segments: None | float = None,
    **kwargs,
) -> Figure:
    """
    Visualize the full track or a single segment.

    :param kind: Kind of plot to be generated

        - profile: Elevation profile of the track. May be enhanced with additional
          information like Velocity, Heartrate, Cadence, and Power. Pass keyword
          args for [`plot_track_2d`][geo_track_analyzer.visualize.plot_track_2d]
        - profile-slope: Elevation profile with slopes between points. Use the
          reduce_pp_intervals argument to reduce the number of slope intervals.
          Pass keyword args for
          [`plot_track_with_slope`][geo_track_analyzer.visualize.plot_track_with_slope]
        - map-line: Visualize coordinates on the map. Pass keyword args for
          [`plot_track_line_on_map`][geo_track_analyzer.visualize.plot_track_line_on_map]
        - map-line-enhanced: Visualize coordinates on the map. Enhance with
          additional information like Elevation, Velocity, Heartrate, Cadence, and
          Power. Pass keyword args for [`plot_track_enriched_on_map`][geo_track_analyzer.visualize.plot_track_enriched_on_map]
        - map-segments: Visualize coordinates on the map split into segments.
          Pass keyword args for
          [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
        - zone_summary : Visualize an aggregate (time, distance, speed) value for a
            metric (heartrate, power, cadence) with defined zones. Pass keyword args
            for [`plot_track_zones`][geo_track_analyzer.visualize.plot_track_zones],
            `aggregate` and `metric` are required.
        - segment_zone_summary : Same as "zone-summary" but split aggregate per
            segment [`plot_segment_zones`][geo_track_analyzer.visualize.plot_segment_zones]
        - segment_box : Box plot of a metric (heartrate, power, cadence, speed,
            elevation) per segment. Pass keyword args for [`plot_segments_on_map`][geo_track_analyzer.visualize.plot_segments_on_map]
            `metric` is required.
        - segment_summary : Visualize a aggregate (total_time, total_distance,
            avg_speed, max_speed) per segment. Pass keyword args for [`plot_segment_summary`][geo_track_analyzer.visualize.plot_segment_summary]
            `aggregate` is required.
        - metrics: Visualize the progression of of a metric (elevation, heartrate,
            power, cadence, power) over the course of a track. Can be plotted over distance
            and duration.
    :param segment: Select a specific segment, multiple segments or all segmenets,
        defaults to None
    :param reduce_pp_intervals: Optionally pass a distance in m which is used to
        reduce the points in a track, defaults to None
    :param use_distance_segments: Ignore all segments in data and split full track
        into segments with passed cummulated distance in meters. If passed, segment
        arg must be None. Defaults to None.
    :raises VisualizationSetupError: If the plot prequisites are not met

    :return: Figure (plotly)
    """
    from geo_track_analyzer.utils.track import generate_distance_segments

    if use_distance_segments is not None and segment is not None:
        raise VisualizationSetupError(
            f"use_distance_segments {use_distance_segments} cannot be passed with "
            f"segment {segment}."
        )

    valid_kinds = [
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
        "metrics",
    ]

    if "_" in kind and kind.replace("_", "-") in valid_kinds:
        warnings.warn(
            "Found %s but in versions >=2 only %s will be supported"
            % (kind, kind.replace("_", "-")),
            DeprecationWarning,
        )
        kind = kind.replace("_", "-")  # type: ignore

    require_elevation = ["profile", "profile-slope"]
    connect_segment_full = ["map-segments"]
    if kind not in valid_kinds:
        raise VisualizationSetupError(
            f"Kind {kind} is not valid. Pass on of {','.join(valid_kinds)}"
        )

    if kind in ["zone-summary", "segment-zone-summary"] and not all(
        key in kwargs for key in ["metric", "aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** and **aggregate** need to be passed"
        )
    if kind in ["segment-box"] and not all(key in kwargs for key in ["metric"]):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )
    if kind in ["segment-summary"] and not all(
        key in kwargs for key in ["aggregate"]
    ):
        raise VisualizationSetupError(
            f"If {kind} is passed, **metric** needs to be passed"
        )

    require_extensions = (
        set()
        if self.require_data_extensions is None
        else copy(self.require_data_extensions)
    )
    require_extensions.update({"heartrate", "power", "cadence"})

    if segment is None:
        from geo_track_analyzer.utils.track import extract_track_data_for_plot

        data = extract_track_data_for_plot(
            track=self,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            connect_segments="full" if kind in connect_segment_full else "forward",
            extensions=self.extensions,
        )
    elif isinstance(segment, int):
        from geo_track_analyzer.utils.track import extract_segment_data_for_plot

        data = extract_segment_data_for_plot(
            track=self,
            segment=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )
    else:
        from geo_track_analyzer.utils.track import (
            extract_multiple_segment_data_for_plot,
        )

        data = extract_multiple_segment_data_for_plot(
            track=self,
            segments=segment,
            kind=kind,
            require_elevation=require_elevation,
            intervals=reduce_pp_intervals,
            extensions=self.extensions,
        )

    # Special case if all points are stopped. Since we still want
    # a plot, we interpret it as all moving. This is expected behaviour
    # for tracks that all have the same lat/lon coordinates
    if (data.moving == False).all():  # noqa: E712
        data.moving = True
        data.cum_time_moving = data.cum_time_stopped

    if use_distance_segments is not None:
        data = generate_distance_segments(data, use_distance_segments)

    fig: Figure
    if kind == "profile":
        fig = plot_track_2d(data=data, **kwargs)
    elif kind == "profile-slope":
        fig = plot_track_with_slope(data=data, **kwargs)
    elif kind == "map-line":
        fig = plot_track_line_on_map(data=data, **kwargs)
    elif kind == "map-line-enhanced":
        fig = plot_track_enriched_on_map(data=data, **kwargs)
    elif kind == "map-segments":
        fig = plot_segments_on_map(data=data, **kwargs)
    elif kind == "zone-summary":
        fig = plot_track_zones(data=data, **kwargs)
    elif kind == "segment-zone-summary":
        fig = plot_segment_zones(data=data, **kwargs)
    elif kind == "segment-summary":
        fig = plot_segment_summary(data=data, **kwargs)
    elif kind == "segment-box":
        fig = plot_segment_box_summary(data=data, **kwargs)
    elif kind == "metrics":
        fig = plot_metrics(data=data, **kwargs)

    return fig

remove_segement(n_segment, merge='before')

Remove a given segment from the track and merge it with the previous or next segment. Will return False, of the passed parameters lead to not possible operation.

Parameters:

Name Type Description Default
n_segment int

Index of the segment the overview should be generated for,

required
merge Literal['before', 'after']

Direction of the merge. Possible values of "before" and "after".

'before'

Returns:

Type Description
bool

Boolean value reflecting if a segment was removed

Source code in geo_track_analyzer/track.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def remove_segement(
    self, n_segment: int, merge: Literal["before", "after"] = "before"
) -> bool:
    """
    Remove a given segment from the track and merge it with the previous or next
    segment. Will return False, of the passed parameters lead to not possible
    operation.

    :param n_segment: Index of the segment the overview should be generated for,
    :param merge: Direction of the merge. Possible values of "before" and "after".

    :return: Boolean value reflecting if a segment was removed
    """
    assert merge in ["before", "after"]
    if n_segment == 0 and merge != "after":
        logger.error("First segement can only be merged with the after method")
        return False
    if merge == "after" and n_segment == len(self.track.segments) - 1:
        logger.error("Last segment can only be merged with the before method")
        return False
    try:
        self.track.segments[n_segment]
    except IndexError:
        logger.error(
            "Cannot remove segment %s. No valid key in segments", n_segment
        )
        return False

    if merge == "after":
        idx_end = None
        if _points_eq(
            self.track.segments[n_segment].points[-1],
            self.track.segments[n_segment + 1].points[0],
        ):
            idx_end = -1
        self.track.segments[n_segment + 1].points = (
            self.track.segments[n_segment].points[0:idx_end]
            + self.track.segments[n_segment + 1].points
        )
        self.track.segments.pop(n_segment)
        return True
    else:
        idx_start = 0
        if _points_eq(
            self.track.segments[n_segment].points[0],
            self.track.segments[n_segment - 1].points[-1],
        ):
            idx_start = 1
        self.track.segments[n_segment - 1].points = (
            self.track.segments[n_segment - 1].points
            + self.track.segments[n_segment].points[idx_start:]
        )
        self.track.segments.pop(n_segment)
        return True

split(coords, distance_threshold=20)

Split the track at the passed coordinates. The distance_threshold parameter defines the maximum distance between the passed coordingates and the closest point in the track.

Parameters:

Name Type Description Default
coords tuple[float, float]

Latitude, Longitude point at which the split should be made

required
distance_threshold float

Maximum distance between coords and closest point, defaults to 20

20

Raises:

Type Description
TrackTransformationError

If distance exceeds threshold

Source code in geo_track_analyzer/track.py
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
def split(
    self, coords: tuple[float, float], distance_threshold: float = 20
) -> None:
    """
    Split the track at the passed coordinates. The distance_threshold parameter
    defines the maximum distance between the passed coordingates and the closest
    point in the track.

    :param coords: Latitude, Longitude point at which the split should be made
    :param distance_threshold: Maximum distance between coords and closest point,
        defaults to 20

    :raises TrackTransformationError: If distance exceeds threshold
    """
    lat, long = coords
    point_distance = get_point_distance(
        self.track, None, latitude=lat, longitude=long
    )

    if point_distance.distance > distance_threshold:
        raise TrackTransformationError(
            f"Closes point in track has distance {point_distance.distance:.2f}m "
            "from passed coordingates"
        )
    # Split the segment. The closest point should be the first
    # point of the second segment
    pre_segment, post_segment = self.track.segments[
        point_distance.segment_idx
    ].split(point_distance.segment_point_idx - 1)

    self.track.segments[point_distance.segment_idx] = pre_segment
    self.track.segments.insert(point_distance.segment_idx + 1, post_segment)

    self._processed_segment_data = {}
    self._processed_track_data = {}

strip_segements()

Strip all segments from the track. Duplicate points at the segmentment boardes will be dropped.

Source code in geo_track_analyzer/track.py
122
123
124
125
126
127
128
129
130
131
def strip_segements(self) -> bool:
    """
    Strip all segments from the track. Duplicate points at the segmentment boardes
    will be dropped.
    """
    while len(self.track.segments) != 1:
        if not self.remove_segement(len(self.track.segments) - 1, "before"):
            return False

    return True