Fix lat-aware buffer + count WKB parse failures

- query_buffered_line: replace the static _DEG_PER_M with _buffer_degrees_for_meters(),
  which scales longitude degrees by cos(lat) and uses the larger lat/lon equivalent;
  buffer at the polyline avg latitude. Early-return [] for empty coords.
- _load_table: count WKB parse failures and logger.warning once per table when > 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt 2026-05-25 23:22:01 +00:00
commit adfc110520

View file

@ -7,6 +7,7 @@ indicating which roads/trails are open or closed to specific vehicle modes.
MVUM is motor-vehicle specific foot mode should skip this layer entirely. MVUM is motor-vehicle specific foot mode should skip this layer entirely.
""" """
import logging import logging
import math
import os import os
import re import re
import sqlite3 import sqlite3
@ -33,9 +34,14 @@ def navi_db_path() -> Path:
logger = logging.getLogger("navi_offroute.mvum_spatial") logger = logging.getLogger("navi_offroute.mvum_spatial")
# Rough degrees-per-metre for small buffers (latitude scale; good enough for the def _buffer_degrees_for_meters(meters: float, lat: float) -> float:
# coarse candidate filter at Layer 0). """Approximate buffer radius in degrees for a metre tolerance at a given latitude.
_DEG_PER_M = 1.0 / 111320.0 Longitude degrees shrink with cos(lat); use the larger of the lat/lon equivalents so
the bbox-coarse buffer stays conservative."""
cos_lat = max(math.cos(math.radians(lat)), 0.01)
lat_deg = meters / 111320.0
lon_deg = meters / (111320.0 * cos_lat)
return max(lat_deg, lon_deg)
class MVUMSpatialIndex: class MVUMSpatialIndex:
@ -74,6 +80,7 @@ class MVUMSpatialIndex:
def _load_table(self, table, kind): def _load_table(self, table, kind):
count = 0 count = 0
parse_errors = 0
conn = sqlite3.connect(f"file:{self.db_path}?mode=ro", uri=True) conn = sqlite3.connect(f"file:{self.db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
try: try:
@ -83,6 +90,7 @@ class MVUMSpatialIndex:
try: try:
geom = wkb.loads(bytes(row["shape"])) geom = wkb.loads(bytes(row["shape"]))
except Exception: except Exception:
parse_errors += 1
continue continue
if geom.is_empty: if geom.is_empty:
continue continue
@ -101,6 +109,8 @@ class MVUMSpatialIndex:
count += 1 count += 1
finally: finally:
conn.close() conn.close()
if parse_errors > 0:
logger.warning("%s: %d rows had unparseable WKB shape blobs", table, parse_errors)
return count return count
@property @property
@ -127,9 +137,12 @@ class MVUMSpatialIndex:
MVUM features which merely cross the route are rejected, keeping only those MVUM features which merely cross the route are rejected, keeping only those
that run alongside it. that run alongside it.
""" """
if not coords:
return []
pts = [(lon, lat) for (lat, lon) in coords] pts = [(lon, lat) for (lat, lon) in coords]
geom = LineString(pts) if len(pts) >= 2 else Point(pts[0]) geom = LineString(pts) if len(pts) >= 2 else Point(pts[0])
return self._query_geom(geom.buffer(tolerance_m * _DEG_PER_M)) avg_lat = sum(lat for (lat, lon) in coords) / len(coords)
return self._query_geom(geom.buffer(_buffer_degrees_for_meters(tolerance_m, avg_lat)))
def parse_date_range(date_str: str) -> List[Tuple[int, int, int, int]]: def parse_date_range(date_str: str) -> List[Tuple[int, int, int, int]]: