Rename mode constants: mtb→2w, atv→4w (#20)

* Rename mode constants: mtb->2w, atv->4w

Rename the OFFROUTE travel-mode identifiers mtb->2w and atv->4w across backend and
frontend. MVUM vehicle-access classes/columns (atv, motorcycle) are a separate
vocabulary and are left untouched. UI labels (MTB/ATV) and the cost.py __main__ demo
local variables (cannot be digit-initial identifiers) are unchanged.

Backend: MODE_PROFILES, MODE_TO_COSTING, MODE_TO_VALID_HIGHWAYS, AUTO_MODE_PRIORITY,
_MODES_* sets, CATEGORY_ELIGIBLE_MODES, route()/compute_cost*/_pathfind_wilderness
Literals, VALID_MODES, and tests. offroute_route.py adds a backward-compat shim mapping
legacy mode=mtb->2w and mode=atv->4w before validation so bookmarked URLs still work.

Frontend: store.js routeMode doc, DirectionsPanel TRAVEL_MODES ids + SELECTED_MODE_LABEL
keys, Panel TRAVEL_MODES ids, ManeuverList network_mode->verb map keys, api.js jsdoc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix user-visible labels: MTB->2W, ATV->4W + grep cleanup

Update the user-facing travel-mode labels to match the renamed ids:
- DirectionsPanel TRAVEL_MODES (2w -> "2W", 4w -> "4W") + SELECTED_MODE_LABEL values.
- Panel TRAVEL_MODES labels.
- Stale test comment: mode=mtb -> mode=2w.

Grep pass over frontend/src + backend/services/navi_offroute found no remaining
mode-identifier string literals to rename. Residual hits are all out-of-scope: MVUM
vehicle-access vocabulary (mvum.py, /api/mvum output, single-quoted test fixtures), the
cost.py __main__ demo locals, the offroute_route.py back-compat shim, and comments
referencing the historical names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
malice 2026-05-25 15:33:47 -06:00 committed by GitHub
commit ede6852657
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 69 additions and 64 deletions

View file

@ -1,7 +1,7 @@
"""
Multi-mode travel cost functions for OFFROUTE.
Supports four travel modes: foot, mtb, atv, vehicle.
Supports four travel modes: foot, 2w, 4w, vehicle.
Each mode has its own speed function, max slope, trail access rules,
and terrain friction overrides.
@ -120,8 +120,8 @@ MODE_PROFILES: Dict[str, ModeProfile] = {
wilderness_impassable=False,
),
"mtb": ModeProfile(
name="mtb",
"2w": ModeProfile(
name="2w",
description="Mountain bike / dirt bike (Herzog wheeled model)",
speed_function="herzog",
base_speed_kmh=12.0,
@ -143,8 +143,8 @@ MODE_PROFILES: Dict[str, ModeProfile] = {
wilderness_impassable=True,
),
"atv": ModeProfile(
name="atv",
"4w": ModeProfile(
name="4w",
description="ATV / side-by-side (Herzog wheeled model, higher base speed)",
speed_function="herzog",
base_speed_kmh=25.0,
@ -225,7 +225,7 @@ def compute_cost_multiplier_grid(
friction: Optional[np.ndarray] = None,
friction_raw: Optional[np.ndarray] = None,
wilderness: Optional[np.ndarray] = None,
mode: Literal["foot", "mtb", "atv", "vehicle"] = "foot",
mode: Literal["foot", "2w", "4w", "vehicle"] = "foot",
) -> np.ndarray:
"""Per-cell SLOPE-FREE context cost multiplier for the anisotropic A* pathfinder.
@ -300,7 +300,7 @@ def compute_cost_grid(
wilderness: Optional[np.ndarray] = None,
mvum: Optional[np.ndarray] = None,
boundary_mode: Literal["strict", "pragmatic", "emergency"] = "pragmatic",
mode: Literal["foot", "mtb", "atv", "vehicle"] = "foot"
mode: Literal["foot", "2w", "4w", "vehicle"] = "foot"
) -> np.ndarray:
"""
Compute isotropic travel cost grid from elevation data.
@ -327,7 +327,7 @@ def compute_cost_grid(
MVUM closures respond to boundary_mode (strict/pragmatic/emergency).
Foot mode should pass None (MVUM is motor-vehicle specific).
boundary_mode: How to handle barriers ("strict", "pragmatic", "emergency")
mode: Travel mode ("foot", "mtb", "atv", "vehicle")
mode: Travel mode ("foot", "2w", "4w", "vehicle")
Returns:
2D array of travel cost in seconds per cell.
@ -550,7 +550,7 @@ if __name__ == "__main__":
trails = np.zeros((10, 10), dtype=np.uint8)
trails[5, :] = 5 # Road across middle
for mode_name in ["foot", "mtb", "atv", "vehicle"]:
for mode_name in ["foot", "2w", "4w", "vehicle"]:
cost = compute_cost_grid(
elev, cell_size_m=30.0,
friction=friction,
@ -567,7 +567,7 @@ if __name__ == "__main__":
wilderness = np.zeros((10, 10), dtype=np.uint8)
wilderness[3:7, 3:7] = 255
for mode_name in ["foot", "mtb", "atv", "vehicle"]:
for mode_name in ["foot", "2w", "4w", "vehicle"]:
cost = compute_cost_grid(
elev, cell_size_m=30.0,
wilderness=wilderness,

View file

@ -18,7 +18,7 @@ logger = logging.getLogger('navi_offroute.route')
bp = Blueprint('offroute', __name__)
VALID_MODES = ("auto", "foot", "mtb", "atv", "vehicle")
VALID_MODES = ("auto", "foot", "2w", "4w", "vehicle")
VALID_BOUNDARY_MODES = ("strict", "pragmatic", "emergency")
@ -29,7 +29,7 @@ def api_offroute():
Request body:
{start:[lat,lon], end:[lat,lon],
mode: auto|foot|mtb|atv|vehicle (default foot),
mode: auto|foot|2w|4w|vehicle (default foot),
boundary_mode: strict|pragmatic|emergency (default pragmatic)}
Response: {status:"ok", route:<GeoJSON FeatureCollection>, summary:{...}}
@ -53,8 +53,13 @@ def api_offroute():
end_lat, end_lon = float(end[0]), float(end[1])
mode = data.get("mode", "foot")
# Backward-compat: map legacy mode names from bookmarked URLs.
if mode == "mtb":
mode = "2w"
elif mode == "atv":
mode = "4w"
if mode not in VALID_MODES:
return jsonify({"status": "error", "message": "mode must be auto, foot, mtb, atv, or vehicle"}), 400
return jsonify({"status": "error", "message": "mode must be auto, foot, 2w, 4w, or vehicle"}), 400
boundary_mode = data.get("boundary_mode", "pragmatic")
if boundary_mode not in VALID_BOUNDARY_MODES:

View file

@ -11,7 +11,7 @@ Off-network detection: Valhalla /locate snap distance > 500m = off-network.
IMPORTANT: The wilderness segment ALWAYS uses foot mode for pathfinding.
The user's selected mode affects:
1. Which entry points are valid (foot=any, mtb=tracks+roads, vehicle=roads only)
1. Which entry points are valid (foot=any, 2w=tracks+roads, vehicle=roads only)
2. The Valhalla costing profile for the network segment
"""
import gc
@ -83,20 +83,20 @@ PATH_USE_VALUES = frozenset({
MODE_TO_COSTING = {
"auto": "auto",
"foot": "pedestrian",
"mtb": "bicycle",
"atv": "auto",
"2w": "bicycle",
"4w": "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"]
AUTO_MODE_PRIORITY = ["vehicle", "4w", "2w", "foot"]
# Per-endpoint travel-mode eligibility from an OSM-style "key:value" category hint.
# Looked up exact first, then "key:*" wildcard (see _eligible_modes_from_category).
_MODES_ALL = frozenset({"vehicle", "atv", "mtb", "foot"})
_MODES_TRACK = frozenset({"atv", "mtb", "foot"})
_MODES_PATH = frozenset({"mtb", "foot"})
_MODES_ALL = frozenset({"vehicle", "4w", "2w", "foot"})
_MODES_TRACK = frozenset({"4w", "2w", "foot"})
_MODES_PATH = frozenset({"2w", "foot"})
_MODES_FOOT = frozenset({"foot"})
CATEGORY_ELIGIBLE_MODES = {
@ -115,9 +115,9 @@ CATEGORY_ELIGIBLE_MODES = {
"place:village": _MODES_ALL, "place:hamlet": _MODES_ALL,
"place:suburb": _MODES_ALL, "place:neighbourhood": _MODES_ALL,
"railway:station": _MODES_ALL,
# Track-like -> atv/mtb/foot
# Track-like -> 4w/2w/foot
"highway:track": _MODES_TRACK, "highway:trailhead": _MODES_TRACK,
# Path-like -> mtb/foot
# Path-like -> 2w/foot
"highway:path": _MODES_PATH, "highway:bridleway": _MODES_PATH,
# Foot-only
"highway:footway": _MODES_FOOT, "highway:steps": _MODES_FOOT,
@ -130,15 +130,15 @@ CATEGORY_ELIGIBLE_MODES = {
}
# Mode to valid entry point highway classes
# foot = any trail/track/road, mtb = tracks and roads, vehicle = roads only
# foot = any trail/track/road, 2w = tracks and roads, vehicle = roads only
MODE_TO_VALID_HIGHWAYS = {
"auto": {"primary", "secondary", "tertiary", "unclassified", "residential",
"service"},
"foot": {"primary", "secondary", "tertiary", "unclassified", "residential",
"service", "track", "path", "footway", "bridleway"},
"mtb": {"primary", "secondary", "tertiary", "unclassified", "residential",
"2w": {"primary", "secondary", "tertiary", "unclassified", "residential",
"service", "track"},
"atv": {"primary", "secondary", "tertiary", "unclassified", "residential",
"4w": {"primary", "secondary", "tertiary", "unclassified", "residential",
"service", "track"},
"vehicle": {"primary", "secondary", "tertiary", "unclassified", "residential",
"service"},
@ -597,7 +597,7 @@ class OffrouteRouter:
start_lon: float,
end_lat: float,
end_lon: float,
mode: Literal["auto", "foot", "mtb", "atv", "vehicle"] = "foot",
mode: Literal["auto", "foot", "2w", "4w", "vehicle"] = "foot",
boundary_mode: Literal["strict", "pragmatic", "emergency"] = "pragmatic",
start_category: Optional[str] = None,
end_category: Optional[str] = None
@ -614,7 +614,7 @@ class OffrouteRouter:
Args:
start_lat, start_lon: Starting coordinates
end_lat, end_lon: Destination coordinates
mode: Travel mode (foot, mtb, atv, vehicle)
mode: Travel mode (foot, 2w, 4w, vehicle)
boundary_mode: How to handle private land (strict, pragmatic, emergency)
Returns a GeoJSON FeatureCollection with route segments.
@ -704,8 +704,8 @@ class OffrouteRouter:
Runs the three distinct costings (auto/pedestrian/bicycle) in parallel and
applies the per-mode snap-distance + road-class rules. snap_cache dedupes
/locate results within a single request."""
# auto costing -> vehicle/atv reach, bicycle -> mtb reach, pedestrian -> foot
costing_modes = {"auto": "vehicle", "pedestrian": "foot", "bicycle": "mtb"}
# auto costing -> vehicle/4w reach, bicycle -> 2w reach, pedestrian -> foot
costing_modes = {"auto": "vehicle", "pedestrian": "foot", "bicycle": "2w"}
need = [c for c in costing_modes if (lat, lon, c) not in snap_cache]
if need:
with ThreadPoolExecutor(max_workers=3) as ex:
@ -727,15 +727,15 @@ 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: near a paved road OR a track (auto costing)
# 4w: 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: near a paved road OR a track/path (bicycle costing)
modes.add("4w")
# 2w: 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")
modes.add("2w")
return frozenset(modes)
def _route_auto(
@ -945,7 +945,7 @@ class OffrouteRouter:
if not entry_points:
if mode == "vehicle":
msg = f"No roads found within {EXPANDED_SEARCH_RADIUS_KM}km. Try a different mode."
elif mode in ("mtb", "atv"):
elif mode in ("2w", "4w"):
msg = f"No tracks or roads found within {EXPANDED_SEARCH_RADIUS_KM}km. Try foot mode."
else:
msg = f"No trail entry points found within {EXPANDED_SEARCH_RADIUS_KM}km of start."
@ -1025,7 +1025,7 @@ class OffrouteRouter:
if not entry_points:
if mode == "vehicle":
msg = f"No roads found within {EXPANDED_SEARCH_RADIUS_KM}km of destination. Try a different mode."
elif mode in ("mtb", "atv"):
elif mode in ("2w", "4w"):
msg = f"No tracks or roads found within {EXPANDED_SEARCH_RADIUS_KM}km of destination. Try foot mode."
else:
msg = f"No trail entry points found within {EXPANDED_SEARCH_RADIUS_KM}km of destination."

View file

@ -265,7 +265,7 @@ def test_admin_info_no_secrets_and_probes(client, monkeypatch):
from services.navi_offroute.router import OffrouteRouter, AUTO_MODE_PRIORITY
ALL_MODES = frozenset({"vehicle", "atv", "mtb", "foot"})
ALL_MODES = frozenset({"vehicle", "4w", "2w", "foot"})
def _stub_route(per_mode, calls):
@ -284,8 +284,8 @@ def _all_ok():
def test_eligible_modes_exact_match():
r = object.__new__(OffrouteRouter)
assert r._eligible_modes_from_category("highway:residential") == ALL_MODES
assert r._eligible_modes_from_category("highway:track") == frozenset({"atv", "mtb", "foot"})
assert r._eligible_modes_from_category("highway:path") == frozenset({"mtb", "foot"})
assert r._eligible_modes_from_category("highway:track") == frozenset({"4w", "2w", "foot"})
assert r._eligible_modes_from_category("highway:path") == frozenset({"2w", "foot"})
assert r._eligible_modes_from_category("highway:footway") == frozenset({"foot"})
@ -329,8 +329,8 @@ 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"},
"4w": {"status": "error", "message": "No tracks found"},
"2w": {"status": "error", "message": "No tracks found"},
"foot": {"status": "ok"},
}
monkeypatch.setattr(OffrouteRouter, "route", _stub_route(per_mode, calls))
@ -338,7 +338,7 @@ def test_route_auto_falls_through_to_foot(monkeypatch):
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"]
assert calls == ["vehicle", "4w", "2w", "foot"]
def test_route_auto_all_error_returns_error(monkeypatch):
@ -351,7 +351,7 @@ def test_route_auto_all_error_returns_error(monkeypatch):
assert out["status"] == "error"
assert "selected_mode" not in out
assert out["selected_mode_set"] == sorted(ALL_MODES)
assert calls == ["vehicle", "atv", "mtb", "foot"]
assert calls == ["vehicle", "4w", "2w", "foot"]
def test_route_auto_selected_mode_present_in_ok_response(monkeypatch):
@ -359,15 +359,15 @@ 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"},
"4w": {"status": "ok", "route": {"type": "FeatureCollection", "features": []}},
"2w": {"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 out["selected_mode"] == "atv"
assert out["selected_mode"] == "4w"
# ── _route_auto with category type hints (real _eligible_modes_from_category) ──
@ -388,9 +388,9 @@ def test_route_auto_address_to_trailhead_picks_atv(monkeypatch):
r = object.__new__(OffrouteRouter)
out = r._route_auto(42.0, -114.0, 42.5, -114.5, "pragmatic",
start_category="building:house", end_category="highway:trailhead")
assert out["selected_mode"] == "atv"
assert out["selected_mode"] == "4w"
assert "vehicle" not in calls
assert out["selected_mode_set"] == sorted({"atv", "mtb", "foot"})
assert out["selected_mode_set"] == sorted({"4w", "2w", "foot"})
def test_route_auto_address_to_peak_picks_foot(monkeypatch):
@ -436,7 +436,7 @@ def test_spatial_service_other_picks_vehicle(monkeypatch):
r = object.__new__(OffrouteRouter)
modes = r._spatial_eligible_modes(43.6, -116.2, {})
assert "vehicle" in modes
assert modes == frozenset({"vehicle", "atv", "mtb", "foot"})
assert modes == frozenset({"vehicle", "4w", "2w", "foot"})
def test_spatial_use_track_picks_atv_mtb_foot(monkeypatch):
@ -445,7 +445,7 @@ def test_spatial_use_track_picks_atv_mtb_foot(monkeypatch):
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 modes == frozenset({"4w", "2w", "foot"})
assert "vehicle" not in modes
@ -455,7 +455,7 @@ def test_spatial_use_footway_picks_mtb_foot(monkeypatch):
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"})
assert modes == frozenset({"2w", "foot"})
def test_spatial_no_class_no_use_picks_foot(monkeypatch):
@ -635,7 +635,7 @@ def test_compute_cost_multiplier_grid_math():
fr = _np.full((4, 4), 30, dtype=_np.uint8)
fr[0, 0] = 80
mult = compute_cost_multiplier_grid(
elev, 30.0, 30.0, friction=friction, friction_raw=fr, wilderness=None, mode="mtb")
elev, 30.0, 30.0, friction=friction, friction_raw=fr, wilderness=None, mode="2w")
assert mult[1, 1] == 4.0 # 2.0 friction * 2.0 grass override
assert _np.isinf(mult[0, 0]) # water impassable
@ -659,7 +659,7 @@ class _FakeGrid:
def test_pathfind_wilderness_always_uses_foot_effort(monkeypatch):
# Even when called with mode="mtb", the wilderness cost is computed as foot:
# Even when called with mode="2w", the wilderness cost is computed as foot:
# compute_cost_multiplier_grid receives mode="foot", and A* gets the foot speed
# function (tobler=0), foot base speed (6.0), and foot trail friction.
captured = {}
@ -688,10 +688,10 @@ def test_pathfind_wilderness_always_uses_foot_effort(monkeypatch):
r.wilderness_reader = None # foot is not wilderness_impassable -> not loaded anyway
ep = [{"lat": 44.001, "lon": -115.001, "highway_class": "track", "name": "t", "land_status": "open"}]
out = r._pathfind_wilderness(44.0, -115.0, 44.001, -115.001, ep, "pragmatic", "start", mode="mtb")
out = r._pathfind_wilderness(44.0, -115.0, 44.001, -115.001, ep, "pragmatic", "start", mode="2w")
assert out["status"] == "ok"
assert captured["mult_mode"] == "foot" # cost grid built as foot despite mode=mtb
assert captured["mult_mode"] == "foot" # cost grid built as foot despite mode=2w
assert captured["speed_function_id"] == 0 # tobler (foot)
assert captured["base_speed"] == 6.0 # foot base speed
assert captured["lookup"][5] == 0.1 # foot road

View file

@ -331,7 +331,7 @@ const MVUM_URL = "/api/mvum"
* Request an offroute route from the pathfinder API.
* @param {object} start - { lat, lon }
* @param {object} end - { lat, lon }
* @param {string} mode - auto | foot | mtb | atv | vehicle
* @param {string} mode - auto | foot | 2w | 4w | vehicle
* @param {string} boundaryMode - strict | pragmatic | emergency
* @param {string} [startCategory] - OSM "key:value" hint for the origin (Auto mode)
* @param {string} [endCategory] - OSM "key:value" hint for the destination (Auto mode)

View file

@ -10,13 +10,13 @@ import ManeuverList from "./ManeuverList"
const TRAVEL_MODES = [
{ id: "auto", label: "Auto", Icon: Zap },
{ id: "foot", label: "Foot", Icon: Footprints },
{ id: "mtb", label: "MTB", Icon: Bike },
{ id: "atv", label: "ATV", Icon: Car },
{ id: "2w", label: "2W", Icon: Bike },
{ id: "4w", label: "4W", Icon: Car },
{ id: "vehicle", label: "Drive", Icon: Car },
]
// Maps the backend's selected_mode to the chip label shown in the "Auto chose X" badge.
const SELECTED_MODE_LABEL = { vehicle: "Drive", atv: "ATV", mtb: "MTB", foot: "Foot" }
const SELECTED_MODE_LABEL = { vehicle: "Drive", "4w": "4W", "2w": "2W", foot: "Foot" }
const BOUNDARY_MODES = [
{ id: "strict", label: "Strict", Icon: Shield, title: "Avoid barriers" },

View file

@ -106,7 +106,7 @@ function ManeuverIcon({ type }) {
/**
* Add transport mode prefix to network maneuver instruction.
* "Drive east on..." for auto, "Walk south on..." for foot, "Ride north on..." for mtb
* "Drive east on..." for auto, "Walk south on..." for foot, "Ride north on..." for 2w
*/
function formatNetworkInstruction(instruction, mode) {
if (!instruction) return ''
@ -116,9 +116,9 @@ function formatNetworkInstruction(instruction, mode) {
'auto': 'Drive',
'foot': 'Walk',
'pedestrian': 'Walk',
'mtb': 'Ride',
'2w': 'Ride',
'bicycle': 'Ride',
'atv': 'Drive',
'4w': 'Drive',
'vehicle': 'Drive',
}
const verb = modeVerbs[mode] || 'Go'

View file

@ -14,8 +14,8 @@ import PlaceDetail from './PlaceDetail'
const TRAVEL_MODES = [
{ id: 'auto', label: 'Drive', Icon: Car },
{ id: 'foot', label: 'Foot', Icon: Footprints },
{ id: 'mtb', label: 'MTB', Icon: Bike },
{ id: 'atv', label: 'ATV', Icon: Car },
{ id: '2w', label: '2W', Icon: Bike },
{ id: '4w', label: '4W', Icon: Car },
{ id: 'vehicle', label: '4x4', Icon: Car },
]

View file

@ -31,7 +31,7 @@ export const useStore = create((set, get) => ({
routeStart: null, // { lat, lon, name }
routeEnd: null, // { lat, lon, name }
stops: [], // Intermediate waypoints only: [{ id, lat, lon, name }, ...]
routeMode: "auto", // auto | foot | mtb | atv | vehicle
routeMode: "auto", // auto | foot | 2w | 4w | vehicle
boundaryMode: "strict", // strict | pragmatic | emergency
routeResult: null, // Response from /api/offroute
routeLoading: false,