mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-08-26 17:31:37 +00:00
Adds OSM parking lots as a third multi-modal-Auto transition source alongside
MVUM trailheads (3a) and surface-change points (3c), so Auto can suggest
"drive to a parking lot, switch to foot/2w/4w" trips where no MVUM trailhead
exists -- BLM/state land, urban edges, anywhere OSM has parking but the USFS
trailhead layer does not. Backend-only; consumes the already-ingested
/mnt/nav/osm-parking.db read-only (no data-pipeline change).
- mvum_parking.py: OSMParkingIndex (process-wide singleton via load_parking_index)
over a shapely STRtree of parking points, mirroring MVUMSpatialIndex /
TrailheadIndex. Read-only SQLite. Drops access in (private,no,permit) at load.
query_parking_near_line(coords, buffer_m=2000) with the same coarse-bbox +
precise-distance filter as TrailheadIndex. Records carry
{lat, lon, name, road_class="parking", parking_type, access}.
Perf note: the ingest already stored representative_point() in lat/lon, so the
STRtree is built straight from those columns -- parsing the 1.6M WKB blobs at
boot would add minutes for an identical point.
- router.py: _try_hybrid_auto generalized to gather candidates from each AVAILABLE
source (trailhead index if present + surface-change always + parking index if
present) instead of hard-returning when trailhead_index is None, so parking-only
candidates still work. Combined list keeps the existing closest-first sort +
HYBRID_MAX_TRAILHEADS cap. Signature unchanged; record shape already compatible.
- app.py / offroute_route.py: load + inject the OSM parking singleton, mirroring
MVUM_SPATIAL_INDEX / MVUM_TRAILHEAD_INDEX. Failure logs a warning, degrades None.
- admin.py: GET /api/admin/osm-parking/info -> {count, build_time_seconds,
memory_estimate_mb}, mirroring /api/admin/mvum-spatial/info.
- backend/scripts/ingest_parking.py + README-osm-parking-ingest.md: the
data-pipeline ingest lifted to the repo with argparse (--geojsonseq/--db, no
/tmp) + the download/filter/export/ingest/restart refresh recipe.
Tests: test_mvum_parking.py (loads, near-line close-only, private/no/permit
filtered, null-access kept) + test_offroute.py::test_hybrid_consumes_parking_
candidates (parking-only source probed as a leg-1 destination). Full offroute
suite: 82 passed.
Real-DB sanity (not deployed): index loads 1,489,054 usable parking objects
(182,945 access-blocked dropped) in ~12 s using ~950 MB RSS per worker; a Redfish
Lake/Sawtooth corridor query returns 8 lots. The ~950 MB/worker memory cost is
notable -- flagging for review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Ingest OSM amenity=parking into the OSM parking SQLite DB consumed by
|
|
navi-offroute's Layer 3b (mvum_parking.OSMParkingIndex).
|
|
|
|
Pipeline (see README-osm-parking-ingest.md):
|
|
geofabrik <region>-latest.osm.pbf
|
|
-> osmium tags-filter nwr/amenity=parking -> <region>-parking.osm.pbf
|
|
-> osmium export -f geojsonseq -a type,id -> <region>-parking.geojsonseq
|
|
-> THIS SCRIPT -> osm-parking.db
|
|
|
|
Reads the GeoJSONSeq stream (one RFC-8142 record per line, 0x1e-prefixed),
|
|
keeps only features tagged amenity=parking, and writes one row per parking
|
|
object: a Point for nodes, a (Multi)Polygon for closed-area ways/relations.
|
|
|
|
osmium export emits each closed area-way TWICE -- once as the raw LineString and
|
|
once as the assembled (Multi)Polygon -- so we drop Line geometries to keep a
|
|
single geometry per object (rare genuinely-open parking ways, which are data
|
|
errors, are dropped). lat/lon store an interior representative_point used by the
|
|
index to build its STRtree without re-parsing the WKB.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import sqlite3
|
|
|
|
from shapely.geometry import shape
|
|
from shapely import to_wkb
|
|
|
|
DEFAULT_DB = "/mnt/nav/osm-parking.db"
|
|
|
|
|
|
def parse_capacity(v):
|
|
"""Leading-integer parse of an OSM capacity value (e.g. '120', '12;disabled'); None if non-numeric."""
|
|
if v is None:
|
|
return None
|
|
digits = ""
|
|
for ch in str(v):
|
|
if ch.isdigit():
|
|
digits += ch
|
|
else:
|
|
break
|
|
return int(digits) if digits else None
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Ingest amenity=parking GeoJSONSeq into osm-parking.db")
|
|
ap.add_argument("--geojsonseq", required=True,
|
|
help="input GeoJSONSeq from `osmium export -f geojsonseq -a type,id`")
|
|
ap.add_argument("--db", default=DEFAULT_DB,
|
|
help=f"output SQLite DB (default {DEFAULT_DB})")
|
|
args = ap.parse_args()
|
|
|
|
con = sqlite3.connect(args.db)
|
|
con.execute("PRAGMA journal_mode=OFF")
|
|
con.execute("PRAGMA synchronous=OFF")
|
|
con.execute("DROP TABLE IF EXISTS parking")
|
|
con.execute("""CREATE TABLE parking (
|
|
id INTEGER PRIMARY KEY,
|
|
osm_id TEXT, osm_type TEXT, name TEXT,
|
|
capacity INTEGER, access TEXT, parking_type TEXT,
|
|
lat REAL, lon REAL, shape BLOB)""")
|
|
|
|
INS = ("INSERT INTO parking (osm_id,osm_type,name,capacity,access,parking_type,lat,lon,shape)"
|
|
" VALUES (?,?,?,?,?,?,?,?,?)")
|
|
|
|
batch, n, skipped, bad = [], 0, 0, 0
|
|
with open(args.geojsonseq, "rb") as f:
|
|
for raw in f:
|
|
raw = raw.strip().lstrip(b"\x1e").strip()
|
|
if not raw:
|
|
continue
|
|
try:
|
|
feat = json.loads(raw)
|
|
except Exception:
|
|
bad += 1; continue
|
|
props = feat.get("properties") or {}
|
|
if props.get("amenity") != "parking":
|
|
skipped += 1; continue
|
|
gj = feat.get("geometry")
|
|
if not gj:
|
|
skipped += 1; continue
|
|
try:
|
|
geom = shape(gj)
|
|
if geom.is_empty:
|
|
bad += 1; continue
|
|
# Drop the duplicate LineString osmium emits for each closed area-way.
|
|
if geom.geom_type in ("LineString", "MultiLineString"):
|
|
skipped += 1; continue
|
|
rep = geom.representative_point()
|
|
wkb = to_wkb(geom, output_dimension=2, byte_order=1)
|
|
except Exception:
|
|
bad += 1; continue
|
|
batch.append((str(props.get("@id")), props.get("@type"), props.get("name"),
|
|
parse_capacity(props.get("capacity")), props.get("access"),
|
|
props.get("parking"), rep.y, rep.x, wkb))
|
|
n += 1
|
|
if len(batch) >= 5000:
|
|
con.executemany(INS, batch); batch = []
|
|
if batch:
|
|
con.executemany(INS, batch)
|
|
con.execute("CREATE INDEX idx_parking_latlon ON parking(lat, lon)")
|
|
con.commit()
|
|
print(f"inserted={n} skipped_non_parking_or_line={skipped} bad_geom={bad}")
|
|
con.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|