[trails] Add geo-derived names, terminus and route tags to trail imports

This commit is contained in:
2026-08-13 15:06:58 -04:00
parent f41a98cbdd
commit 8a392f4fa2
7 changed files with 258 additions and 3 deletions

View File

@ -22,6 +22,18 @@ SAMPLE_GPX = os.path.join(
os.path.dirname(__file__), "..", "..", "data", "sample_trail.gpx"
)
LOOP_GPX = """<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="test" xmlns="http://www.topografix.com/GPX/1/1">
<trk>
<name>Loop Walk</name>
<trkseg>
<trkpt lat="40.000000" lon="-75.000000"><ele>100</ele><time>2022-01-01T10:00:00Z</time></trkpt>
<trkpt lat="40.000100" lon="-75.000100"><ele>105</ele><time>2022-01-01T10:10:00Z</time></trkpt>
</trkseg>
</trk>
</gpx>
"""
@pytest.fixture
def user(db):
@ -80,6 +92,13 @@ class TestImportTrailGPX:
assert trail.trailhead_location is not None
assert round(trail.trailhead_location.lat, 6) == 34.190598
def test_sets_terminus(self, user, sample_gpx_path):
import_trail_gpx(sample_gpx_path, user.id)
trail = Trail.objects.filter(title="Morning Run ⛅").first()
assert trail.trail_terminus_location is not None
assert round(trail.trail_terminus_location.lat, 6) == 34.187565
assert round(trail.trail_terminus_location.lon, 6) == -118.847091
def test_creates_scrobble(self, user, sample_gpx_path):
import_trail_gpx(sample_gpx_path, user.id)
assert Scrobble.objects.filter(source="GPX Import").count() == 1
@ -110,6 +129,59 @@ class TestImportTrailGPX:
scrobble = Scrobble.objects.filter(source="GPX Import").first()
assert scrobble.trail.id == trail.id
def test_lookup_existing_trail_sets_terminus(self, user, sample_gpx_path):
geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
trail = Trail.objects.create(title="Existing Trail", trailhead_location=geo)
import_trail_gpx(sample_gpx_path, user.id)
trail.refresh_from_db()
assert trail.trail_terminus_location is not None
assert round(trail.trail_terminus_location.lat, 6) == 34.187565
assert round(trail.trail_terminus_location.lon, 6) == -118.847091
def test_new_trail_uses_geo_titles(self, user, sample_gpx_path):
GeoLocation.objects.create(
lat=34.190598, lon=-118.844015, title="Start Trailhead"
)
GeoLocation.objects.create(
lat=34.187565, lon=-118.847091, title="End Trailhead"
)
import_trail_gpx(sample_gpx_path, user.id)
trail = Trail.objects.get(title="Start Trailhead to End Trailhead")
assert trail.trailhead_location.title == "Start Trailhead"
assert trail.trail_terminus_location.title == "End Trailhead"
def test_new_trail_loop_uses_single_geo_title(self, user, sample_gpx_path):
GeoLocation.objects.create(
lat=34.190598, lon=-118.844015, title="Coral Canyon Loop"
)
GeoLocation.objects.create(
lat=34.187565, lon=-118.847091, title="Coral Canyon Loop"
)
import_trail_gpx(sample_gpx_path, user.id)
assert Trail.objects.filter(title="Coral Canyon Loop").exists()
def test_new_trail_falls_back_to_track_name_without_geo_titles(
self, user, sample_gpx_path
):
import_trail_gpx(sample_gpx_path, user.id)
assert Trail.objects.filter(title="Morning Run ⛅").exists()
def test_tags_point_to_point_trail_and_scrobble(self, user, sample_gpx_path):
import_trail_gpx(sample_gpx_path, user.id)
trail = Trail.objects.get(title="Morning Run ⛅")
assert set(trail.tags.names()) == {"point-to-point"}
scrobble = Scrobble.objects.get(trail=trail)
assert set(scrobble.tags.names()) == {"point-to-point"}
def test_tags_out_and_back_trail_and_scrobble(self, user, tmp_path):
gpx_path = tmp_path / "loop.gpx"
gpx_path.write_text(LOOP_GPX)
import_trail_gpx(str(gpx_path), user.id)
trail = Trail.objects.get(title="Loop Walk")
assert set(trail.tags.names()) == {"out-and-back"}
scrobble = Scrobble.objects.get(trail=trail)
assert set(scrobble.tags.names()) == {"out-and-back"}
def test_dedup(self, user, sample_gpx_path):
import_trail_gpx(sample_gpx_path, user.id)
import_trail_gpx(sample_gpx_path, user.id)
@ -201,6 +273,85 @@ class TestFindRouteWaypoint:
assert find_route_waypoint([]) is None
class TestDeriveDefaultTitle:
def test_returns_none_without_geo(self, db):
assert Trail.derive_default_title(None, None) is None
no_title = GeoLocation.objects.create(lat=1.0, lon=2.0)
assert Trail.derive_default_title(None, no_title) is None
def test_returns_none_without_geo_titles(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0)
end = GeoLocation.objects.create(lat=3.0, lon=4.0)
assert Trail.derive_default_title(start, end) is None
def test_point_to_point(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0, title="Start Point")
end = GeoLocation.objects.create(lat=3.0, lon=4.0, title="End Point")
assert Trail.derive_default_title(start, end) == "Start Point to End Point"
def test_same_location(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0, title="The Trailhead")
end = GeoLocation.objects.create(lat=1.0, lon=2.0, title="The Trailhead")
assert Trail.derive_default_title(start, end) == "The Trailhead"
def test_loop_within_tolerance(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0, title="The Loop")
end = GeoLocation.objects.create(lat=1.0005, lon=2.0005, title="The Loop End")
assert Trail.derive_default_title(start, end) == "The Loop"
class TestRouteTag:
def test_point_to_point(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0)
end = GeoLocation.objects.create(lat=3.0, lon=4.0)
trail = Trail.objects.create(
title="Point to Point",
trailhead_location=start,
trail_terminus_location=end,
)
assert trail.route_tag == "point-to-point"
def test_out_and_back_same_location(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0)
trail = Trail.objects.create(
title="Out and Back",
trailhead_location=start,
trail_terminus_location=start,
)
assert trail.route_tag == "out-and-back"
def test_out_and_back_within_jitter(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0)
end = GeoLocation.objects.create(lat=1.0005, lon=2.0005)
trail = Trail.objects.create(
title="Near Loop", trailhead_location=start, trail_terminus_location=end
)
assert trail.route_tag == "out-and-back"
def test_none_without_terminus(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0)
trail = Trail.objects.create(title="No Terminus", trailhead_location=start)
assert trail.route_tag is None
def test_signal_tags_on_create(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0)
end = GeoLocation.objects.create(lat=3.0, lon=4.0)
trail = Trail.objects.create(
title="Auto Tagged", trailhead_location=start, trail_terminus_location=end
)
assert set(Trail.objects.get(pk=trail.pk).tags.names()) == {"point-to-point"}
def test_signal_updates_tag_when_terminus_changes(self, db):
start = GeoLocation.objects.create(lat=1.0, lon=2.0)
end = GeoLocation.objects.create(lat=3.0, lon=4.0)
trail = Trail.objects.create(
title="Changed Route", trailhead_location=start, trail_terminus_location=end
)
trail.trail_terminus_location = start
trail.save(update_fields=["trail_terminus_location"])
assert set(Trail.objects.get(pk=trail.pk).tags.names()) == {"out-and-back"}
class TestFindByTrailhead:
def test_exact_match(self, db):
geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)