mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-08-26 17:31:37 +00:00
Backend-only. For strict-boundary motorized routes, turn MVUM-closed segments into buffered Valhalla exclude_polygons so routing actively avoids them. pragmatic/emergency keep Layer-1 annotate-only behavior; foot is never excluded. - mvum_exclude.py (new): build_exclude_polygons(start,end,mode,spatial_index,on_date, boundary_mode) -> GeoJSON Polygon dicts, or None when not applicable (foot / non-strict / no index / unmappable mode). Queries the Layer-0 index over a 5km-expanded bbox, keeps only features closed to the mode (via mvum_annotate._status_for_feature), buffers each ~15m (lat-corrected), emits one Polygon per part (MultiPolygon split). Caps at 500 with a warning. - router.py: route() computes self._exclude_polygons once per call (after mode validation; has boundary_mode), so each Auto candidate probes against its own exclusions. Both Valhalla /route builders (_route_D_network_only and _valhalla_route) inject exclude_polygons in array-of-rings form (outer ring per Polygon); omitted when None/empty. - 6 tests: strict builds polygons, pragmatic/emergency/foot -> None, open not excluded, 1000 closed -> capped at 500 + warning. No frontend changes (Layer-1 closure warning still fires for residual closures). Wilderness pathfinder, Layer-0 index, and Layer-1 annotation untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
78 lines
3 KiB
Python
78 lines
3 KiB
Python
"""MVUM Layer 2c — build Valhalla exclude_polygons for closure-avoiding routing.
|
|
|
|
For strict-boundary motorized routes, turn MVUM-closed segments into buffered
|
|
exclusion polygons so Valhalla actively routes around them. Only strict boundary mode
|
|
excludes; pragmatic/emergency keep Layer-1 annotate-only behavior. No wilderness, Layer-0
|
|
or Layer-1 changes.
|
|
"""
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from shapely.geometry import mapping
|
|
|
|
from .mvum import get_mode_field, _buffer_degrees_for_meters
|
|
from .mvum_annotate import _ROUTE_MODE_TO_MVUM, _status_for_feature
|
|
|
|
logger = logging.getLogger("navi_offroute.mvum_exclude")
|
|
|
|
EXCLUDE_BUFFER_M = 15.0 # buffer around a closed segment
|
|
BBOX_EXPAND_M = 5000.0 # candidate search box expansion around the route
|
|
MAX_EXCLUDE_POLYGONS = 500 # Valhalla request guard
|
|
|
|
|
|
def build_exclude_polygons(start_lat, start_lon, end_lat, end_lon, mode,
|
|
spatial_index, on_date, boundary_mode) -> Optional[List[dict]]:
|
|
"""Return GeoJSON Polygon dicts for MVUM segments closed to `mode`, or None when
|
|
exclusion does not apply (foot, non-strict boundary, or no spatial index).
|
|
|
|
The router converts these to Valhalla's array-of-rings exclude_polygons format.
|
|
"""
|
|
if mode == "foot":
|
|
return None
|
|
if boundary_mode != "strict":
|
|
return None
|
|
if spatial_index is None:
|
|
return None
|
|
mvum_mode = _ROUTE_MODE_TO_MVUM.get(mode)
|
|
if mvum_mode is None: # unmappable / auto -> nothing to exclude
|
|
return None
|
|
|
|
on_date = on_date or datetime.now()
|
|
status_field, dates_field = get_mode_field(mvum_mode)
|
|
check_date = (on_date.month, on_date.day)
|
|
|
|
min_lat, max_lat = min(start_lat, end_lat), max(start_lat, end_lat)
|
|
min_lon, max_lon = min(start_lon, end_lon), max(start_lon, end_lon)
|
|
mid_lat = (min_lat + max_lat) / 2.0
|
|
bbox_buf = _buffer_degrees_for_meters(BBOX_EXPAND_M, mid_lat)
|
|
seg_buf = _buffer_degrees_for_meters(EXCLUDE_BUFFER_M, mid_lat)
|
|
|
|
candidates = spatial_index.query_bbox(
|
|
min_lat - bbox_buf, min_lon - bbox_buf, max_lat + bbox_buf, max_lon + bbox_buf)
|
|
|
|
polys: List[dict] = []
|
|
for rec in candidates:
|
|
if _status_for_feature(rec, status_field, dates_field, mvum_mode, check_date) != "closed":
|
|
continue
|
|
geom = rec.get("geometry")
|
|
if geom is None or geom.is_empty:
|
|
continue
|
|
buffered = geom.buffer(seg_buf)
|
|
if buffered.is_empty:
|
|
continue
|
|
if buffered.geom_type == "Polygon":
|
|
parts = [buffered]
|
|
elif buffered.geom_type == "MultiPolygon":
|
|
parts = list(buffered.geoms)
|
|
else:
|
|
continue
|
|
for part in parts:
|
|
polys.append(mapping(part))
|
|
|
|
if len(polys) > MAX_EXCLUDE_POLYGONS:
|
|
logger.warning(
|
|
"MVUM exclude polygons (%d) exceed cap; emitting first %d",
|
|
len(polys), MAX_EXCLUDE_POLYGONS)
|
|
return polys[:MAX_EXCLUDE_POLYGONS]
|
|
return polys
|