navi/backend/scripts/README-mvum-ingest.md
Matt 26a84e3705 Commit reproducible MVUM ingest script (P4)
Operational scripts only (no service code, no tests).

- backend/scripts/mvum_backfill.py: cleaned, repo-ready version of the P2/P3 NFS-centerline
  backfill. argparse --db-path (default /mnt/nav/navi.db) / --nfs-gdb (default the EDW
  Trans_Trail_NFS_Publish.gdb) / --dry-run; no /tmp, no test-DB paths, no prod-refusing
  guard (intended for prod, gated by --dry-run + the README snapshot step). Extracts NFS
  geometry from the .gdb via ogr2ogr into a cache beside it (no GDAL Python bindings here),
  matches null mvum_trails rows by TRAIL_NO+TRAIL_NAME within the forest ADMIN_ORG prefix,
  merges segments, writes WKB. Idempotent (UPDATE WHERE shape IS NULL). Prints
  rows_attempted/rows_updated/rows_skipped_no_match/rows_skipped_nfs_null_geom.
- backend/scripts/README-mvum-ingest.md: source URLs, initial ingest (ogr2ogr shapes),
  refresh, repair (the NULL-shape gap), snapshot-first + stop-service guardrails, and the
  produce-vs-consume pointer to mvum.py / mvum_annotate.py / mvum_exclude.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 02:01:55 +00:00

4.1 KiB
Raw Blame History

MVUM ingest pipeline

How the mvum_roads / mvum_trails tables in navi.db are produced from USFS data. The navi-offroute service code only consumes these tables — see backend/services/navi_offroute/mvum.py (access logic + MVUMSpatialIndex), mvum_annotate.py (Layer 1 per-edge annotation), and mvum_exclude.py (Layer 2c exclude_polygons). The scripts here are the producers.

Source datasets (USFS EDW)

Published as File Geodatabases (national rollups):

Keep the unzipped .gdbs under /mnt/nav/sources/mvum/. Both MVUM and NFS data are NAD83 / EPSG:4269, and ogr2ogr is loaded with SPATIALITE=NO so geometry is stored as plain WKB blobs that shapely and MVUMSpatialIndex read directly.

Always, before any write to navi.db: snapshot to a named, dated, persistent file (never /tmp) and stop the service:

sudo systemctl stop navi-offroute
sqlite3 /mnt/nav/navi.db ".backup /mnt/nav/navi-pre-<change>-<YYYY-MM-DD>.db"
#   ... run the ingest/backfill ...
sudo systemctl start navi-offroute

Rollback = restore the snapshot over navi.db and restart.

1. Initial ingest (one-time, building navi.db from scratch)

Load each MVUM GDB layer into its table. ogr2ogr shape (run from /mnt/nav/sources/mvum/):

# roads -> mvum_roads
ogr2ogr -f SQLite /mnt/nav/navi.db Trans_MVUM_Road.gdb  Trans_MVUM_Road \
        -dsco SPATIALITE=NO -nln mvum_roads  -update -overwrite

# trails -> mvum_trails
ogr2ogr -f SQLite /mnt/nav/navi.db Trans_MVUM_Trail.gdb Trans_MVUM_Trail \
        -dsco SPATIALITE=NO -nln mvum_trails -update -overwrite

-update appends to the existing navi.db (it holds other navi tables); -overwrite replaces just the named table. This yields ogc_fid, a shape WKB blob, and the MVUM attribute columns (id, name, forestname, symbol, atv, highclearancevehicle, e_bike_class1, the *_datesopen fields, etc.). Then run the trail backfill (step 3) to fill the geometry-sparse trail rows.

2. Refresh (USFS published new MVUM data)

cd /mnt/nav/sources/mvum
for f in Trans_MVUM_Road Trans_MVUM_Trail Trans_Trail_NFS_Publish; do
  curl -L --fail -o "$f.gdb.zip" \
    "https://data.fs.usda.gov/geodata/edw/edw_resources/fc/$f.gdb.zip"
  rm -rf "$f.gdb" && unzip -o "$f.gdb.zip"
done
# (snapshot + stop service as above)
ogr2ogr -f SQLite /mnt/nav/navi.db Trans_MVUM_Road.gdb  Trans_MVUM_Road  -dsco SPATIALITE=NO -nln mvum_roads  -update -overwrite
ogr2ogr -f SQLite /mnt/nav/navi.db Trans_MVUM_Trail.gdb Trans_MVUM_Trail -dsco SPATIALITE=NO -nln mvum_trails -update -overwrite
python backend/scripts/mvum_backfill.py --db-path /mnt/nav/navi.db   # then restart service

3. Repair (mvum_trails has NULL shapes — the gap fixed in P1P3)

When only the trail geometry is missing (no full re-ingest needed), just run the NFS backfill. Preview first, then apply:

# preview (no writes)
python backend/scripts/mvum_backfill.py --db-path /mnt/nav/navi.db --dry-run

# apply (after snapshot + stopping the service)
python backend/scripts/mvum_backfill.py --db-path /mnt/nav/navi.db

mvum_backfill.py extracts the NFS centerlines from --nfs-gdb (default /mnt/nav/sources/mvum/Trans_Trail_NFS_Publish.gdb) into a _nfs_centerline_extract.sqlite cache beside the .gdb, matches each null mvum_trails row by TRAIL_NO+TRAIL_NAME within the forest's ADMIN_ORG prefix, merges the matched segments, and writes WKB. It is idempotent (UPDATE ... WHERE shape IS NULL) and prints counters (rows_attempted, rows_updated, rows_skipped_no_match, rows_skipped_nfs_null_geom). Typical full-population recovery is ~97%; the residual is rows with no join key or trails absent from NFS / null in NFS too.

After any write: sudo systemctl start navi-offroute (each worker rebuilds MVUMSpatialIndex at boot, ~8 s).