feat(offroute): Phase O4 — multi-mode cost functions (foot/mtb/atv/vehicle)

- Add ModeProfile dataclass for data-driven mode configuration
- Implement three speed functions:
  * Tobler off-path hiking (foot)
  * Herzog wheeled-transport polynomial (mtb/atv)
  * Linear speed degradation (vehicle)
- Add WildernessReader for PAD-US Des_Tp=WA wilderness areas
- Mode-specific terrain friction overrides:
  * Forest impassable for ATV/vehicle, high friction for MTB
  * Wetland/mangrove impassable for all wheeled modes
- Trail access rules:
  * Foot trails (value 25) impassable for ATV/vehicle
- Wilderness blocking for mtb/atv/vehicle modes
- Vehicle mode allows flat grassland/cropland traversal
- Memory optimization: limit entry points, constrain bbox size
- Update router to pass mode and wilderness to cost function
- Add vehicle to API mode validation

Validated all four modes with test route:
- foot: 0.46km off-network, 12.11km network, 89% on trail
- mtb: 0.47km off-network, 13.13km network, 90% on trail
- atv: 0.47km off-network, 12.81km network, 90% on trail
- vehicle: 0.46km off-network, 12.81km network, 89% on trail

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-05-08 14:11:56 +00:00
commit bc463188d5
4 changed files with 744 additions and 331 deletions

View file

@ -2768,8 +2768,8 @@ def api_offroute():
# Parse options
mode = data.get("mode", "foot")
if mode not in ("foot", "mtb", "atv"):
return jsonify({"status": "error", "message": "mode must be foot, mtb, or atv"}), 400
if mode not in ("foot", "mtb", "atv", "vehicle"):
return jsonify({"status": "error", "message": "mode must be foot, mtb, atv, or vehicle"}), 400
boundary_mode = data.get("boundary_mode", "pragmatic")
if boundary_mode not in ("strict", "pragmatic", "emergency"):