mirror of
https://github.com/zvx-echo6/recon.git
synced 2026-05-20 06:34:40 +02:00
feat(offroute): MVUM legal access — pathfinder integration + places panel API + boundary_mode control
MVUM Data Import: - Downloaded USFS MVUM Roads (150,636 features) and Trails (28,741 features) - Imported to navi.db as mvum_roads and mvum_trails tables - Idaho coverage: ~8,994 roads and ~4,504 trails across 7 national forests - Preserved all vehicle-class fields (ATV, MOTORCYCLE, HIGHCLEARANCEVEHICLE, etc.) - Preserved seasonal date ranges (*_DATESOPEN fields) New mvum.py module: - MVUMReader class for querying MVUM data by bbox and nearest point - parse_date_range() for seasonal date string parsing (MM/DD-MM/DD format) - check_access() for determining open/closed status with date checking - symbol_to_access() fallback when per-vehicle fields are null - get_mvum_access_grid() for rasterizing MVUM to pathfinder grid Cost function integration: - Added mvum parameter to compute_cost_grid() - MVUM closures respond to boundary_mode: * strict = impassable (np.inf) * pragmatic = 5x friction penalty * emergency = ignored entirely - Foot mode skips MVUM (motor-vehicle specific) Router integration: - Loads MVUM access grid for motorized modes (mtb, atv, vehicle) - Tracks mvum_closed_crossings in path summary Places Panel API: - GET /api/mvum?lat=XX&lon=XX&radius=50 - Returns MVUM feature with access status for all vehicle classes - Includes seasonal date ranges, maintenance level, forest/district info - GeoJSON geometry for map display Validation: - MVUM places endpoint tested with Sawtooth NF road - All four modes validated with strict/pragmatic/emergency boundary modes - Foot mode correctly ignores MVUM restrictions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bc463188d5
commit
2252905986
4 changed files with 809 additions and 2 deletions
|
|
@ -27,6 +27,7 @@ from .cost import compute_cost_grid
|
|||
from .friction import FrictionReader, friction_to_multiplier
|
||||
from .barriers import BarrierReader, WildernessReader, DEFAULT_WILDERNESS_PATH
|
||||
from .trails import TrailReader
|
||||
from .mvum import get_mvum_access_grid
|
||||
|
||||
# Paths
|
||||
NAVI_DB_PATH = Path("/mnt/nav/navi.db")
|
||||
|
|
@ -407,6 +408,22 @@ class OffrouteRouter:
|
|||
target_shape=elevation.shape
|
||||
)
|
||||
|
||||
# Load MVUM access data (only for motorized modes)
|
||||
# MVUM is motor-vehicle specific — foot mode skips entirely
|
||||
mvum = None
|
||||
if mode in ("mtb", "atv", "vehicle"):
|
||||
try:
|
||||
mvum = get_mvum_access_grid(
|
||||
south=bbox["south"], north=bbox["north"],
|
||||
west=bbox["west"], east=bbox["east"],
|
||||
target_shape=elevation.shape,
|
||||
mode=mode,
|
||||
check_date=None, # TODO: accept date parameter
|
||||
)
|
||||
except Exception as e:
|
||||
# MVUM data may not be available - continue without it
|
||||
pass
|
||||
|
||||
# Compute cost grid with mode-specific parameters
|
||||
cost = compute_cost_grid(
|
||||
elevation,
|
||||
|
|
@ -416,12 +433,13 @@ class OffrouteRouter:
|
|||
trails=trails,
|
||||
barriers=barriers,
|
||||
wilderness=wilderness,
|
||||
mvum=mvum,
|
||||
boundary_mode=boundary_mode,
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
# Free intermediate arrays to reduce memory before MCP
|
||||
# Note: Keep trails and barriers - needed for path statistics
|
||||
# Note: Keep trails, barriers, and mvum - needed for path statistics
|
||||
del friction_mult, friction_raw, wilderness
|
||||
import gc
|
||||
gc.collect()
|
||||
|
|
@ -471,6 +489,7 @@ class OffrouteRouter:
|
|||
elevations = []
|
||||
trail_values = []
|
||||
barrier_crossings = 0
|
||||
mvum_closed_crossings = 0
|
||||
|
||||
for row, col in path_indices:
|
||||
lat, lon = self.dem_reader.pixel_to_latlon(row, col, meta)
|
||||
|
|
@ -479,6 +498,8 @@ class OffrouteRouter:
|
|||
trail_values.append(trails[row, col])
|
||||
if barriers[row, col] == 255:
|
||||
barrier_crossings += 1
|
||||
if mvum is not None and mvum[row, col] == 255:
|
||||
mvum_closed_crossings += 1
|
||||
|
||||
# Calculate stats
|
||||
wilderness_distance_m = 0
|
||||
|
|
@ -497,8 +518,10 @@ class OffrouteRouter:
|
|||
total_cells = len(trail_arr)
|
||||
on_trail_pct = float(100 * on_trail_cells / total_cells) if total_cells > 0 else 0
|
||||
|
||||
# Free trails and barriers now that path stats are computed
|
||||
# Free trails, barriers, and mvum now that path stats are computed
|
||||
del trails, barriers
|
||||
if mvum is not None:
|
||||
del mvum
|
||||
|
||||
# Entry point
|
||||
entry_lat = best_entry["entry_point"]["lat"]
|
||||
|
|
@ -572,6 +595,7 @@ class OffrouteRouter:
|
|||
"on_trail_pct": on_trail_pct,
|
||||
"cell_count": total_cells,
|
||||
"barrier_crossings": barrier_crossings,
|
||||
"mvum_closed_crossings": mvum_closed_crossings,
|
||||
"mode": mode,
|
||||
},
|
||||
"geometry": {"type": "LineString", "coordinates": wilderness_coords}
|
||||
|
|
@ -620,6 +644,7 @@ class OffrouteRouter:
|
|||
"network_duration_minutes": float(network_segment["duration_minutes"]) if network_segment else 0,
|
||||
"on_trail_pct": on_trail_pct,
|
||||
"barrier_crossings": barrier_crossings,
|
||||
"mvum_closed_crossings": mvum_closed_crossings,
|
||||
"boundary_mode": boundary_mode,
|
||||
"mode": mode,
|
||||
"entry_point": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue