From bd6e865acce51e0fce91d3e18e7058a2ff7f1ad0 Mon Sep 17 00:00:00 2001 From: FABallemand Date: Wed, 14 Jun 2023 23:17:33 +0200 Subject: [PATCH] #2 Work in progress: -Track extensions class -New GPX methods --- ezgpx/gpx/gpx.py | 116 ++++++++---------- ezgpx/gpx_elements/__init__.py | 1 + ezgpx/gpx_elements/gpx.py | 47 +++++++ ezgpx/gpx_elements/track.py | 4 +- ezgpx/gpx_elements/track_extensions.py | 38 ++++++ test_files/{ => files}/garmin_etrex_1.gpx | 0 .../{ => files}/garmin_etrex_1_test.gpx | 0 test_files/{ => files}/garmin_etrex_2.gpx | 0 test_files/{ => files}/river_run.kmz | Bin test_files/{ => files}/square_1.gpx | 0 test_files/{ => files}/square_2.gpx | 0 test_files/{ => files}/strava_running_1.gpx | 0 12 files changed, 139 insertions(+), 67 deletions(-) create mode 100644 ezgpx/gpx_elements/track_extensions.py rename test_files/{ => files}/garmin_etrex_1.gpx (100%) rename test_files/{ => files}/garmin_etrex_1_test.gpx (100%) rename test_files/{ => files}/garmin_etrex_2.gpx (100%) rename test_files/{ => files}/river_run.kmz (100%) rename test_files/{ => files}/square_1.gpx (100%) rename test_files/{ => files}/square_2.gpx (100%) rename test_files/{ => files}/strava_running_1.gpx (100%) diff --git a/ezgpx/gpx/gpx.py b/ezgpx/gpx/gpx.py index 0950a5b..ba72c58 100644 --- a/ezgpx/gpx/gpx.py +++ b/ezgpx/gpx/gpx.py @@ -1,7 +1,8 @@ -from typing import * import pandas as pd import matplotlib.pyplot as plt +import logging + from ..gpx_elements import Gpx from ..gpx_parser import Parser from ..gpx_writer import Writer @@ -28,6 +29,15 @@ def nb_points(self): for track_segment in track.track_segments: nb_pts += len(track_segment.track_points) return nb_pts + + def distance(self): + """ + Returns the distance (meters) of the tracks contained in the GPX. + + Returns: + float: Distance (meters). + """ + return self.gpx.distance() def to_string(self) -> str: return self.writer.gpx_to_string(self.gpx) @@ -44,71 +54,45 @@ def to_dataframe(self) -> pd.DataFrame: """ return self.gpx.to_dataframe() - # def removeGPSErrors(gpx, error_distance=1000): - # """ - # Remove GPS errors. - - # Args: - # gpx (GPX): GPX object. - # error_distance (int, optional): GPS error threshold distance (in meters) between two points. Defaults to 1000. - - # Returns: - # GPX: GPX object without GPS error. - # list: List of removed points (GPS errors). - # """ - # # Create new "file" - # cleaned_gpx = gpxpy.gpx.GPX() - - # previous_point = None - # GPS_errors = [] - - # for track in gpx.tracks: - # # Create track - # gpx_track = gpxpy.gpx.GPXTrack() - # cleaned_gpx.tracks.append(gpx_track) - # for segment in track.segments: - # # Create segment - # gpx_segment = gpxpy.gpx.GPXTrackSegment() - # gpx_track.segments.append(gpx_segment) - # for point in segment.points: - # # Create points - # if previous_point is None or gpxpy.geo.haversine_distance(previous_point.latitude, - # previous_point.longitude, - # point.latitude, - # point.longitude) < error_distance: - # gpx_segment.points.append(gpxpy.gpx.GPXTrackPoint(point.latitude, point.longitude, elevation=point.elevation)) - # previous_point = point - # else: - # GPS_errors.append(point) - # return cleaned_gpx, GPS_errors - - # def compressFile(gpx, compression_method="Ramer-Douglas-Peucker algorithm", vertical_smooth=True, horizontal_smooth=True): - # """ - # Compress GPX file. - - # Args: - # gpx (GPX): GPX object. - # compression_method (str, optional): Method used to compress GPX. Defaults to "RPD". - # vertical_smooth (bool, optional): Vertical smoothing. Defaults to True. - # horizontal_smooth (bool, optional): Horizontal smoothing. Defaults to True. - - # Returns: - # GPX: Compressed GPX object. - # """ - # # Smoothing - # gpx.smooth(vertical=vertical_smooth, horizontal=horizontal_smooth) - - # # Compression - # if compression_method == "Ramer-Douglas-Peucker algorithm": - # gpx.simplify() - # elif compression_method == "Remove 25% points": - # gpx.reduce_points(int(gpx.get_track_points_no() * 0.75)) - # elif compression_method == "Remove 50% points": - # gpx.reduce_points(int(gpx.get_track_points_no() * 0.5)) - # elif compression_method == "Remove 75% points": - # gpx.reduce_points(int(gpx.get_track_points_no() * 0.25)) - - # return gpx + def remove_metadata(self): + """ + Remove metadata (ie: metadata will not be written when saving the GPX object as a .gpx file). + """ + self.writer.metadata = False + + def remove_elevation(self): + """ + Remove elevation data (ie: elevation data will not be written when saving the GPX object as a .gpx file). + """ + self.writer.ele = False + + def remove_time(self): + """ + Remove time data (ie: time data will not be written when saving the GPX object as a .gpx file). + """ + self.writer.time = False + + def remove_gps_errors(self): + self.gpx.remove_gps_errors() + + def compress(self, compression_method: str = "Ramer-Douglas-Peucker algorithm"): + """ + Compress GPX by removing points. + + Args: + compression_method (str, optional): Method used to compress GPX. Defaults to "Ramer-Douglas-Peucker algorithm". + """ + if compression_method == "Ramer-Douglas-Peucker algorithm": + logging.debug("Ramer-Douglas-Peucker algorithm is not implemented yet") + pass + elif compression_method == "Remove 25% points": + self.gpx.remove_points(4) + elif compression_method == "Remove 50% points": + self.gpx.remove_points(2) + elif compression_method == "Remove 75% points": + pass + elif compression_method == "Remove elevation": + logging.debug("Removing elevation is not implemented yet") def plot(self, title: str = "Track", base_color: str = "#101010", start_stop: bool = False, elevation_color: bool = False, file_path: str = None,): diff --git a/ezgpx/gpx_elements/__init__.py b/ezgpx/gpx_elements/__init__.py index 7610d8c..b5e6d52 100644 --- a/ezgpx/gpx_elements/__init__.py +++ b/ezgpx/gpx_elements/__init__.py @@ -1,5 +1,6 @@ from .gpx import * from .metadata import * from .track import * +from .track_extensions import * from .track_segment import * from .track_point import * \ No newline at end of file diff --git a/ezgpx/gpx_elements/gpx.py b/ezgpx/gpx_elements/gpx.py index a5717f3..d3c3e7a 100644 --- a/ezgpx/gpx_elements/gpx.py +++ b/ezgpx/gpx_elements/gpx.py @@ -3,6 +3,8 @@ from .metadata import * from .track import * +from ..utils import haversine_distance + class Gpx(): """ Gpx (gpx) element in GPX file. @@ -12,6 +14,24 @@ def __init__(self, metadata: Metadata = None, tracks: list[Track] = []): self.metadata: Metadata = metadata self.tracks: list[Track] = tracks + def distance(self): + """ + Compute the total distance (in meters) of the tracks contained in the Gpx element. + + Returns: + float: Distance (meters) + """ + dst = 0 + previous_latitude = self.tracks[0].track_segments[0].track_points[0].latitude + previous_longitude = self.tracks[0].track_segments[0].track_points[0].longitude + for track in self.tracks: + for track_segment in track.track_segments: + for track_point in track_segment.track_points: + dst += haversine_distance(previous_latitude, previous_longitude, track_point.latitude, track_point.longitude) + previous_latitude = track_point.latitude + previous_longitude = track_point.longitude + return dst + def to_dataframe(self) -> pd.DataFrame: """ Convert Gpx element to Pandas Dataframe. @@ -30,6 +50,33 @@ def to_dataframe(self) -> pd.DataFrame: }) df = pd.DataFrame(route_info) return df + + def remove_gps_errors(self, error_distance=1000): + """ + Remove GPS errors. + + Args: + error_distance (int, optional): GPS error threshold distance (meters) between two points. Defaults to 1000. + + Returns: + list: List of removed points (GPS errors). + """ + previous_point = None + gps_errors = [] + + for track in self.tracks: + for track_segment in track.track_segments: + for track_point in track_segment.track_points: + # Create points + if previous_point is not None and haversine_distance(previous_point.latitude, + previous_point.longitude, + track_point.latitude, + track_point.longitude) < error_distance: + gps_errors.append(track_point) + track_segment.track_points.remove(track_point) + else: + previous_point = track_point + return gps_errors def remove_points(self, remove_factor: int = 2): count = 0 diff --git a/ezgpx/gpx_elements/track.py b/ezgpx/gpx_elements/track.py index ad34da4..4ea26a1 100644 --- a/ezgpx/gpx_elements/track.py +++ b/ezgpx/gpx_elements/track.py @@ -1,3 +1,4 @@ +from track_extensions import * from .track_segment import * class Track(): @@ -5,6 +6,7 @@ class Track(): Track (trk) element in GPX file. """ - def __init__(self, name: str = "", track_segments: list[TrackSegment] = []): + def __init__(self, name: str = "", track_extensions: TrackExtensions = None, track_segments: list[TrackSegment] = []): self.name: str = name + self.track_extensions: TrackExtensions = track_extensions self.track_segments: list[TrackSegment] = track_segments \ No newline at end of file diff --git a/ezgpx/gpx_elements/track_extensions.py b/ezgpx/gpx_elements/track_extensions.py new file mode 100644 index 0000000..09e57a8 --- /dev/null +++ b/ezgpx/gpx_elements/track_extensions.py @@ -0,0 +1,38 @@ + +class TrackExtensions(): + """ + Track extensions. + """ + + def __init__( + self, + display_color: str = "Cyan", + distance: float = None, + total_elapsed_time: float = None, + moving_time: float = None, + stopped_time: float = None, + moving_speed: float = None, + max_speed: float = None, + max_elevation: float = None, + min_elevation: float = None, + ascent: float = None, + descent: float = None, + avg_ascent_rate: float = None, + max_ascent_rate: float = None, + avg_descent_rate: float = None, + max_descent_rate: float = None) -> None: + self.display_color: str = display_color + self.distance: float = distance + self.total_elapsed_time: float = total_elapsed_time # int? + self.moving_time: float = moving_time # int? + self.stopped_time: float = stopped_time + self.moving_speed: float = moving_speed + self.max_speed: float = max_speed + self.max_elevation: float = max_elevation + self.min_elevation: float = min_elevation + self.ascent: float = ascent + self.descent: float = descent + self.avg_ascent_rate: float = avg_ascent_rate + self.max_ascent_rate: float = max_ascent_rate + self.avg_descent_rate: float = avg_descent_rate + self.max_descent_rate: float = max_descent_rate diff --git a/test_files/garmin_etrex_1.gpx b/test_files/files/garmin_etrex_1.gpx similarity index 100% rename from test_files/garmin_etrex_1.gpx rename to test_files/files/garmin_etrex_1.gpx diff --git a/test_files/garmin_etrex_1_test.gpx b/test_files/files/garmin_etrex_1_test.gpx similarity index 100% rename from test_files/garmin_etrex_1_test.gpx rename to test_files/files/garmin_etrex_1_test.gpx diff --git a/test_files/garmin_etrex_2.gpx b/test_files/files/garmin_etrex_2.gpx similarity index 100% rename from test_files/garmin_etrex_2.gpx rename to test_files/files/garmin_etrex_2.gpx diff --git a/test_files/river_run.kmz b/test_files/files/river_run.kmz similarity index 100% rename from test_files/river_run.kmz rename to test_files/files/river_run.kmz diff --git a/test_files/square_1.gpx b/test_files/files/square_1.gpx similarity index 100% rename from test_files/square_1.gpx rename to test_files/files/square_1.gpx diff --git a/test_files/square_2.gpx b/test_files/files/square_2.gpx similarity index 100% rename from test_files/square_2.gpx rename to test_files/files/square_2.gpx diff --git a/test_files/strava_running_1.gpx b/test_files/files/strava_running_1.gpx similarity index 100% rename from test_files/strava_running_1.gpx rename to test_files/files/strava_running_1.gpx