Skip to content

Commit

Permalink
#4 work in progress:
Browse files Browse the repository at this point in the history
-Perpendicular distance
-First Rameur-Douglas-Peucker algorithm
  • Loading branch information
FABallemand committed Jun 27, 2023
1 parent 49b46d5 commit d2b6668
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 6,251 deletions.
17 changes: 15 additions & 2 deletions ezgpx/gpx/gpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ def nb_points(self) -> int:
"""
nb_pts = 0
for track in self.gpx.tracks:
for track_segment in track.track_segments:
nb_pts += len(track_segment.track_points)
for track_segment in track.trkseg:
nb_pts += len(track_segment.trkpt)
return nb_pts

def distance(self) -> float:
Expand Down Expand Up @@ -159,8 +159,21 @@ def remove_time(self):
self.writer.time = False

def remove_gps_errors(self):
"""
Remove GPS errors.
"""
self.gpx.remove_gps_errors()

def simplify(self, epsilon: float = 1):
"""
Simplify the tracks using Rameur-Douglas-Peucker algorithm.
Args:
epsilon (float, optional): Tolerance. Defaults to 1.
"""
logging.info("Simplify 1")
self.gpx.simplify(epsilon)

def compress(self, compression_method: str = "Ramer-Douglas-Peucker algorithm"):
"""
Compress GPX by removing points.
Expand Down
76 changes: 44 additions & 32 deletions ezgpx/gpx_elements/gpx.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from datetime import datetime, timezone

import logging
import pandas as pd
from datetime import datetime, timezone

from .metadata import *
from .track import *

from ..utils import haversine_distance
from ..utils import haversine_distance, ramer_douglas_peucker

class Gpx():
"""
Expand Down Expand Up @@ -47,11 +47,11 @@ def distance(self) -> float:
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
previous_latitude = self.tracks[0].trkseg[0].trkpt[0].latitude
previous_longitude = self.tracks[0].trkseg[0].trkpt[0].longitude
for track in self.tracks:
for track_segment in track.track_segments:
for track_point in track_segment.track_points:
for track_segment in track.trkseg:
for track_point in track_segment.trkpt:
dst += haversine_distance(previous_latitude, previous_longitude, track_point.latitude, track_point.longitude)
previous_latitude = track_point.latitude
previous_longitude = track_point.longitude
Expand All @@ -65,10 +65,10 @@ def ascent(self) -> float:
float: Ascent (meters)
"""
ascent = 0
previous_elevation = self.tracks[0].track_segments[0].track_points[0].elevation
previous_elevation = self.tracks[0].trkseg[0].trkpt[0].elevation
for track in self.tracks:
for track_segment in track.track_segments:
for track_point in track_segment.track_points:
for track_segment in track.trkseg:
for track_point in track_segment.trkpt:
if track_point.elevation > previous_elevation:
ascent += track_point.elevation - previous_elevation
previous_elevation = track_point.elevation
Expand All @@ -82,10 +82,10 @@ def descent(self) -> float:
float: Descent (meters)
"""
descent = 0
previous_elevation = self.tracks[0].track_segments[0].track_points[0].elevation
previous_elevation = self.tracks[0].trkseg[0].trkpt[0].elevation
for track in self.tracks:
for track_segment in track.track_segments:
for track_point in track_segment.track_points:
for track_segment in track.trkseg:
for track_point in track_segment.trkpt:
if track_point.elevation < previous_elevation:
descent += previous_elevation - track_point.elevation
previous_elevation = track_point.elevation
Expand All @@ -98,10 +98,10 @@ def min_elevation(self) -> float:
Returns:
float: Minimum elevation (meters).
"""
min_elevation = self.tracks[0].track_segments[0].track_points[0].elevation
min_elevation = self.tracks[0].trkseg[0].trkpt[0].elevation
for track in self.tracks:
for track_segment in track.track_segments:
for track_point in track_segment.track_points:
for track_segment in track.trkseg:
for track_point in track_segment.trkpt:
if track_point.elevation < min_elevation:
min_elevation = track_point.elevation
return min_elevation
Expand All @@ -113,10 +113,10 @@ def max_elevation(self) -> float:
Returns:
float: Maximum elevation (meters).
"""
max_elevation = self.tracks[0].track_segments[0].track_points[0].elevation
max_elevation = self.tracks[0].trkseg[0].trkpt[0].elevation
for track in self.tracks:
for track_segment in track.track_segments:
for track_point in track_segment.track_points:
for track_segment in track.trkseg:
for track_point in track_segment.trkpt:
if track_point.elevation > max_elevation:
max_elevation = track_point.elevation
return max_elevation
Expand All @@ -128,7 +128,7 @@ def utc_start_time(self) -> datetime:
Returns:
datetime: UTC start time.
"""
return self.tracks[0].track_segments[0].track_points[0].time
return self.tracks[0].trkseg[0].trkpt[0].time

def utc_stop_time(self):
"""
Expand All @@ -137,7 +137,7 @@ def utc_stop_time(self):
Returns:
datetime: UTC stop time.
"""
return self.tracks[-1].track_segments[-1].track_points[-1].time
return self.tracks[-1].trkseg[-1].trkpt[-1].time

def start_time(self) -> datetime:
"""
Expand All @@ -146,7 +146,7 @@ def start_time(self) -> datetime:
Returns:
datetime: Start time.
"""
return self.tracks[0].track_segments[0].track_points[0].time.replace(tzinfo=timezone.utc).astimezone(tz=None)
return self.tracks[0].trkseg[0].trkpt[0].time.replace(tzinfo=timezone.utc).astimezone(tz=None)

def stop_time(self):
"""
Expand All @@ -155,7 +155,7 @@ def stop_time(self):
Returns:
datetime: Stop time.
"""
return self.tracks[-1].track_segments[-1].track_points[-1].time.replace(tzinfo=timezone.utc).astimezone(tz=None)
return self.tracks[-1].trkseg[-1].trkpt[-1].time.replace(tzinfo=timezone.utc).astimezone(tz=None)

def total_elapsed_time(self) -> datetime:
"""
Expand Down Expand Up @@ -191,8 +191,8 @@ def to_dataframe(self) -> pd.DataFrame:
"""
route_info = []
for track in self.tracks:
for segment in track.track_segments:
for point in segment.track_points:
for segment in track.trkseg:
for point in segment.trkpt:
route_info.append({
"latitude": point.latitude,
"longitude": point.longitude,
Expand All @@ -215,24 +215,36 @@ def remove_gps_errors(self, error_distance=1000):
gps_errors = []

for track in self.tracks:
for track_segment in track.track_segments:
for track_point in track_segment.track_points:
for track_segment in track.trkseg:
for track_point in track_segment.trkpt:
# 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)
track_segment.trkpt.remove(track_point)
else:
previous_point = track_point
return gps_errors

def remove_points(self, remove_factor: int = 2):
count = 0
for track in self.tracks:
for track_segment in track.track_segments:
for track_point in track_segment.track_points:
for track_segment in track.trkseg:
for track_point in track_segment.trkpt:
if count % remove_factor == 0:
track_segment.track_points.remove(track_point)
count += 1
track_segment.trkpt.remove(track_point)
count += 1

def simplify(self, epsilon):
"""
Simplify GPX tracks using Rameur-Douglas-Peucker algorithm.
Args:
epsilon (float): Tolerance.
"""
logging.info("Simplify 2")
for track in self.tracks:
for segment in track.trkseg:
segment.trkpt = ramer_douglas_peucker(segment.trkpt, epsilon)
7 changes: 5 additions & 2 deletions ezgpx/gpx_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,12 @@ def parse_point(self, point) -> TrackPoint:
except:
logging.error(f"{point} contains invalid elevation: {self.find_text(point, 'topo:ele')}")
elevation = None
try:
time = datetime.strptime(self.find_text(point, "topo:time"), "%Y-%m-%dT%H:%M:%SZ")
except:
logging.error(f"{point} contains invalid time: {self.find_text(point, 'topo:time')}")
time = None

time = datetime.strptime(self.find_text(point, "topo:time"), "%Y-%m-%dT%H:%M:%SZ")

return TrackPoint(lat, lon, elevation, time)

def parse_segment(self, segment) -> TrackSegment:
Expand Down
1 change: 1 addition & 0 deletions ezgpx/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .algorithms import *
from .distance import *
from .projections import *
46 changes: 46 additions & 0 deletions ezgpx/utils/algorithms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import logging

from .distance import perpendicular_distance

def ramer_douglas_peucker(points: list, epsilon: float = 1):
"""
Simplify a curve using the Ramer-Douglas-Peucker algorithm.
Source: https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
Args:
points (list[TrackPoint]): List of points defining the curve.
epsilon (float, optional): _description_. Defaults to 1.
Returns:
list[TrackPoint]: List of points defining the simplified curve.
"""
# Find the point with the maximum distance
d_max = 0
i_max = 0

start_point = points[0]
end_point = points[len(points)-1]

for i in range(1, len(points)-1):
d = perpendicular_distance(start_point, end_point, points[i])
if d > d_max:
d_max = d
i_max = i

logging.info(f"d_max = {d_max}")

result = []

# If max distance is greater than epsilon, recursively simplify
if d_max > epsilon:
# Recursive call
logging.info("rec")
result_1 = ramer_douglas_peucker(points[0:i_max+1], epsilon)
result_2 = ramer_douglas_peucker(points[i_max: len(points)], epsilon)

# Build result list
result = result_1 + result_2[1:]
else:
result = [start_point, end_point]

return result
44 changes: 44 additions & 0 deletions ezgpx/utils/distance.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import math as m
import logging

# latitude/longitude in GPX files is always in WGS84 datum
# WGS84 defined the Earth semi-major axis with 6378.137 km
Expand Down Expand Up @@ -30,3 +31,46 @@ def haversine_distance(latitude_1: float, longitude_1: float, latitude_2: float,

return d

def perpendicular_distance(start_point, end_point, point):
"""
Distance between a point and a line.
Args:
start_point (TrackPoint): A point on the line.
end_point (TrackPoint): A point on the line.
point (TrackPoint): A point to measure the distance from.
Returns:
float: Perpendicular distance between the point *point* and the line defined by *start_point* and *end_point*.
"""

def line_coefficients(point_1, point_2):
"""
Compute the coefficients of a line equation of the form: ax+by+c=0.
Args:
point_1 (TrackPoint): A point on the line.
point_2 (TrackPoint): A point on the line.
Returns:
tuple: Coefficients of the line equation.
"""
delta_x = point_1.longitude - point_2.longitude
delta_y = point_1.latitude - point_2.latitude
try:
a = delta_y / delta_x
b = -1
c = point_1.latitude - a * point_1.longitude
except:
a = 1
b = 0
c = point_1.longitude
logging.warning("Vertical line")

return a, b, c

a, b, c = line_coefficients(start_point, end_point)

d = abs(a*point.longitude + b*point.latitude + c) / m.sqrt(a*a + b*b)
logging.info(f"perpendicular_distance = {d}")
return d
Loading

0 comments on commit d2b6668

Please sign in to comment.