makepad/libs/geodata/README.md
Admin 2492ad3bde map: rain radar overlay, flying 3D markers, Europe routing, geodata crate
- rain radar layer: pure-Rust KNMI HDF5 reader (superblock v0 walk,
  single-chunk deflate datasets, byte-exact vs h5py), ellipsoidal polar
  stereographic reprojection (20 m vs product corners), bilinear value
  sampling -> smooth banded isolines, textured quad overlay through the
  overlay camera, 25-frame nowcast animation; RadarSync polls at most
  once per 4 min through a disk-persisted gate and caches frames on disk
- makepad-geodata + makepad-tesla crates join the workspace (overlay
  builders, radar sync, NL open-data layers; transit routes now z7-14)
- Europe major-roads routing graph: nav-build --major-roads does a
  ways-first scan (5.7M ways / 46M nodes / 194 s / 971 MB) and the app
  falls back to it when a route leaves the regional graph — Amsterdam to
  Paris routes offline (501.8 km)
- 3D flying markers: chargers/POIs/stops ride thin stalks with DYNAMIC
  height (each pin clears its own building +8 m); labels, kW text,
  brand and tap zones all consume the baked per-marker lift; stalks and
  buildings grow together on the 2D->3D transition (per-tile flat->3D
  fade heights, no replay on zoom regens)
- markers depth-honest (small bias, buildings occlude them); phong-lit
  canopy/light spheres matching the buildings' NW sun; buildings tint by
  BAG age in 3D; district area tints (rank 60, alpha .32); transit line
  labels + stop names; follow-mode is an explicit attach/detach toggle;
  rotation release schedules the label re-place (no stuck upside-down
  labels after a fast spin)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 18:00:27 +02:00

150 lines
7.7 KiB
Markdown

# makepad-geodata
Bulk open-geodata fetching, per-layer overlay database building, live-source
syncing (rain radar), and a structured query surface — for the map stack.
Netherlands-first. Companion docs: `datasources.md` (source survey, licenses)
and `gps.md` (interaction layer) at the repo root.
The two consumers of every layer database:
1. **The renderer** — standard gzipped MVT vector tiles / PNG raster tiles.
2. **An LLM (via the map app)** — the `features` sidecar table + `query`
module answer "what is at/near this location" with structured JSON, and
raster layers carry class tables so values are nameable.
Design rules, in order:
1. **Bulk downloads only.** Every static source is a single downloadable
file. No API paging, no WFS spidering, no tile scraping. Live sources
(radar) poll their official file APIs at most once per data refresh.
Search-style APIs (Wikipedia, Overpass, NDW live traffic, GTFS-RT) are
*not* this crate's job — the map app queries those by viewport at runtime.
2. **One .mbtiles per layer**, never merged into the base map archive:
`local/overlays/nl-<layer>.mbtiles` — independently rebuildable,
shippable, deletable.
3. **Reuse the existing stack.** SQLite read *and* write via
`makepad-mbtile-reader` (GeoPackages are just SQLite; the reader grew
`open_sqlite` / `schema_entries` / `for_each_row` /
`for_each_row_in_range`, the writer grew extra-table support). Mercator
math from `makepad-map-nav`. New here: RD New <-> WGS84 polynomials, WKB,
MVT encoding, TIFF subset reader, PNG codec, tiling.
4. **Library first.** The `geodata` binary is a thin CLI; the map app embeds
the same machinery (`fetch_source` for periodic re-syncs, `RadarSync` for
live radar, `query::LayerDb` for the LLM/tap-inspect surface).
## Politeness (enforced in `fetch.rs` / `radar.rs`, not left to callers)
- descriptive User-Agent with contact address; one transfer at a time; fixed
pause after every network hit
- cached files are not even revalidated before `recheck_days`; afterwards
If-Modified-Since makes an unchanged file cost one 304
- interrupted downloads resume; per-source `--limit-rate` for small servers
- radar: `min_poll_secs` gate armed *before* the request (a failing API
cannot get hammered), request pacing + one 429 backoff-retry
## CLI
```
geodata list layers + sources + ready/planned
geodata fetch <layer|all> [--force] download / revalidate bulk sources
geodata build <layer|all> build nl-<layer>.mbtiles
geodata status cache + outputs
geodata query <layer> <lon> <lat> [--radius m] [--limit n] features JSON
geodata radar-sync [forecast|reflectivity] poll rain radar
```
Defaults: cache `local/overlays/cache/`, output `local/overlays/`.
## Database contract
Written by `MbtilesWriter` (64 KB pages, deterministic block-major rowids,
passes `PRAGMA integrity_check`). Per file:
- `tiles` — gzipped MVT 2.1 (vector, `format=pbf`) or PNG (raster,
`format=png`).
- `features` — the query sidecar (vector layers): columns `cell, layer,
name, min_lon, min_lat, max_lon, max_lat, attrs (JSON), ring (JSON|null)`.
rowid = `(z12 grid cell << 24) | seq` so bbox queries become b-tree range
scans. Polygon layers opt into `ring` (simplified exterior) for exact
point-in-polygon ("which buurt am I in").
- `metadata` — mbtiles standard keys + `attribution`, `license`, TileJSON
`json.vector_layers` (field names/types per MVT layer), and for rasters
`geodata_encoding` (`terrarium` | `class-index`) + `geodata_classmap`
(JSON class -> label/color) so both shader and query side interpret pixels.
Raster encodings: **terrarium** RGB (elevation, e = h+32768) — read by the
renderer's future hillshade/3D and by map_nav's EV grade baking; **class
index** gray8 (noise dB bands, flood depth bands) — colormapped in the
shader, named via the classmap.
## Layers (all implemented)
| layer | content | source (license) | zooms | output |
|---|---|---|---|---|
| `nature` | Natura 2000 + wetlands polygons, rings in sidecar | PDOK (CC0) | 6-12 | 5.9 MB |
| `chargers` | 66k EV charging locations, operator/power | NDW OCPI (open) | 8-14 | 25 MB |
| `demographics` | CBS 500m+100m grid stats per cell | CBS (CC BY) | 8-13 | 262 MB |
| `wijkbuurt` | gemeente/wijk/buurt polygons + kerncijfers, rings | CBS/Kadaster (CC BY) | 6-13 | 87 MB |
| `transit` | all NL stops + rail/tram/metro/ferry shapes | OVapi GTFS (CC0) | 7-14 | 13 MB |
| `buildings-age` | **all 11.4M BAG buildings** with bouwjaar+status | PDOK bag-light (CC0, 7.8 GB) | 13-14 | 1.5 GB |
| `terrain` | GLO-30 elevation, terrarium | Copernicus (attr) | 6-12 | raster |
| `noise` | RIVM 10m Lden binned to 5 dB classes | RIVM (CC0) | 6-13 | raster |
| `flood` | JRC RP100 river flood depth classes | JRC (CC BY) | 6-11 | raster |
Live source: **rain radar** (`radar.rs`) — KNMI `radar_forecast` (+2h
nowcast, one file per 5 min, the map-app default) and
`radar_reflectivity_composites` (5-min frames, keeps ~1 h). `RadarSync::sync`
is safe to call every frame; it polls at most once per `min_poll_secs` and
returns cached `RadarFrame`s (raw KNMI HDF5 — decode to raster is the next
step). API key: config > `KNMI_API_KEY` env > shared anonymous key (register
a free personal key for real use; the anonymous quota is shared).
## Query surface (the LLM tool-call backend)
```rust
let mut db = query::LayerDb::open(path)?;
db.query_point(lon, lat, limit) // exact point-in-polygon w/ rings
db.query_radius(lon, lat, meters, limit) // nearest-first with distances
db.query_bbox(min_lon, min_lat, max_lon, max_lat, limit)
```
Verified examples: Dam Square resolves gemeente Amsterdam -> wijk
Burgwallen-Nieuwe Zijde -> buurt Nieuwe Kerk e.o. (with populations); nearest
charger 128 m (operator + kW); Royal Palace bouwjaar 1655 out of 11.4M
buildings.
## Module map
```
src/fetch.rs polite bulk downloader (curl), cache + .meta.json
src/radar.rs RadarSync: KNMI radar poll/cache/prune, app-embeddable
src/geo.rs RD New <-> WGS84 polynomials (round-trip tested < 1 m);
writer-order tile key; mercator via makepad-map-nav
src/wkb.rs GeoPackage blob + ISO/EWKB parser
src/gpkg.rs GeoPackage feature iteration (schema-driven, srs transform)
src/mvt.rs MVT 2.1 encoder (extent 4096, key/value dedup)
src/tiler.rs geometry -> clipped tile features (shared); in-memory
Tileset for <= few-million-feature layers
src/spool.rs SpoolTiler: country-scale layers via per-block disk spool
(BAG: 11.4M polygons in one pass, peak memory = one block)
src/sidecar.rs features table builder (grid rowids, JSON attrs, rings)
src/query.rs LayerDb: point/radius/bbox queries over the sidecar
src/tiff.rs GeoTIFF subset: tiled/striped, deflate/LZW/none,
predictors 1/2/3, int/float samples, geo tags, nodata
src/png.rs PNG encode/decode (gray8 + rgb8) with hand-rolled CRC32
src/raster.rs sampler -> 256px PNG tile pyramid (terrarium/class-index)
src/layers/ one module per layer + registry
src/main.rs CLI
```
## Known limits / follow-ups
- Radar HDF5 -> renderable raster decode is not in yet (files are cached and
indexed; decode next). OPERA/MeteoGate is the Europe-wide radar follow-up.
- Flood: JRC `spurious_depth_areas` mask not applied; PDOK ROR official zone
polygons (CC0) planned as vector companion.
- Points are not duplicated into neighbor-tile buffers (edge icons may clip).
- No Douglas-Peucker (dedup + area culling only); BigTIFF unsupported;
`unzip` is shelled out.
- Raster query helper (`sample elevation/class at lon/lat` via PNG decode)
belongs in `query.rs` next, so the LLM can ask "how high / how loud /
flood depth here".