feat(offroute): backend Auto mode probes [vehicle,atv,mtb,foot]

When mode="auto", OffrouteRouter._route_auto() probes AUTO_MODE_PRIORITY
(vehicle -> atv -> mtb -> foot) and returns the first mode whose network can
serve the route, tagging the result with selected_mode. route() already errors
when a mode cannot reach an endpoint, so the first status==ok is the most
road-capable feasible mode. Adds 4 isolation tests (route monkeypatched).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt 2026-05-24 08:53:23 +00:00
commit c1f71ccdc5
2 changed files with 108 additions and 1 deletions

View file

@ -68,6 +68,10 @@ MODE_TO_COSTING = {
"vehicle": "auto",
}
# Auto mode probes these concrete modes in capability order (most -> least
# demanding terrain) and uses the first that yields a usable route.
AUTO_MODE_PRIORITY = ["vehicle", "atv", "mtb", "foot"]
# Mode to valid entry point highway classes
# foot = any trail/track/road, mtb = tracks and roads, vehicle = roads only
MODE_TO_VALID_HIGHWAYS = {
@ -511,7 +515,7 @@ class OffrouteRouter:
start_lon: float,
end_lat: float,
end_lon: float,
mode: Literal["foot", "mtb", "atv", "vehicle"] = "foot",
mode: Literal["auto", "foot", "mtb", "atv", "vehicle"] = "foot",
boundary_mode: Literal["strict", "pragmatic", "emergency"] = "pragmatic"
) -> Dict:
"""
@ -531,6 +535,11 @@ class OffrouteRouter:
Returns a GeoJSON FeatureCollection with route segments.
"""
if mode == "auto":
return self._route_auto(
start_lat, start_lon, end_lat, end_lon, boundary_mode
)
if mode not in MODE_TO_COSTING:
return {"status": "error", "message": f"Unknown mode: {mode}"}
@ -563,6 +572,37 @@ class OffrouteRouter:
start_lat, start_lon, end_lat, end_lon, mode, boundary_mode
)
def _route_auto(
self,
start_lat: float, start_lon: float,
end_lat: float, end_lon: float,
boundary_mode: str
) -> Dict:
"""
Auto mode: pick the best concrete travel mode by terrain feasibility.
Probes AUTO_MODE_PRIORITY (vehicle -> atv -> mtb -> foot) and returns the
first mode whose network can serve the route. Each candidate's route()
already reports status="error" when its network can't reach an endpoint,
so the first status="ok" is the most road-capable feasible mode. The chosen
mode is reported back as result["selected_mode"] for the UI.
"""
last_error = None
for candidate in AUTO_MODE_PRIORITY:
result = self.route(
start_lat, start_lon, end_lat, end_lon,
mode=candidate, boundary_mode=boundary_mode
)
if result.get("status") == "ok":
result["selected_mode"] = candidate
return result
last_error = result
return last_error or {
"status": "error",
"message": "No route found in any mode"
}
def _route_D_network_only(
self,
start_lat: float, start_lon: float,

View file

@ -256,3 +256,70 @@ def test_admin_info_no_secrets_and_probes(client, monkeypatch):
assert {'dem', 'osm_pbf', 'navi_db', 'barriers_tif', 'wilderness_tif',
'trails_tif', 'friction_vrt'} == fs_names
assert all(set(f) == {'name', 'path', 'exists', 'readable'} for f in d['filesystem'])
# ── OffrouteRouter._route_auto — feasibility-based mode selection ─────────
# Tested in isolation: a bare router (no __init__/readers) with OffrouteRouter.route
# monkeypatched to a per-mode fixture. Exercises _route_auto directly, not the blueprint.
from services.navi_offroute.router import OffrouteRouter, AUTO_MODE_PRIORITY
def _stub_route(per_mode, calls):
def stub(self, start_lat, start_lon, end_lat, end_lon, mode="foot", boundary_mode="pragmatic"):
calls.append(mode)
return dict(per_mode[mode])
return stub
def test_route_auto_first_probe_ok_returns_vehicle(monkeypatch):
calls = []
per_mode = {m: {"status": "ok"} for m in AUTO_MODE_PRIORITY}
monkeypatch.setattr(OffrouteRouter, "route", _stub_route(per_mode, calls))
r = object.__new__(OffrouteRouter)
out = r._route_auto(42.0, -114.0, 42.5, -114.5, "pragmatic")
assert out["status"] == "ok"
assert out["selected_mode"] == "vehicle"
assert calls == ["vehicle"] # stops probing after first success
def test_route_auto_falls_through_to_foot(monkeypatch):
calls = []
per_mode = {
"vehicle": {"status": "error", "message": "No roads found"},
"atv": {"status": "error", "message": "No tracks found"},
"mtb": {"status": "error", "message": "No tracks found"},
"foot": {"status": "ok"},
}
monkeypatch.setattr(OffrouteRouter, "route", _stub_route(per_mode, calls))
r = object.__new__(OffrouteRouter)
out = r._route_auto(42.0, -114.0, 42.5, -114.5, "pragmatic")
assert out["status"] == "ok"
assert out["selected_mode"] == "foot"
assert calls == ["vehicle", "atv", "mtb", "foot"]
def test_route_auto_all_error_returns_error(monkeypatch):
calls = []
per_mode = {m: {"status": "error", "message": f"{m} failed"} for m in AUTO_MODE_PRIORITY}
monkeypatch.setattr(OffrouteRouter, "route", _stub_route(per_mode, calls))
r = object.__new__(OffrouteRouter)
out = r._route_auto(42.0, -114.0, 42.5, -114.5, "pragmatic")
assert out["status"] == "error"
assert "selected_mode" not in out
assert calls == ["vehicle", "atv", "mtb", "foot"]
def test_route_auto_selected_mode_present_in_ok_response(monkeypatch):
calls = []
per_mode = {
"vehicle": {"status": "error", "message": "No roads found"},
"atv": {"status": "ok", "route": {"type": "FeatureCollection", "features": []}},
"mtb": {"status": "ok"},
"foot": {"status": "ok"},
}
monkeypatch.setattr(OffrouteRouter, "route", _stub_route(per_mode, calls))
r = object.__new__(OffrouteRouter)
out = r._route_auto(42.0, -114.0, 42.5, -114.5, "pragmatic")
assert out["status"] == "ok"
assert "selected_mode" in out and out["selected_mode"] == "atv"