[trails] Use half way waypoints
All checks were successful
build & deploy / test (push) Successful in 1m51s
build & deploy / build-and-deploy (push) Successful in 29s

This commit is contained in:
2026-05-21 17:13:04 -04:00
parent 4d8e925f8c
commit 9bafe45951
4 changed files with 131 additions and 6 deletions

View File

@ -9,6 +9,7 @@ from django.core.files import File
from locations.models import GeoLocation
from scrobbles.importers.trail_gpx import (
compute_trail_stats,
find_route_waypoint,
import_trail_gpx,
parse_trackpoints,
)
@ -182,6 +183,24 @@ class TestTrailGPXImportModel:
assert imp.processed_finished is not None
class TestFindRouteWaypoint:
def test_returns_halfway_point(self, sample_gpx_path):
result = parse_trackpoints(sample_gpx_path)
pt = find_route_waypoint(result["points"])
assert pt is not None
lat, lon = pt
assert lat == pytest.approx(34.177853, abs=0.001)
assert lon == pytest.approx(-118.829944, abs=0.001)
def test_returns_last_point_for_short_track(self):
points = [(34.0, -118.0, None, None), (34.001, -118.001, None, None)]
pt = find_route_waypoint(points)
assert pt == (34.001, -118.001)
def test_returns_none_for_empty_points(self):
assert find_route_waypoint([]) is None
class TestFindByTrailhead:
def test_exact_match(self, db):
geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
@ -206,6 +225,49 @@ class TestFindByTrailhead:
found = Trail.find_by_trailhead(34.190598, -118.844015)
assert found is None
def test_same_trailhead_same_route_matches(self, db):
geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
trail = Trail.objects.create(
title="Same Route Trail",
trailhead_location=geo,
route_lat=34.192167,
route_lon=-118.843143,
)
found = Trail.find_by_trailhead(
34.190598, -118.844015,
route_lat=34.192167, route_lon=-118.843143,
tolerance_m=100,
)
assert found == trail
def test_same_trailhead_different_route_does_not_match(self, db):
geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
Trail.objects.create(
title="Different Route Trail",
trailhead_location=geo,
route_lat=34.200000,
route_lon=-118.850000,
)
found = Trail.find_by_trailhead(
34.190598, -118.844015,
route_lat=34.192167, route_lon=-118.843143,
tolerance_m=100,
)
assert found is None
def test_legacy_trail_without_route_still_matches(self, db):
geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
trail = Trail.objects.create(
title="Legacy Trail",
trailhead_location=geo,
)
found = Trail.find_by_trailhead(
34.190598, -118.844015,
route_lat=34.192167, route_lon=-118.843143,
tolerance_m=100,
)
assert found == trail
class TestFindOrCreate:
def test_find_existing(self, db):

View File

@ -27,6 +27,27 @@ def haversine(lat1, lon1, lat2, lon2):
return R * 2 * asin(sqrt(a))
def find_route_waypoint(points):
if not points:
return None
total = 0.0
for i in range(1, len(points)):
lat1, lon1 = points[i - 1][0], points[i - 1][1]
lat2, lon2 = points[i][0], points[i][1]
if lat1 is not None and lon1 is not None and lat2 is not None and lon2 is not None:
total += haversine(lat1, lon1, lat2, lon2)
halfway = total / 2.0
cumulative = 0.0
for i in range(1, len(points)):
lat1, lon1 = points[i - 1][0], points[i - 1][1]
lat2, lon2 = points[i][0], points[i][1]
if lat1 is not None and lon1 is not None and lat2 is not None and lon2 is not None:
cumulative += haversine(lat1, lon1, lat2, lon2)
if cumulative >= halfway:
return (lat2, lon2)
return (points[-1][0], points[-1][1])
def compute_trail_stats(points):
distance_m = 0.0
elevation_gain = 0.0
@ -221,15 +242,27 @@ def import_trail_gpx(file_path, user_id, original_filename=None):
logger.warning(f"No timestamps in {file_path}")
return []
route_pt = find_route_waypoint(points)
geo, _ = GeoLocation.objects.get_or_create(
lat=round(first_lat, 6),
lon=round(first_lon, 6),
defaults={"altitude": None},
)
trail = Trail.find_by_trailhead(first_lat, first_lon, tolerance_m=100)
trail = Trail.find_by_trailhead(
first_lat, first_lon,
route_lat=route_pt[0] if route_pt else None,
route_lon=route_pt[1] if route_pt else None,
tolerance_m=100,
)
if not trail:
trail = Trail.objects.create(title=track_name, trailhead_location=geo)
trail = Trail.objects.create(
title=track_name,
trailhead_location=geo,
route_lat=route_pt[0] if route_pt else None,
route_lon=route_pt[1] if route_pt else None,
)
timestamp = first_time
stop_timestamp = last_time

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.29 on 2026-05-21 19:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("trails", "0008_alter_trail_genre"),
]
operations = [
migrations.AddField(
model_name="trail",
name="route_lat",
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name="trail",
name="route_lon",
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -61,6 +61,8 @@ class Trail(ScrobblableMixin):
on_delete=models.DO_NOTHING,
**BNULL,
)
route_lat = models.FloatField(**BNULL)
route_lon = models.FloatField(**BNULL)
strava_id = models.CharField(max_length=255, **BNULL)
trailforks_id = models.CharField(max_length=255, **BNULL)
alltrails_id = models.CharField(max_length=255, **BNULL)
@ -91,7 +93,7 @@ class Trail(ScrobblableMixin):
return trail
@classmethod
def find_by_trailhead(cls, lat, lon, tolerance_m=100):
def find_by_trailhead(cls, lat, lon, tolerance_m=100, route_lat=None, route_lon=None):
candidates = cls.objects.filter(
trailhead_location__isnull=False,
).select_related("trailhead_location")
@ -100,9 +102,14 @@ class Trail(ScrobblableMixin):
for trail in candidates:
loc = trail.trailhead_location
d = haversine(lat, lon, loc.lat, loc.lon)
if d < best_dist and d <= tolerance_m:
best = trail
best_dist = d
if d >= best_dist or d > tolerance_m:
continue
if route_lat is not None and trail.route_lat is not None:
route_dist = haversine(route_lat, route_lon, trail.route_lat, trail.route_lon)
if route_dist > tolerance_m:
continue
best = trail
best_dist = d
return best
def scrobbles(self, user_id):