Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GPXTrack.get_nearest_locations method #273

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 99 additions & 13 deletions gpxpy/gpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -1218,13 +1218,13 @@ def get_location_at(self, time: mod_datetime.datetime) -> Optional[GPXTrackPoint
# TODO: If between two points -- approx position!
# return mod_geo.Location(point.latitude, point.longitude)
return point

return None

def get_nearest_location(self, location: mod_geo.Location) -> Optional[NearestLocationData]:
""" Return the (location, track_point_no) on this track segment """
return min((NearestLocationData(pt, -1, -1, pt_no) for (pt, pt_no) in self.walk()) # type: ignore
,key=lambda x: x.location.distance_2d(location) if x is not None else mod_math.inf
,key=lambda x: x.location.distance_2d(location) or mod_math.inf if x is not None else mod_math.inf
,default=None)

def smooth(self, vertical: bool=True, horizontal: bool=False, remove_extremes: bool=False) -> None:
Expand Down Expand Up @@ -1898,12 +1898,95 @@ def has_elevations(self) -> bool:

return result

def get_points_data(self, distance_2d: bool=False) -> List[PointData]:
"""
Returns a list of tuples containing the actual point, its distance from the start,
-1, segment_no, and segment_point_no
"""
distance_from_start: float = 0.0
previous_point: Optional[GPXTrackPoint] = None

# (point, distance_from_start) pairs:
points = []

for segment_no, segment in enumerate(self.segments):
for point_no, point in enumerate(segment.points):
if previous_point and point_no > 0:
if distance_2d:
distance = point.distance_2d(previous_point)
else:
distance = point.distance_3d(previous_point)

distance_from_start += distance or 0.0

points.append(PointData(point, distance_from_start, -1, segment_no, point_no))

previous_point = point

return points

def get_nearest_locations(self, location: mod_geo.Location, threshold_distance: float=0.01) -> List[NearestLocationData]:
"""
Returns a list of locations of elements like
consisting of points where the location may be on the track

threshold_distance is the minimum distance from the track
so that the point *may* be counted as to be "on the track".
For example 0.01 means 1% of the track distance.
"""

assert location
assert threshold_distance

result: List[NearestLocationData] = []

points = self.get_points_data()

if not points:
return result

distance: Optional[float] = points[- 1][1]

threshold = (distance or 0.0) * threshold_distance

min_distance_candidate = None
distance_from_start_candidate = None
track_no_candidate: Optional[int] = None
segment_no_candidate: Optional[int] = None
point_no_candidate: Optional[int] = None
point_candidate: Optional[GPXTrackPoint] = None

for point, distance_from_start, track_no, segment_no, point_no in points:
distance = location.distance_3d(point)
if (distance or 0.0) < threshold:
if min_distance_candidate is None or distance < min_distance_candidate:
min_distance_candidate = distance
distance_from_start_candidate = distance_from_start
track_no_candidate = track_no
segment_no_candidate = segment_no
point_no_candidate = point_no
point_candidate = point
else:
if distance_from_start_candidate is not None and point_candidate and track_no_candidate is not None and segment_no_candidate is not None and point_no_candidate is not None:
result.append(NearestLocationData(point_candidate, track_no_candidate, segment_no_candidate, point_no_candidate))
min_distance_candidate = None
distance_from_start_candidate = None
track_no_candidate = None
segment_no_candidate = None
point_no_candidate = None
point_candidate = None

if distance_from_start_candidate is not None and point_candidate and track_no_candidate is not None and segment_no_candidate is not None and point_no_candidate is not None:
result.append(NearestLocationData(point_candidate, track_no_candidate, segment_no_candidate, point_no_candidate))

return result

def get_nearest_location(self, location: mod_geo.Location) -> Optional[NearestLocationData]:
""" Returns (location, track_segment_no, track_point_no) for nearest location on track """
return min((NearestLocationData(pt, -1, seg, pt_no) for (pt, seg, pt_no) in self.walk()) # type: ignore
,key=lambda x: x.location.distance_2d(location) if x is not None else mod_math.inf
,key=lambda x: x.location.distance_2d(location) or mod_math.inf if x is not None else mod_math.inf
,default=None)

def clone(self) -> "GPXTrack":
return mod_copy.deepcopy(self)

Expand Down Expand Up @@ -2155,7 +2238,7 @@ def get_bounds(self) -> Optional[GPXBounds]:
max_longitude : float
Maximum longitude of track in decimal degrees [-180, 180]
"""
result: Optional[GPXBounds] = None
result: Optional[GPXBounds] = None
for track in self.tracks:
track_bounds = track.get_bounds()
if not result:
Expand Down Expand Up @@ -2436,8 +2519,8 @@ def get_points_data(self, distance_2d: bool=False) -> List[PointData]:
Returns a list of tuples containing the actual point, its distance from the start,
track_no, segment_no, and segment_point_no
"""
distance_from_start = 0
previous_point = None
distance_from_start: float = 0.0
previous_point: Optional[GPXTrackPoint] = None

# (point, distance_from_start) pairs:
points = []
Expand All @@ -2451,7 +2534,7 @@ def get_points_data(self, distance_2d: bool=False) -> List[PointData]:
else:
distance = point.distance_3d(previous_point)

distance_from_start += distance
distance_from_start += distance or 0.0

points.append(PointData(point, distance_from_start, track_no, segment_no, point_no))

Expand Down Expand Up @@ -2488,6 +2571,7 @@ def get_nearest_locations(self, location: mod_geo.Location, threshold_distance:
track_no_candidate: Optional[int] = None
segment_no_candidate: Optional[int] = None
point_no_candidate: Optional[int] = None
point_candidate: Optional[GPXTrackPoint] = None

for point, distance_from_start, track_no, segment_no, point_no in points:
distance = location.distance_3d(point)
Expand All @@ -2498,17 +2582,19 @@ def get_nearest_locations(self, location: mod_geo.Location, threshold_distance:
track_no_candidate = track_no
segment_no_candidate = segment_no
point_no_candidate = point_no
point_candidate = point
else:
if distance_from_start_candidate is not None and point and track_no_candidate is not None and segment_no_candidate is not None and point_no_candidate is not None:
result.append(NearestLocationData(point, track_no_candidate, segment_no_candidate, point_no_candidate))
if distance_from_start_candidate is not None and point_candidate and track_no_candidate is not None and segment_no_candidate is not None and point_no_candidate is not None:
result.append(NearestLocationData(point_candidate, track_no_candidate, segment_no_candidate, point_no_candidate))
min_distance_candidate = None
distance_from_start_candidate = None
track_no_candidate = None
segment_no_candidate = None
point_no_candidate = None
point_candidate = None

if distance_from_start_candidate is not None and point and track_no_candidate is not None and segment_no_candidate is not None and point_no_candidate is not None:
result.append(NearestLocationData(point, track_no_candidate, segment_no_candidate, point_no_candidate))
if distance_from_start_candidate is not None and point_candidate and track_no_candidate is not None and segment_no_candidate is not None and point_no_candidate is not None:
result.append(NearestLocationData(point_candidate, track_no_candidate, segment_no_candidate, point_no_candidate))

return result

Expand All @@ -2517,7 +2603,7 @@ def get_nearest_location(self, location: mod_geo.Location) -> Optional[NearestLo
""" Returns (location, track_no, track_segment_no, track_point_no) for the
nearest location on map """
return min((NearestLocationData(pt, tr, seg, pt_no) for (pt,tr, seg, pt_no) in self.walk()) # type:ignore
,key=lambda x: x.location.distance_2d(location) if x is not None else mod_math.inf
,key=lambda x: x.location.distance_2d(location) or mod_math.inf if x is not None else mod_math.inf
,default=None)

def add_elevation(self, delta: float) -> None:
Expand Down