mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-08-26 17:31:37 +00:00
Merge pull request #12 from zvx-echo6/feat/use-classification-use
fix(offroute): use classification.use for track/path; add service_other to paved
This commit is contained in:
commit
91d03f3fe7
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_TERRAIN_DELTA_M = 5
|
||||||
FLAT_SAMPLE_RADIUS_M = 50
|
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({
|
PAVED_HIGHWAY_CLASSES = frozenset({
|
||||||
"motorway", "trunk", "primary", "secondary", "tertiary",
|
"motorway", "trunk", "primary", "secondary", "tertiary",
|
||||||
"unclassified", "residential", "service",
|
"unclassified", "residential", "service", "service_other",
|
||||||
})
|
})
|
||||||
TRACK_HIGHWAY_CLASSES = frozenset({"track"})
|
TRACK_USE_VALUES = frozenset({"track"})
|
||||||
PATH_HIGHWAY_CLASSES = frozenset({
|
PATH_USE_VALUES = frozenset({
|
||||||
"path", "footway", "bridleway", "steps", "pedestrian", "cycleway",
|
"path", "footway", "cycleway", "bridleway", "steps", "pedestrian",
|
||||||
})
|
})
|
||||||
|
|
||||||
# Mode to Valhalla costing mapping
|
# Mode to Valhalla costing mapping
|
||||||
|
|
@ -553,9 +555,11 @@ class OffrouteRouter:
|
||||||
"snap_distance_m": snap_dist,
|
"snap_distance_m": snap_dist,
|
||||||
"snapped_lat": snap_lat,
|
"snapped_lat": snap_lat,
|
||||||
"snapped_lon": snap_lon,
|
"snapped_lon": snap_lon,
|
||||||
# Valhalla puts the highway class under edge.classification.classification
|
# Valhalla (verbose=true) puts the road grade under
|
||||||
# (verbose=true); defensive .get chain so missing keys yield None.
|
# 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"),
|
"road_class": edge.get("edge", {}).get("classification", {}).get("classification"),
|
||||||
|
"use": edge.get("edge", {}).get("classification", {}).get("use"),
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -566,6 +570,7 @@ class OffrouteRouter:
|
||||||
"snapped_lat": lat,
|
"snapped_lat": lat,
|
||||||
"snapped_lon": lon,
|
"snapped_lon": lon,
|
||||||
"road_class": None,
|
"road_class": None,
|
||||||
|
"use": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def route(
|
def route(
|
||||||
|
|
@ -693,8 +698,10 @@ class OffrouteRouter:
|
||||||
|
|
||||||
auto_snap = snap_cache[(lat, lon, "auto")]
|
auto_snap = snap_cache[(lat, lon, "auto")]
|
||||||
bike_snap = snap_cache[(lat, lon, "bicycle")]
|
bike_snap = snap_cache[(lat, lon, "bicycle")]
|
||||||
d_auto, cls_auto = auto_snap["snap_distance_m"], auto_snap.get("road_class")
|
d_auto = auto_snap["snap_distance_m"]
|
||||||
d_bike, cls_bike = bike_snap["snap_distance_m"], bike_snap.get("road_class")
|
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
|
modes = {"foot"} # foot is always eligible
|
||||||
# vehicle: on a paved road (tight), or near one if paved AND flat (relaxed)
|
# 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
|
(d_auto <= AUTO_SNAP_RELAXED_M and cls_auto in PAVED_HIGHWAY_CLASSES
|
||||||
and self._is_terrain_flat(lat, lon)):
|
and self._is_terrain_flat(lat, lon)):
|
||||||
modes.add("vehicle")
|
modes.add("vehicle")
|
||||||
# atv: on/near a paved or track edge
|
# atv: near a paved road OR a track (auto costing)
|
||||||
if d_auto <= AUTO_SNAP_RELAXED_M and cls_auto in (PAVED_HIGHWAY_CLASSES | TRACK_HIGHWAY_CLASSES):
|
if d_auto <= AUTO_SNAP_RELAXED_M and (
|
||||||
|
cls_auto in PAVED_HIGHWAY_CLASSES or use_auto in TRACK_USE_VALUES):
|
||||||
modes.add("atv")
|
modes.add("atv")
|
||||||
# mtb: on/near a paved, track, or path edge (bicycle costing)
|
# mtb: near a paved road OR a track/path (bicycle costing)
|
||||||
if d_bike <= AUTO_SNAP_RELAXED_M and cls_bike in (
|
if d_bike <= AUTO_SNAP_RELAXED_M and (
|
||||||
PAVED_HIGHWAY_CLASSES | TRACK_HIGHWAY_CLASSES | PATH_HIGHWAY_CLASSES):
|
cls_bike in PAVED_HIGHWAY_CLASSES
|
||||||
|
or use_bike in (TRACK_USE_VALUES | PATH_USE_VALUES)):
|
||||||
modes.add("mtb")
|
modes.add("mtb")
|
||||||
return frozenset(modes)
|
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
|
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 len(spatial_calls) == 2 # both endpoints resolved spatially
|
||||||
assert out["selected_mode"] == "vehicle"
|
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