mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-08-26 17:31:37 +00:00
fix(offroute): use classification.use for track/path; add service_other to paved
Align spatial eligibility with Valhalla's actual /locate vocabulary: - road grade lives in classification.classification (8-value enum incl service_other, which was missing from PAVED_HIGHWAY_CLASSES); track/path/footway are NOT grades, they live in classification.use. - _locate_on_network now also returns use (defensive .get chain; None in fallback). - Renamed TRACK/PATH_HIGHWAY_CLASSES -> TRACK_USE_VALUES/PATH_USE_VALUES; atv/mtb now match on use, vehicle still on the paved grade. - 4 tests: service_other->vehicle, use=track->atv/mtb/foot, use=footway->mtb/foot, none->foot. Live-verified: the Boise end point (43.626,-116.215, service_other) now yields [atv,mtb,vehicle,foot] instead of [foot]; Auto intersection picks vehicle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
21cd810272
commit
b1c2058586
2 changed files with 70 additions and 14 deletions
|
|
@ -67,14 +67,16 @@ AUTO_SNAP_RELAXED_M = 100 # near the edge -> vehicle needs paved + flat terrain
|
|||
FLAT_TERRAIN_DELTA_M = 5
|
||||
FLAT_SAMPLE_RADIUS_M = 50
|
||||
|
||||
# Highway-class buckets used by the spatial eligibility rules.
|
||||
# Spatial eligibility vocabulary. Valhalla's verbose /locate exposes the road grade
|
||||
# under classification.classification (PAVED below) and the edge purpose under
|
||||
# classification.use (TRACK/PATH below) — track/path/footway are NOT road grades.
|
||||
PAVED_HIGHWAY_CLASSES = frozenset({
|
||||
"motorway", "trunk", "primary", "secondary", "tertiary",
|
||||
"unclassified", "residential", "service",
|
||||
"unclassified", "residential", "service", "service_other",
|
||||
})
|
||||
TRACK_HIGHWAY_CLASSES = frozenset({"track"})
|
||||
PATH_HIGHWAY_CLASSES = frozenset({
|
||||
"path", "footway", "bridleway", "steps", "pedestrian", "cycleway",
|
||||
TRACK_USE_VALUES = frozenset({"track"})
|
||||
PATH_USE_VALUES = frozenset({
|
||||
"path", "footway", "cycleway", "bridleway", "steps", "pedestrian",
|
||||
})
|
||||
|
||||
# Mode to Valhalla costing mapping
|
||||
|
|
@ -553,9 +555,11 @@ class OffrouteRouter:
|
|||
"snap_distance_m": snap_dist,
|
||||
"snapped_lat": snap_lat,
|
||||
"snapped_lon": snap_lon,
|
||||
# Valhalla puts the highway class under edge.classification.classification
|
||||
# (verbose=true); defensive .get chain so missing keys yield None.
|
||||
# Valhalla (verbose=true) puts the road grade under
|
||||
# edge.classification.classification and the edge purpose under
|
||||
# edge.classification.use; defensive .get chains -> None if absent.
|
||||
"road_class": edge.get("edge", {}).get("classification", {}).get("classification"),
|
||||
"use": edge.get("edge", {}).get("classification", {}).get("use"),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -566,6 +570,7 @@ class OffrouteRouter:
|
|||
"snapped_lat": lat,
|
||||
"snapped_lon": lon,
|
||||
"road_class": None,
|
||||
"use": None,
|
||||
}
|
||||
|
||||
def route(
|
||||
|
|
@ -693,8 +698,10 @@ class OffrouteRouter:
|
|||
|
||||
auto_snap = snap_cache[(lat, lon, "auto")]
|
||||
bike_snap = snap_cache[(lat, lon, "bicycle")]
|
||||
d_auto, cls_auto = auto_snap["snap_distance_m"], auto_snap.get("road_class")
|
||||
d_bike, cls_bike = bike_snap["snap_distance_m"], bike_snap.get("road_class")
|
||||
d_auto = auto_snap["snap_distance_m"]
|
||||
cls_auto, use_auto = auto_snap.get("road_class"), auto_snap.get("use")
|
||||
d_bike = bike_snap["snap_distance_m"]
|
||||
cls_bike, use_bike = bike_snap.get("road_class"), bike_snap.get("use")
|
||||
|
||||
modes = {"foot"} # foot is always eligible
|
||||
# vehicle: on a paved road (tight), or near one if paved AND flat (relaxed)
|
||||
|
|
@ -702,12 +709,14 @@ class OffrouteRouter:
|
|||
(d_auto <= AUTO_SNAP_RELAXED_M and cls_auto in PAVED_HIGHWAY_CLASSES
|
||||
and self._is_terrain_flat(lat, lon)):
|
||||
modes.add("vehicle")
|
||||
# atv: on/near a paved or track edge
|
||||
if d_auto <= AUTO_SNAP_RELAXED_M and cls_auto in (PAVED_HIGHWAY_CLASSES | TRACK_HIGHWAY_CLASSES):
|
||||
# atv: near a paved road OR a track (auto costing)
|
||||
if d_auto <= AUTO_SNAP_RELAXED_M and (
|
||||
cls_auto in PAVED_HIGHWAY_CLASSES or use_auto in TRACK_USE_VALUES):
|
||||
modes.add("atv")
|
||||
# mtb: on/near a paved, track, or path edge (bicycle costing)
|
||||
if d_bike <= AUTO_SNAP_RELAXED_M and cls_bike in (
|
||||
PAVED_HIGHWAY_CLASSES | TRACK_HIGHWAY_CLASSES | PATH_HIGHWAY_CLASSES):
|
||||
# mtb: near a paved road OR a track/path (bicycle costing)
|
||||
if d_bike <= AUTO_SNAP_RELAXED_M and (
|
||||
cls_bike in PAVED_HIGHWAY_CLASSES
|
||||
or use_bike in (TRACK_USE_VALUES | PATH_USE_VALUES)):
|
||||
modes.add("mtb")
|
||||
return frozenset(modes)
|
||||
|
||||
|
|
|
|||
|
|
@ -418,3 +418,50 @@ def test_route_auto_both_unknown_uses_spatial_fallback(monkeypatch):
|
|||
out = r._route_auto(42.0, -114.0, 42.5, -114.5, "pragmatic") # no categories
|
||||
assert len(spatial_calls) == 2 # both endpoints resolved spatially
|
||||
assert out["selected_mode"] == "vehicle"
|
||||
|
||||
|
||||
# ── _spatial_eligible_modes — Valhalla classification.classification + .use ──
|
||||
# _locate_on_network is monkeypatched to inject snap fixtures (road_class + use).
|
||||
|
||||
def _stub_locate_fixed(snap):
|
||||
def stub(self, lat, lon, mode="vehicle"):
|
||||
return dict(snap)
|
||||
return stub
|
||||
|
||||
|
||||
def test_spatial_service_other_picks_vehicle(monkeypatch):
|
||||
# (a) paved grade "service_other" close in -> vehicle eligible (tight tier)
|
||||
snap = {"snap_distance_m": 3.0, "road_class": "service_other", "use": "road"}
|
||||
monkeypatch.setattr(OffrouteRouter, "_locate_on_network", _stub_locate_fixed(snap))
|
||||
r = object.__new__(OffrouteRouter)
|
||||
modes = r._spatial_eligible_modes(43.6, -116.2, {})
|
||||
assert "vehicle" in modes
|
||||
assert modes == frozenset({"vehicle", "atv", "mtb", "foot"})
|
||||
|
||||
|
||||
def test_spatial_use_track_picks_atv_mtb_foot(monkeypatch):
|
||||
# (b) no road grade, use="track" -> atv/mtb/foot, NOT vehicle
|
||||
snap = {"snap_distance_m": 20.0, "road_class": None, "use": "track"}
|
||||
monkeypatch.setattr(OffrouteRouter, "_locate_on_network", _stub_locate_fixed(snap))
|
||||
r = object.__new__(OffrouteRouter)
|
||||
modes = r._spatial_eligible_modes(43.6, -116.2, {})
|
||||
assert modes == frozenset({"atv", "mtb", "foot"})
|
||||
assert "vehicle" not in modes
|
||||
|
||||
|
||||
def test_spatial_use_footway_picks_mtb_foot(monkeypatch):
|
||||
# (c) no road grade, use="footway" -> mtb/foot (path, not track -> no atv)
|
||||
snap = {"snap_distance_m": 20.0, "road_class": None, "use": "footway"}
|
||||
monkeypatch.setattr(OffrouteRouter, "_locate_on_network", _stub_locate_fixed(snap))
|
||||
r = object.__new__(OffrouteRouter)
|
||||
modes = r._spatial_eligible_modes(43.6, -116.2, {})
|
||||
assert modes == frozenset({"mtb", "foot"})
|
||||
|
||||
|
||||
def test_spatial_no_class_no_use_picks_foot(monkeypatch):
|
||||
# (d) nothing recognized -> foot only
|
||||
snap = {"snap_distance_m": 20.0, "road_class": None, "use": None}
|
||||
monkeypatch.setattr(OffrouteRouter, "_locate_on_network", _stub_locate_fixed(snap))
|
||||
r = object.__new__(OffrouteRouter)
|
||||
modes = r._spatial_eligible_modes(43.6, -116.2, {})
|
||||
assert modes == frozenset({"foot"})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue