From edb6e93b6e4891e0a9435791eec3cf6d19052728 Mon Sep 17 00:00:00 2001 From: FABallemand Date: Mon, 24 Jul 2023 15:59:11 +0200 Subject: [PATCH] #5 #6 #3 Work in progress: -Add speed, pace and ascent_rate to WayPoint -Enhance matplotlib_plot method -Update example files -Update doc -Add haversine_distance function test [ci skip] --- doc/tutorials/plotting.rst | 8 +- examples/plot.py | 20 +-- ezgpx/gpx/gpx.py | 219 ++++++++++++++++++++----- ezgpx/gpx_elements/gpx.py | 280 +++++++++++++++++++++++++++++--- ezgpx/gpx_elements/way_point.py | 7 + notes.md | 2 +- tests/test_utils.py | 4 +- 7 files changed, 459 insertions(+), 81 deletions(-) diff --git a/doc/tutorials/plotting.rst b/doc/tutorials/plotting.rst index a2a191f..45194e6 100644 --- a/doc/tutorials/plotting.rst +++ b/doc/tutorials/plotting.rst @@ -14,7 +14,7 @@ Matplotlib gpx = ezgpx.GPX("file.gpx") # Plot with Matplotlib - gpx.matplotlib_plot(elevation_color=True, + gpx.matplotlib_plot(color="elevation", start_stop_colors=("green", "red"), way_points_color="blue", title=gpx.name(), @@ -40,7 +40,7 @@ Matplotlib Basemap Toolkit gpx = ezgpx.GPX("file.gpx") # Plot with Matplotlib Basemap Toolkit - gpx.matplotlib_basemap_plot(base_color="darkorange", + gpx.matplotlib_basemap_plot(color="darkorange", start_stop_colors=("darkgreen", "darkred"), way_points_color="darkblue", title=gpx.name(), @@ -68,7 +68,7 @@ gmplot gpx = ezgpx.GPX("file.gpx") # Plot with gmplot (Google Maps) - gpx.gmplot_plot(base_color="yellow", + gpx.gmplot_plot(color="yellow", start_stop_colors=("green", "red"), way_points_color="blue", zoom=13, @@ -94,7 +94,7 @@ Folium # Plot with Folium gpx.folium_plot(tiles="OpenStreetMap", - base_color="orange", + color="orange", start_stop_colors=("green", "red"), way_points_color="blue", minimap=True, diff --git a/examples/plot.py b/examples/plot.py index 3f9e5bf..7c8870c 100644 --- a/examples/plot.py +++ b/examples/plot.py @@ -4,7 +4,7 @@ gpx = ezgpx.GPX("file.gpx") # Plot with Matplotlib -gpx.matplotlib_plot(elevation_color=True, +gpx.matplotlib_plot(color="elevation", start_stop_colors=("green", "red"), way_points_color="blue", title=gpx.name(), @@ -16,7 +16,7 @@ file_path="img_1") # Plot with Matplotlib Basemap Toolkit -gpx.matplotlib_basemap_plot(base_color="darkorange", +gpx.matplotlib_basemap_plot(color="darkorange", start_stop_colors=("darkgreen", "darkred"), way_points_color="darkblue", title=gpx.name(), @@ -28,17 +28,17 @@ file_path="img_2") # Plot with gmplot (Google Maps) -gpx.gmplot_plot(base_color="yellow", - start_stop_colors=("green", "red"), - way_points_color="blue", - zoom=14, - title=gpx.name(), - file_path="map_1.html", - open=False) +gpx.gmplot_plot(color="yellow", + start_stop_colors=("green", "red"), + way_points_color="blue", + zoom=14, + title=gpx.name(), + file_path="map_1.html", + open=False) # Plot with Folium gpx.folium_plot(tiles="OpenStreetMap", - base_color="orange", + color="orange", start_stop_colors=("green", "red"), way_points_color="blue", minimap=True, diff --git a/ezgpx/gpx/gpx.py b/ezgpx/gpx/gpx.py index 8020b42..537407c 100644 --- a/ezgpx/gpx/gpx.py +++ b/ezgpx/gpx/gpx.py @@ -8,6 +8,7 @@ from matplotlib.axes import Axes from matplotlib.figure import Figure +import matplotlib.colors import matplotlib.pyplot as plt from mpl_toolkits.basemap import Basemap @@ -59,6 +60,15 @@ def name(self) -> str: str: Activity name. """ return self.gpx.name() + + def set_name(self, new_name: str) -> None: + """ + Set name. + + Args: + new_name (str): New name. + """ + self.gpx.set_name(new_name) def nb_points(self) -> int: """ @@ -76,7 +86,7 @@ def first_point(self) -> WayPoint: Returns: WayPoint: First point. """ - return self.gpx.tracks[0].trkseg[0].trkpt[0] + return self.gpx.first_point() def last_point(self) -> WayPoint: """ @@ -85,7 +95,7 @@ def last_point(self) -> WayPoint: Returns: WayPoint: Last point. """ - return self.gpx.tracks[-1].trkseg[-1].trkpt[-1] + return self.gpx.last_point() def bounds(self) -> tuple[float, float, float, float]: """ @@ -131,6 +141,30 @@ def descent(self) -> float: float: Descent (meters). """ return self.gpx.descent() + + def compute_points_ascent_rate(self) -> None: + """ + Compute ascent rate at each point. + """ + self.gpx.compute_points_ascent_rate() + + def min_ascent_rate(self) -> float: + """ + Return activity minimum ascent rate. + + Returns: + float: Minimum ascent rate. + """ + return self.gpx.min_ascent_rate() + + def max_ascent_rate(self) -> float: + """ + Return activity maximum ascent rate. + + Returns: + float: Maximum ascent rate. + """ + return self.gpx.max_ascent_rate() def min_elevation(self) -> float: """ @@ -212,11 +246,31 @@ def avg_moving_speed(self) -> float: float: Average moving speed (kilometers per hour). """ return self.gpx.avg_moving_speed() + + def compute_points_speed(self) -> None: + """ + Compute speed (kilometers per hour) at each track point. + """ + self.gpx.compute_points_speed() + + def min_speed(self) -> float: + """ + Return the minimum speed during the activity. + + Returns: + float: Minimum speed. + """ + return self.gpx.min_speed() def max_speed(self) -> float: - # TODO - pass + """ + Return the maximum speed during the activity. + Returns: + float: Maximum speed. + """ + return self.gpx.max_speed() + def avg_pace(self) -> float: """ Return average pace (minutes per kilometer) during the activity. @@ -234,6 +288,36 @@ def avg_moving_pace(self) -> float: float: Average moving pace (minutes per kilometer). """ return self.gpx.avg_moving_pace() + + def compute_points_pace(self) -> None: + """ + Compute pace at each track point. + """ + self.gpx.compute_points_pace() + + def min_pace(self) -> float: + """ + Return the minimum pace during the activity. + + Returns: + float: Minimum pace. + """ + return self.gpx.min_pace() + + def max_pace(self) -> float: + """ + Return the maximum pace during the activity. + + Returns: + float: Maximum pace. + """ + return self.gpx.max_pace() + + def compute_points_ascent_speed(self) -> None: + """ + Compute ascent speed (kilometers per hour) at each track point. + """ + self.gpx.compute_points_ascent_speed() def remove_metadata(self): """ @@ -319,14 +403,29 @@ def to_string(self) -> str: """ return self.writer.gpx_to_string(self.gpx) - def to_dataframe(self, projection: bool = False) -> pd.DataFrame: + def to_dataframe( + self, + projection: bool = False, + elevation: bool = True, + speed: bool = False, + pace: bool = False, + ascent_rate: bool = False, + ascent_speed: bool = False) -> pd.DataFrame: """ Convert GPX object to Pandas Dataframe. + Args: + projection (bool, optional): Toggle projection. Defaults to False. + elevation (bool, optional): Toggle elevation. Defaults to True. + speed (bool, optional): Toggle speed. Defaults to False. + pace (bool, optional): Toggle pace. Defaults to False. + ascent_rate (bool, optional): Toggle ascent rate. Defaults to False. + ascent_speed (bool, optional): Toggle ascent speed. Defaults to False. + Returns: - pd.DataFrame: Dataframe containing position data from GPX. + pd.DataFrame: Dataframe containing data from GPX. """ - return self.gpx.to_dataframe(projection) + return self.gpx.to_dataframe(projection, elevation, speed, pace, ascent_rate, ascent_speed) def to_gpx(self, path: str): """ @@ -339,7 +438,13 @@ def to_gpx(self, path: str): def to_csv( self, - path: str, + projection: bool = False, + elevation: bool = True, + speed: bool = False, + pace: bool = False, + ascent_rate: bool = False, + ascent_speed: bool = False, + path: str = "unnamed.csv", sep: str = ",", header: bool = True, index: bool = False): @@ -347,9 +452,18 @@ def to_csv( Write the GPX object track coordinates to a .csv file. Args: - path (str): Path to the .csv file. - """ - self.to_dataframe().to_csv(path, sep=sep, header=header, index=index) + projection (bool, optional): Toogle projected coordinates. Defaults to False. + elevation (bool, optional): Toggle elevation. Defaults to True. + speed (bool, optional): Toggle speed. Defaults to False. + pace (bool, optional): Toggle pace. Defaults to False. + ascent_rate (bool, optional): Toggle ascent rate. Defaults to False. + ascent_speed (bool, optional): Toggle ascent speed. Defaults to False. + path (str, optional): Path. Defaults to "unnamed.csv". + sep (str, optional): Separator. Defaults to ",". + header (bool, optional): Toggle header. Defaults to True. + index (bool, optional): Toggle index. Defaults to False. + """ + self.to_dataframe(projection, elevation, speed, pace, ascent_rate, ascent_speed).to_csv(path, sep=sep, header=header, index=index) def _matplotlib_plot_text( self, @@ -403,8 +517,8 @@ def matplotlib_axes_plot( self, axes: Axes, projection: Optional[str] = None, - base_color: str = "#101010", - elevation_color: bool = False, + color: str = "#101010", + colorbar: bool = False, start_stop_colors: Optional[tuple[str, str]] = None, way_points_color: Optional[str] = None, title: Optional[str] = None, @@ -419,7 +533,8 @@ def matplotlib_axes_plot( Args: axes (matplotlib.axes.Axes): Axes to plot on. projection (Optional[str], optional): Projection. Defaults to None. - base_color (str, optional): Track color.. Defaults to "#101010". + color (str, optional): A color string (ie: "#FF0000" or "red") or a track attribute ("elevation", "speed", "pace", "vertical_drop", "ascent_rate", "ascent_speed") Defaults to "#101010". + colorbar (bool, optional): Colorbar toggle. Defaults to False. elevation_color (bool, optional): Color track according to elevation. Defaults to False. start_stop_colors (tuple[str, str], optional): Start and stop points colors. Defaults to False. way_points_color (str, optional): Way point color. Defaults to False. @@ -435,22 +550,48 @@ def matplotlib_axes_plot( # Handle projection (select dataframe columns to use and project if needed) if projection is None: - column_x = "longitude" - column_y = "latitude" + column_x = "lon" + column_y = "lat" else: column_x = "x" column_y = "y" self.gpx.project(projection) # Project all track points # Create dataframe containing data from the GPX file - gpx_df = self.to_dataframe(projection=True) + gpx_df = self.to_dataframe(projection=True, elevation=True, speed=True, pace=True, ascent_rate=True, ascent_speed=True) # Scatter all track points - if elevation_color: - axes.scatter(gpx_df[column_x], gpx_df[column_y], - c=gpx_df["elevation"]) + if color == "elevation": + cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["green","blue"]) + im = axes.scatter(gpx_df[column_x], gpx_df[column_y], + c=gpx_df["ele"], cmap=cmap) + elif color == "speed": + # cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["lightskyblue", "deepskyblue", "blue", "mediumblue", "midnightblue"]) + cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["lightskyblue", "mediumblue", "midnightblue"]) + im = axes.scatter(gpx_df[column_x], gpx_df[column_y], + c=gpx_df["speed"], cmap=cmap) + elif color == "pace": + cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["lightskyblue", "midnightblue"]) + im = axes.scatter(gpx_df[column_x], gpx_df[column_y], + c=gpx_df["pace"], cmap=cmap) + elif color == "vertical_drop": + cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["yellow", "orange", "red", "purple", "black"]) + im = axes.scatter(gpx_df[column_x], gpx_df[column_y], + c=abs(gpx_df["ascent_rate"]), cmap=cmap) + elif color == "ascent_rate": + cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["darkgreen", "green", "yellow", "red", "black"]) + im = axes.scatter(gpx_df[column_x], gpx_df[column_y], + c=gpx_df["ascent_rate"], cmap=cmap) + elif color == "ascent_speed": + cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["deeppink", "lightpink", "lightcoral", "red", "darkred"]) + im = axes.scatter(gpx_df[column_x], gpx_df[column_y], + c=gpx_df["ascent_speed"], cmap=cmap) else: - axes.scatter(gpx_df[column_x], gpx_df[column_y], color=base_color) + im = axes.scatter(gpx_df[column_x], gpx_df[column_y], color=color) + + # Colorbar + if colorbar: + plt.colorbar(im) # Scatter start and stop points with different color if start_stop_colors is not None: @@ -489,8 +630,8 @@ def matplotlib_axes_plot( def matplotlib_plot( self, projection: Optional[str] = None, - base_color: str = "#101010", - elevation_color: bool = False, + color: str = "#101010", + colorbar: bool = False, start_stop_colors: Optional[tuple[str, str]] = None, way_points_color: Optional[str] = None, title: Optional[str] = None, @@ -505,8 +646,8 @@ def matplotlib_plot( Args: projection (Optional[str], optional): Projection. Defaults to None. - base_color (str, optional): Track color.. Defaults to "#101010". - elevation_color (bool, optional): Color track according to elevation. Defaults to False. + color (str, optional): A color string (ie: "#FF0000" or "red") or a track attribute ("elevation", "speed", "pace", "vertical_drop", "ascent_rate", "ascent_speed") Defaults to "#101010". + colorbar (bool, optional): Colorbar toggle. Defaults to False. start_stop_colors (tuple[str, str], optional): Start and stop points colors. Defaults to False. way_points_color (str, optional): Way point color. Defaults to False. title (Optional[str], optional): Title. Defaults to None. @@ -524,8 +665,8 @@ def matplotlib_plot( # Plot on axes self.matplotlib_axes_plot(fig.axes[0], projection, - base_color, - elevation_color, + color, + colorbar, start_stop_colors, way_points_color, title, @@ -549,7 +690,7 @@ def matplotlib_basemap_plot( self, projection: str = "cyl", service: str = "World_Shaded_Relief", - base_color: str = "#101010", + color: str = "#101010", start_stop_colors: Optional[tuple[str, str]] = None, way_points_color: Optional[str] = None, title: Optional[str] = None, @@ -565,7 +706,7 @@ def matplotlib_basemap_plot( Args: projection (str, optional): Projection. Currently supported projections: cyl. Defaults to "cyl". service (str, optional): Service used to fetch map background. Currently supported services: "World_Shaded_Relief". Defaults to "World_Shaded_Relief". - base_color (str, optional): Track color. Defaults to "#101010". + color (str, optional): Track color. Defaults to "#101010". start_stop_colors (tuple[str, str], optional): Start and stop points colors. Defaults to False. way_points_color (str, optional): Way point color. Defaults to False. title (Optional[str], optional): Title. Defaults to None. @@ -616,12 +757,12 @@ def matplotlib_basemap_plot( gpx_df = self.to_dataframe() # Project track points - x, y = map(gpx_df["longitude"], gpx_df["latitude"]) + x, y = map(gpx_df["lon"], gpx_df["lat"]) # Plot track points on the map x = x.tolist() y = y.tolist() - map.plot(x, y, marker=None, color=base_color) + map.plot(x, y, marker=None, color=color) # Scatter start and stop points with different color if start_stop_colors: @@ -654,7 +795,7 @@ def matplotlib_basemap_plot( def gmplot_plot( self, - base_color: str = "#110000", + color: str = "#110000", start_stop_colors: Optional[tuple[str, str]] = None, way_points_color: Optional[str] = None, zoom: float = 10.0, @@ -667,7 +808,7 @@ def gmplot_plot( Plot GPX using gmplot. Args: - base_color (str, optional): Track_color. Defaults to "#110000". + color (str, optional): Track_color. Defaults to "#110000". start_stop_colors (tuple[str, str], optional): Start and stop points colors. Defaults to False. way_points_color (str, optional): Way point color. Defaults to False. zoom (float, optional): Zoom. Defaults to 10.0. @@ -686,11 +827,11 @@ def gmplot_plot( # Scatter track points if scatter: - map.scatter(gpx_df["latitude"], gpx_df["longitude"], - base_color, size=5, marker=False) + map.scatter(gpx_df["lat"], gpx_df["lon"], + color, size=5, marker=False) if plot: - map.plot(gpx_df["latitude"], gpx_df["longitude"], - base_color, edge_width=2.5) + map.plot(gpx_df["lat"], gpx_df["lon"], + color, edge_width=2.5) # Scatter start and stop points with different color if start_stop_colors: @@ -721,7 +862,7 @@ def gmplot_plot( def folium_plot( self, tiles: str = "OpenStreetMap", # "OpenStreetMap", "Stamen Terrain", "Stamen Toner" - base_color: str = "#110000", + color: str = "#110000", start_stop_colors: Optional[tuple[str, str]] = None, way_points_color: Optional[str] = None, minimap: bool = False, @@ -755,7 +896,7 @@ def folium_plot( gpx_df["coordinates"] = list( zip(gpx_df.latitude, gpx_df.longitude)) folium.PolyLine(gpx_df["coordinates"], - tooltip=self.name(), color=base_color).add_to(m) + tooltip=self.name(), color=color).add_to(m) # Scatter start and stop points with different color if start_stop_colors: diff --git a/ezgpx/gpx_elements/gpx.py b/ezgpx/gpx_elements/gpx.py index 85494e1..3591d6a 100644 --- a/ezgpx/gpx_elements/gpx.py +++ b/ezgpx/gpx_elements/gpx.py @@ -89,6 +89,15 @@ def name(self) -> str: str: Activity name. """ return self.tracks[0].name + + def set_name(self, new_name: str) -> None: + """ + Set name. + + Args: + new_name (str): New name. + """ + self.tracks[0].name = new_name def nb_points(self) -> int: """ @@ -103,6 +112,24 @@ def nb_points(self) -> int: nb_pts += len(track_segment.trkpt) return nb_pts + def first_point(self) -> WayPoint: + """ + Return GPX first point. + + Returns: + WayPoint: First point. + """ + return self.tracks[0].trkseg[0].trkpt[0] + + def last_point(self) -> WayPoint: + """ + Return GPX last point. + + Returns: + WayPoint: Last point. + """ + return self.tracks[-1].trkseg[-1].trkpt[-1] + def bounds(self) -> tuple[float, float, float, float]: """ Find minimum and maximum latitude and longitude. @@ -191,6 +218,60 @@ def descent(self) -> float: previous_elevation = track_point.ele return descent + def compute_points_ascent_rate(self) -> None: + """ + Compute ascent rate at each point. + """ + previous_point = self.first_point() + + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + distance = haversine_distance(previous_point, track_point) + ascent = track_point.ele - previous_point.ele + try: + track_point.ascent_rate = (ascent * 100) / distance + logging.info(f"distance={distance} | ascent={ascent} | ascent_rate={track_point.ascent_rate}") + except: + track_point.ascent_rate = 0.0 + previous_point = track_point + + def min_ascent_rate(self) -> float: + """ + Return activity minimum ascent rate. + + Returns: + float: Minimum ascent rate. + """ + min_ascent_rate = 100.0 + self.compute_points_ascent_rate() + + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + if track_point.ascent_rate < min_ascent_rate: + min_ascent_rate = track_point.ascent_rate + + return min_ascent_rate + + def max_ascent_rate(self) -> float: + """ + Return activity maximum ascent rate. + + Returns: + float: Maximum ascent rate. + """ + max_ascent_rate = -1.0 + self.compute_points_ascent_rate() + + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + if track_point.ascent_rate > max_ascent_rate: + max_ascent_rate = track_point.ascent_rate + + return max_ascent_rate + def min_elevation(self) -> float: """ Compute minimum elevation (meters) in the tracks contained in the Gpx element. @@ -345,6 +426,59 @@ def avg_moving_speed(self) -> float: return distance / moving_time + def compute_points_speed(self) -> None: + """ + Compute speed (kilometers per hour) at each track point. + """ + previous_point = self.first_point() + + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + distance = haversine_distance(previous_point, track_point) / 1000 # Convert to kilometers + time = (track_point.time - previous_point.time).total_seconds() / 3600 # Convert to hours + try: + track_point.speed = distance / time + except: + track_point.speed = 0.0 + previous_point = track_point + + def min_speed(self) -> float: + """ + Return the minimum speed during the activity. + + Returns: + float: Minimum speed. + """ + min_speed = 1000.0 + self.compute_points_speed() + + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + if track_point.speed < min_speed: + min_speed = track_point.speed + + return min_speed + + def max_speed(self) -> float: + """ + Return the maximum speed during the activity. + + Returns: + float: Maximum speed. + """ + max_speed = -1.0 + self.compute_points_speed() + + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + if track_point.speed > max_speed: + max_speed = track_point.speed + + return max_speed + def avg_pace(self) -> float: """ Compute the average pace (minute per kilometer) during the activity. @@ -352,7 +486,7 @@ def avg_pace(self) -> float: Returns: float: Average pace (minute per kilometer). """ - return 60 / self.avg_speed() + return 60.0 / self.avg_speed() def avg_moving_pace(self) -> float: """ @@ -361,37 +495,131 @@ def avg_moving_pace(self) -> float: Returns: float: Average moving pace (minute per kilometer). """ - return 60 / self.avg_moving_speed() + return 60.0 / self.avg_moving_speed() + + def compute_points_pace(self) -> None: + """ + Compute pace at each track point. + """ + self.compute_points_speed() - def to_dataframe(self, projection: bool = False) -> pd.DataFrame: + for track in self.tracks: + for segment in track.trkseg: + for point in segment.trkpt: + try: + point.pace = 60.0 / point.speed + except: + point.pace = self.avg_moving_pace() # Fill with average moving space (first point) + + def min_pace(self) -> float: """ - Convert Gpx element to Pandas Dataframe. + Return the minimum pace during the activity. Returns: - Pandas.DataFrame: Dataframe containing position data from GPX. + float: Minimum pace. """ - route_info = [] + min_pace = 1000.0 + self.compute_points_pace() - if projection: - for track in self.tracks: - for segment in track.trkseg: - for point in segment.trkpt: - route_info.append({ - "latitude": point.lat, - "longitude": point.lon, - "elevation": point.ele, - "x": point._x, - "y": point._y - }) - else: - for track in self.tracks: - for segment in track.trkseg: - for point in segment.trkpt: - route_info.append({ - "latitude": point.lat, - "longitude": point.lon, - "elevation": point.ele - }) + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + if track_point.pace < min_pace: + min_pace = track_point.pace + + return min_pace + + def max_pace(self) -> float: + """ + Return the maximum pace during the activity. + + Returns: + float: Maximum pace. + """ + max_pace = -1.0 + self.compute_points_pace() + + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + if track_point.pace > max_pace: + max_pace = track_point.pace + + return max_pace + + def compute_points_ascent_speed(self) -> None: + """ + Compute ascent speed (kilometers per hour) at each track point. + """ + previous_point = self.first_point() + + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + ascent = (track_point.ele - previous_point.ele) / 1000 # Convert to kilometers + time = (track_point.time - previous_point.time).total_seconds() / 3600 # Convert to hours + try: + track_point.ascent_speed = ascent / time + except: + track_point.ascent_speed = 0.0 + previous_point = track_point + + def to_dataframe( + self, + projection: bool = False, + elevation: bool = True, + speed: bool = False, + pace: bool = False, + ascent_rate: bool = False, + ascent_speed: bool = False) -> pd.DataFrame: + """ + Convert GPX object to Pandas Dataframe. + + Args: + projection (bool, optional): Toggle projection. Defaults to False. + elevation (bool, optional): Toggle elevation. Defaults to True. + speed (bool, optional): Toggle speed. Defaults to False. + pace (bool, optional): Toggle pace. Defaults to False. + ascent_rate (bool, optional): Toggle ascent rate. Defaults to False. + ascent_speed (bool, optional): Toggle ascent speed. Defaults to False. + + Returns: + pd.DataFrame: Dataframe containing data from GPX. + """ + test_point = self.first_point() + if projection and test_point._x is None: + logging.warning(f"Converting GPX to dataframe with missing projection data.") + if speed and test_point.speed is None: + self.compute_points_speed() + if pace and test_point.pace is None: + self.compute_points_pace() + if ascent_rate and test_point.ascent_rate is None: + self.compute_points_ascent_rate() + if ascent_speed and test_point.ascent_speed is None: + self.compute_points_ascent_speed() + + route_info = [] + for track in self.tracks: + for track_segment in track.trkseg: + for track_point in track_segment.trkpt: + track_point_dict = { + "lat": track_point.lat, + "lon": track_point.lon + } + if elevation: + track_point_dict["ele"] = track_point.ele + if projection: + track_point_dict["x"] = track_point._x + track_point_dict["y"] = track_point._y + if speed: + track_point_dict["speed"] = track_point.speed + if pace: + track_point_dict["pace"] = track_point.pace + if ascent_rate: + track_point_dict["ascent_rate"] = track_point.ascent_rate + if ascent_speed: + track_point_dict["ascent_speed"] = track_point.ascent_speed + route_info.append(track_point_dict) df = pd.DataFrame(route_info) return df diff --git a/ezgpx/gpx_elements/way_point.py b/ezgpx/gpx_elements/way_point.py index dac891c..fd8206e 100644 --- a/ezgpx/gpx_elements/way_point.py +++ b/ezgpx/gpx_elements/way_point.py @@ -82,6 +82,13 @@ def __init__( self.dgpsid: int = dgpsid self.extensions: Extensions = extensions + # Statistics (for map plotting: https://support.strava.com/hc/en-us/articles/360049869011-Personalized-Stat-Maps) + self.speed: float = None + self.pace: float = None + self.ascent_rate: float = None + self.ascent_speed: float = None + + # Projection self._x: int = None self._y: int = None diff --git a/notes.md b/notes.md index bb8c54c..bc2bb93 100644 --- a/notes.md +++ b/notes.md @@ -36,5 +36,5 @@ ## 📝 TO DO LIST !! - Garmin extensions -- Compute length properly +- Compute length properly (improve haversine distance precision, 2D, 3D) - Add features, deploy and update GPX_Tool \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py index 199af16..3cb7122 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,7 +18,9 @@ class TestUtils(): def test_haversine_distance(self): - pass + point_1 = WayPoint("wpt", 48.0, 2.0) + point_2 = WayPoint("wpt", 43.0, 5.0) + assert math.isclose(utils.haversine_distance(point_1, point_2), 603020.0, abs_tol=1000.0) def _test_perpendicular_distance_horizontal_line(self): start = WayPoint("wpt", 0, 0)