mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-08-26 17:31:37 +00:00
Merge pull request #4 from zvx-echo6/feat/auto-mode-backend
feat(offroute): backend Auto mode probes [vehicle,atv,mtb,foot]
This commit is contained in:
commit
f72a05bcb7
2 changed files with 108 additions and 1 deletions
|
|
@ -68,6 +68,10 @@ MODE_TO_COSTING = {
|
||||||
"vehicle": "auto",
|
"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
|
# Mode to valid entry point highway classes
|
||||||
# foot = any trail/track/road, mtb = tracks and roads, vehicle = roads only
|
# foot = any trail/track/road, mtb = tracks and roads, vehicle = roads only
|
||||||
MODE_TO_VALID_HIGHWAYS = {
|
MODE_TO_VALID_HIGHWAYS = {
|
||||||
|
|
@ -511,7 +515,7 @@ class OffrouteRouter:
|
||||||
start_lon: float,
|
start_lon: float,
|
||||||
end_lat: float,
|
end_lat: float,
|
||||||
end_lon: 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"
|
boundary_mode: Literal["strict", "pragmatic", "emergency"] = "pragmatic"
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -531,6 +535,11 @@ class OffrouteRouter:
|
||||||
|
|
||||||
Returns a GeoJSON FeatureCollection with route segments.
|
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:
|
if mode not in MODE_TO_COSTING:
|
||||||
return {"status": "error", "message": f"Unknown mode: {mode}"}
|
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
|
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(
|
def _route_D_network_only(
|
||||||
self,
|
self,
|
||||||
start_lat: float, start_lon: float,
|
start_lat: float, start_lon: float,
|
||||||
|
|
|
||||||
|
|
@ -256,3 +256,70 @@ def test_admin_info_no_secrets_and_probes(client, monkeypatch):
|
||||||
assert {'dem', 'osm_pbf', 'navi_db', 'barriers_tif', 'wilderness_tif',
|
assert {'dem', 'osm_pbf', 'navi_db', 'barriers_tif', 'wilderness_tif',
|
||||||
'trails_tif', 'friction_vrt'} == fs_names
|
'trails_tif', 'friction_vrt'} == fs_names
|
||||||
assert all(set(f) == {'name', 'path', 'exists', 'readable'} for f in d['filesystem'])
|
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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue