Skip to content

Track Reference

GPXFileTrack(gpx_file, n_track=0, stopped_speed_threshold=1, max_speed_percentile=95, 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
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
913
def __init__(
    self,
    gpx_file: str,
    n_track: int = 0,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    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,
        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]

_create_segment_overview(time, distance, stopped_time, stopped_distance, max_speed, avg_speed, data)

Derive overview metrics for a segmeent

Source code in geo_track_analyzer/track.py
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def _create_segment_overview(
    self,
    time: float,
    distance: float,
    stopped_time: float,
    stopped_distance: float,
    max_speed: None | float,
    avg_speed: None | float,
    data: pd.DataFrame,
) -> SegmentOverview:
    """Derive overview metrics for a segmeent"""
    total_time = time + stopped_time
    total_distance = distance + stopped_distance

    max_elevation = None
    min_elevation = None

    uphill = None
    downhill = None

    if not data.elevation.isna().all():
        max_elevation = data.elevation.max()
        min_elevation = data.elevation.min()
        position_3d = [
            Position3D(
                latitude=rec["latitude"],
                longitude=rec["longitude"],
                elevation=rec["elevation"],
            )
            for rec in data.to_dict("records")
            if not np.isnan(rec["elevation"])
        ]
        elevation_metrics = calc_elevation_metrics(position_3d)

        uphill = elevation_metrics.uphill
        downhill = elevation_metrics.downhill

    return SegmentOverview(
        moving_time_seconds=time,
        total_time_seconds=total_time,
        moving_distance=distance,
        total_distance=total_distance,
        max_velocity=max_speed,
        avg_velocity=avg_speed,
        max_elevation=max_elevation,
        min_elevation=min_elevation,
        uphill_elevation=uphill,
        downhill_elevation=downhill,
    )

_set_processed_track_data(data, connect_segments)

Save processed data internally to reduce compute. Mainly separated for testing

Source code in geo_track_analyzer/track.py
450
451
452
453
454
455
456
457
458
def _set_processed_track_data(
    self,
    data: process_data_tuple_type,
    connect_segments: Literal["full", "forward"],
) -> process_data_tuple_type:
    """Save processed data internally to reduce compute.
    Mainly separated for testing"""
    self._processed_track_data[connect_segments] = (self.n_segments, data)
    return data

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
91
92
93
94
95
96
97
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
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
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
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
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
330
331
332
333
334
335
336
337
338
339
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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
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
357
358
359
360
361
362
363
364
365
366
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
368
369
370
371
372
373
374
375
376
377
378
379
380
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
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
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
460
461
462
463
464
465
466
467
468
469
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
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
244
245
246
247
248
249
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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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):
        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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
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.keys():
        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']

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.

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
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
717
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
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
    ],
    *,
    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.
    :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",
    ]

    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"
        )

    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",
        )
    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,
        )
    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,
        )

    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)

    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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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
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
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
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
 99
100
101
102
103
104
105
106
107
108
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, 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

Patch 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
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
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
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
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
def __init__(
    self,
    source: str | bytes,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    strict_elevation_loading: bool = False,
    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: Patch 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,
        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 = [], [], []
    heartrates, cadences, powers = [], [], []

    split_at = [0]
    for record in fit_data.get_messages(("record", "lap")):  # type: ignore
        record: DataMessage  # type: ignore
        if record.mesg_type.name == "lap":
            split_at.append(len(points))
        lat = record.get_value("position_lat")
        long = record.get_value("position_long")
        ele = record.get_value("enhanced_altitude")
        ts = record.get_value("timestamp")

        hr = record.get_value("heart_rate")
        cad = record.get_value("cadence")
        pw = record.get_value("power")

        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

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

        heartrates.append(hr)
        cadences.append(cad)
        powers.append(pw)

    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"),
        }

    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()

        for (lat, lng), ele, time, hr, cad, pw in zip(
            points[start_idx:end_idx],
            elevations[start_idx:end_idx],
            times[start_idx:end_idx],
            heartrates[start_idx:end_idx],
            cadences[start_idx:end_idx],
            powers[start_idx:end_idx],
        ):
            this_extensions = {}
            if hr is not None:
                this_extensions["heartrate"] = hr
            if cad is not None:
                this_extensions["cadence"] = cad
            if pw is not None:
                this_extensions["power"] = pw
            this_point = get_extended_track_point(
                lat, lng, ele, time, this_extensions
            )

            gpx_segment.points.append(this_point)

        gpx_track.segments.append(gpx_segment)

    self._track = gpx.tracks[0]

_create_segment_overview(time, distance, stopped_time, stopped_distance, max_speed, avg_speed, data)

Derive overview metrics for a segmeent

Source code in geo_track_analyzer/track.py
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def _create_segment_overview(
    self,
    time: float,
    distance: float,
    stopped_time: float,
    stopped_distance: float,
    max_speed: None | float,
    avg_speed: None | float,
    data: pd.DataFrame,
) -> SegmentOverview:
    """Derive overview metrics for a segmeent"""
    total_time = time + stopped_time
    total_distance = distance + stopped_distance

    max_elevation = None
    min_elevation = None

    uphill = None
    downhill = None

    if not data.elevation.isna().all():
        max_elevation = data.elevation.max()
        min_elevation = data.elevation.min()
        position_3d = [
            Position3D(
                latitude=rec["latitude"],
                longitude=rec["longitude"],
                elevation=rec["elevation"],
            )
            for rec in data.to_dict("records")
            if not np.isnan(rec["elevation"])
        ]
        elevation_metrics = calc_elevation_metrics(position_3d)

        uphill = elevation_metrics.uphill
        downhill = elevation_metrics.downhill

    return SegmentOverview(
        moving_time_seconds=time,
        total_time_seconds=total_time,
        moving_distance=distance,
        total_distance=total_distance,
        max_velocity=max_speed,
        avg_velocity=avg_speed,
        max_elevation=max_elevation,
        min_elevation=min_elevation,
        uphill_elevation=uphill,
        downhill_elevation=downhill,
    )

_set_processed_track_data(data, connect_segments)

Save processed data internally to reduce compute. Mainly separated for testing

Source code in geo_track_analyzer/track.py
450
451
452
453
454
455
456
457
458
def _set_processed_track_data(
    self,
    data: process_data_tuple_type,
    connect_segments: Literal["full", "forward"],
) -> process_data_tuple_type:
    """Save processed data internally to reduce compute.
    Mainly separated for testing"""
    self._processed_track_data[connect_segments] = (self.n_segments, data)
    return data

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
91
92
93
94
95
96
97
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
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
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
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
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
330
331
332
333
334
335
336
337
338
339
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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
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
357
358
359
360
361
362
363
364
365
366
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
368
369
370
371
372
373
374
375
376
377
378
379
380
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
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
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
460
461
462
463
464
465
466
467
468
469
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
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
244
245
246
247
248
249
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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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):
        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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
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.keys():
        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']

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.

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
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
717
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
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
    ],
    *,
    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.
    :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",
    ]

    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"
        )

    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",
        )
    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,
        )
    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,
        )

    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)

    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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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
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
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
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
 99
100
101
102
103
104
105
106
107
108
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, 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
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
def __init__(
    self,
    bytefile: bytes,
    n_track: int = 0,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    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,
        heartrate_zones=heartrate_zones,
        power_zones=power_zones,
        cadence_zones=cadence_zones,
    )

    gpx = gpxpy.parse(bytefile)

    self._track = gpx.tracks[n_track]

_create_segment_overview(time, distance, stopped_time, stopped_distance, max_speed, avg_speed, data)

Derive overview metrics for a segmeent

Source code in geo_track_analyzer/track.py
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def _create_segment_overview(
    self,
    time: float,
    distance: float,
    stopped_time: float,
    stopped_distance: float,
    max_speed: None | float,
    avg_speed: None | float,
    data: pd.DataFrame,
) -> SegmentOverview:
    """Derive overview metrics for a segmeent"""
    total_time = time + stopped_time
    total_distance = distance + stopped_distance

    max_elevation = None
    min_elevation = None

    uphill = None
    downhill = None

    if not data.elevation.isna().all():
        max_elevation = data.elevation.max()
        min_elevation = data.elevation.min()
        position_3d = [
            Position3D(
                latitude=rec["latitude"],
                longitude=rec["longitude"],
                elevation=rec["elevation"],
            )
            for rec in data.to_dict("records")
            if not np.isnan(rec["elevation"])
        ]
        elevation_metrics = calc_elevation_metrics(position_3d)

        uphill = elevation_metrics.uphill
        downhill = elevation_metrics.downhill

    return SegmentOverview(
        moving_time_seconds=time,
        total_time_seconds=total_time,
        moving_distance=distance,
        total_distance=total_distance,
        max_velocity=max_speed,
        avg_velocity=avg_speed,
        max_elevation=max_elevation,
        min_elevation=min_elevation,
        uphill_elevation=uphill,
        downhill_elevation=downhill,
    )

_set_processed_track_data(data, connect_segments)

Save processed data internally to reduce compute. Mainly separated for testing

Source code in geo_track_analyzer/track.py
450
451
452
453
454
455
456
457
458
def _set_processed_track_data(
    self,
    data: process_data_tuple_type,
    connect_segments: Literal["full", "forward"],
) -> process_data_tuple_type:
    """Save processed data internally to reduce compute.
    Mainly separated for testing"""
    self._processed_track_data[connect_segments] = (self.n_segments, data)
    return data

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
91
92
93
94
95
96
97
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
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
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
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
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
330
331
332
333
334
335
336
337
338
339
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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
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
357
358
359
360
361
362
363
364
365
366
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
368
369
370
371
372
373
374
375
376
377
378
379
380
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
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
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
460
461
462
463
464
465
466
467
468
469
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
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
244
245
246
247
248
249
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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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):
        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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
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.keys():
        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']

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.

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
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
717
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
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
    ],
    *,
    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.
    :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",
    ]

    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"
        )

    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",
        )
    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,
        )
    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,
        )

    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)

    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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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
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
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
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
 99
100
101
102
103
104
105
106
107
108
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, heartrate=None, cadence=None, power=None, stopped_speed_threshold=1, max_speed_percentile=95, 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 None | list[int]

Optional list of heartrate values for each point

None
cadence None | list[int]

Optional list of cadence values for each point

None
power None | list[int]

Optional list of power values for each point

None
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
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
def __init__(
    self,
    points: list[tuple[float, float]],
    elevations: None | list[float],
    times: None | list[datetime],
    heartrate: None | list[int] = None,
    cadence: None | list[int] = None,
    power: None | list[int] = None,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    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
    """
    super().__init__(
        stopped_speed_threshold=stopped_speed_threshold,
        max_speed_percentile=max_speed_percentile,
        heartrate_zones=heartrate_zones,
        power_zones=power_zones,
        cadence_zones=cadence_zones,
    )

    gpx = GPX()

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

    gpx_segment = self._create_segmeent(
        points=points,
        elevations=elevations,
        times=times,
        heartrate=heartrate,
        cadence=cadence,
        power=power,
    )

    gpx_track.segments.append(gpx_segment)

    self._track = gpx.tracks[0]

_create_segment_overview(time, distance, stopped_time, stopped_distance, max_speed, avg_speed, data)

Derive overview metrics for a segmeent

Source code in geo_track_analyzer/track.py
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def _create_segment_overview(
    self,
    time: float,
    distance: float,
    stopped_time: float,
    stopped_distance: float,
    max_speed: None | float,
    avg_speed: None | float,
    data: pd.DataFrame,
) -> SegmentOverview:
    """Derive overview metrics for a segmeent"""
    total_time = time + stopped_time
    total_distance = distance + stopped_distance

    max_elevation = None
    min_elevation = None

    uphill = None
    downhill = None

    if not data.elevation.isna().all():
        max_elevation = data.elevation.max()
        min_elevation = data.elevation.min()
        position_3d = [
            Position3D(
                latitude=rec["latitude"],
                longitude=rec["longitude"],
                elevation=rec["elevation"],
            )
            for rec in data.to_dict("records")
            if not np.isnan(rec["elevation"])
        ]
        elevation_metrics = calc_elevation_metrics(position_3d)

        uphill = elevation_metrics.uphill
        downhill = elevation_metrics.downhill

    return SegmentOverview(
        moving_time_seconds=time,
        total_time_seconds=total_time,
        moving_distance=distance,
        total_distance=total_distance,
        max_velocity=max_speed,
        avg_velocity=avg_speed,
        max_elevation=max_elevation,
        min_elevation=min_elevation,
        uphill_elevation=uphill,
        downhill_elevation=downhill,
    )

_set_processed_track_data(data, connect_segments)

Save processed data internally to reduce compute. Mainly separated for testing

Source code in geo_track_analyzer/track.py
450
451
452
453
454
455
456
457
458
def _set_processed_track_data(
    self,
    data: process_data_tuple_type,
    connect_segments: Literal["full", "forward"],
) -> process_data_tuple_type:
    """Save processed data internally to reduce compute.
    Mainly separated for testing"""
    self._processed_track_data[connect_segments] = (self.n_segments, data)
    return data

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
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
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
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
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
330
331
332
333
334
335
336
337
338
339
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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
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
357
358
359
360
361
362
363
364
365
366
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
368
369
370
371
372
373
374
375
376
377
378
379
380
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
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
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
460
461
462
463
464
465
466
467
468
469
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
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
244
245
246
247
248
249
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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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):
        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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
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.keys():
        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']

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.

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
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
717
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
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
    ],
    *,
    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.
    :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",
    ]

    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"
        )

    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",
        )
    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,
        )
    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,
        )

    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)

    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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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
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
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
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
 99
100
101
102
103
104
105
106
107
108
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, 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
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
def __init__(
    self,
    segment: GPXTrackSegment,
    stopped_speed_threshold: float = 1,
    max_speed_percentile: int = 95,
    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
    """
    super().__init__(
        stopped_speed_threshold=stopped_speed_threshold,
        max_speed_percentile=max_speed_percentile,
        heartrate_zones=heartrate_zones,
        power_zones=power_zones,
        cadence_zones=cadence_zones,
    )
    gpx = GPX()

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

    gpx_track.segments.append(segment)

    self._track = gpx.tracks[0]

_create_segment_overview(time, distance, stopped_time, stopped_distance, max_speed, avg_speed, data)

Derive overview metrics for a segmeent

Source code in geo_track_analyzer/track.py
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def _create_segment_overview(
    self,
    time: float,
    distance: float,
    stopped_time: float,
    stopped_distance: float,
    max_speed: None | float,
    avg_speed: None | float,
    data: pd.DataFrame,
) -> SegmentOverview:
    """Derive overview metrics for a segmeent"""
    total_time = time + stopped_time
    total_distance = distance + stopped_distance

    max_elevation = None
    min_elevation = None

    uphill = None
    downhill = None

    if not data.elevation.isna().all():
        max_elevation = data.elevation.max()
        min_elevation = data.elevation.min()
        position_3d = [
            Position3D(
                latitude=rec["latitude"],
                longitude=rec["longitude"],
                elevation=rec["elevation"],
            )
            for rec in data.to_dict("records")
            if not np.isnan(rec["elevation"])
        ]
        elevation_metrics = calc_elevation_metrics(position_3d)

        uphill = elevation_metrics.uphill
        downhill = elevation_metrics.downhill

    return SegmentOverview(
        moving_time_seconds=time,
        total_time_seconds=total_time,
        moving_distance=distance,
        total_distance=total_distance,
        max_velocity=max_speed,
        avg_velocity=avg_speed,
        max_elevation=max_elevation,
        min_elevation=min_elevation,
        uphill_elevation=uphill,
        downhill_elevation=downhill,
    )

_set_processed_track_data(data, connect_segments)

Save processed data internally to reduce compute. Mainly separated for testing

Source code in geo_track_analyzer/track.py
450
451
452
453
454
455
456
457
458
def _set_processed_track_data(
    self,
    data: process_data_tuple_type,
    connect_segments: Literal["full", "forward"],
) -> process_data_tuple_type:
    """Save processed data internally to reduce compute.
    Mainly separated for testing"""
    self._processed_track_data[connect_segments] = (self.n_segments, data)
    return data

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
91
92
93
94
95
96
97
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
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
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
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
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
330
331
332
333
334
335
336
337
338
339
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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
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
357
358
359
360
361
362
363
364
365
366
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
368
369
370
371
372
373
374
375
376
377
378
379
380
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
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
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
460
461
462
463
464
465
466
467
468
469
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
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
244
245
246
247
248
249
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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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):
        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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
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.keys():
        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']

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.

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
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
717
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
def plot(
    self,
    kind: Literal[
        "profile",
        "profile-slope",
        "map-line",
        "map-line-enhanced",
        "map-segments",
        "zone-summary",
        "segment-zone-summary",
        "segment-box",
        "segment-summary",
    ],
    *,
    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.
    :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",
    ]

    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"
        )

    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",
        )
    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,
        )
    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,
        )

    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)

    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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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
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
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
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
 99
100
101
102
103
104
105
106
107
108
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