makepad/bridge.md
andodeki d6d1f99ca9 Update fork to upstream dev 5d4483f
- Sync with latest upstream dev branch
- Include all map improvements: 2D/3D toggle, shadows, labels, overlays
- Include platform updates: location API, audio echo cancellation
- Preserve fork-specific re-exports (gltf, csg, test)
2026-07-31 18:47:03 +00:00

161 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# bridge.md — road elevation & grade separation for the 3D car-follow view
How we get overpasses, bridges, tunnels and stacked interchanges to render with
real depth in the perspective/ortho car-follow GPS view. Companion to `map.md`
(renderer), `gps.md` (interaction), `datasources.md` §2 (elevation sources; this
doc expands its "Road elevation / grade separation" subsection). Researched and
endpoint-checked 2026-07-29.
## 1. The data reality: OSM will not store road z
- **OSM has no plans to add elevation to road geometry — it's a settled
position, not an oversight.** The data model is strictly 2D lat/lon. `ele=*`
exists but is for summits/POIs; the wiki is explicit that OSM is not an
elevation database. No active proposal changes this.
- **Overture landed in the same place.** Their transportation `level` property
is relative stacking order only, documented as "an approximation for
rendering, not a precise indication of elevation". Geometry is 2D.
- The freshest ecosystem effort (OSM2World's Prototype-Fund project, June 2026 —
see §3) plans *around* the missing z, not toward adding it. At most it may
propose new helper *tags*.
Conclusion: waiting for upstream z is not a strategy. Everyone derives it;
so will we — at tile/nav build time, baked into our own tiles.
## 2. What road geometry does carry (all already in our PBF)
| Tag | Meaning | Use for us |
|---|---|---|
| `layer=*` | relative stacking, 5..5, per way | the load-bearing input: crossing order |
| `bridge=*` / `tunnel=*` | way is elevated / buried; ways are split at the structure ends | where decks start/end; abutment anchors |
| `incline=*` | signed grade on a way (%, or up/down) | constraint on ramps where mapped |
| `maxheight=*` | legal clearance *under* a structure | lower bound on deck z at that crossing |
| `height=*`, `ele=*` on bridge ways | rare, occasionally present | direct evidence when it exists |
| `bridge:structure`, `man_made=bridge` areas | structure type / deck outline polygon | rendering style; polygon for AHN sampling (§5) |
`layer` is relative and only meaningful *at crossings* — two `layer=1` bridges
in different towns share nothing. Treat it as a partial order between ways whose
geometries intersect, never as an absolute height.
## 3. Prior art
- **OSM2World** is the reference implementation of derive-at-build: an elevation
connector graph is initialized at terrain height, then a constraint pass
enforces equal z at shared nodes, minimum vertical clearance where a higher
`layer` crosses a lower one, tunnels below terrain, and grade
smoothing/clamping (heat-equation smoothing was merged as PR #176).
- **OSM2World "OSM-3D-Terrain"** (Prototype Fund, roadmap 2026-06-03) is
productizing exactly this into 3D tiles with LOD — and explicitly flags that
elevation adjustments must not create discontinuities at tile boundaries.
Same problem class as ours; worth tracking their output formats.
- **MapLibre / the open vector-map world**: nobody renders true grade-separated
interchanges yet. Terrain drape squashes bridges onto the valley floor;
Terrain3D work in MapLibre Native doesn't address elevated roads. Doing this
properly is a visible differentiator for our renderer.
- Commercial nav stacks survey slope/z per link themselves — same conclusion
from the other direction: the community map gives topology and stacking,
measured/derived z comes from elsewhere.
## 4. The derivation recipe (tile/nav build, Europe-wide)
Runs offline in `tools/map_tiles` / `nav-build`; the renderer only ever sees
baked results.
1. **Init**: every road/rail vertex gets z = DEM height at (x,y)
(Mapterhorn PMTiles / AHN, per `datasources.md` §2).
2. **Never sample the DEM mid-bridge or mid-tunnel.** Bridge/tunnel ways are
split at the structure boundary, so the endpoints are the abutments:
interpolate z between abutments (linear; spline if the deck is long).
Sampling under the deck is the classic artifact — the bridge sagging into
the river valley — and it equally poisons the per-edge climb/descent numbers
baked into `region.graph` for EV routing (datasources.md, EV thread).
3. **Constraint pass** over crossing pairs and shared nodes:
- shared node ⇒ equal z (ramps connect continuously);
- `layer(a) > layer(b)` at a geometric crossing ⇒
`z_a ≥ z_b + clearance`; default clearance ≈ 5.5 m per layer step
(≥ 4.7 m legal clearance + ~0.8 m deck structure), overridden upward by
`maxheight` where tagged;
- `tunnel` ⇒ z ≤ terrain cover (default ~2 m below surface at portals,
deeper mid-tunnel is fine);
- respect `incline=*` where mapped.
4. **Smooth + clamp**: grade smoothing along each way chain (moving
average or heat-equation style), then clamp per class — motorway ≤ ~6%,
links ≤ ~10%, service/other ≤ ~15%. Approach ramps to a lifted deck extend
into the adjacent non-bridge ways; ease in/out (cosine) so decks don't pop.
5. **Tile-boundary continuity**: solve on a buffered neighborhood (same trick
as our existing tile pipeline uses for geometry clipping) so a vertex near
the edge gets the same z in both tiles.
6. **Bake**: per-vertex **Δz above DEM** (not absolute z), quantized (dm is
plenty), on road/rail geometry in our tiles. Δz-above-DEM keeps the field
independent of DEM version/resolution mismatches at render time and is 0 for
~99% of vertices ⇒ compresses to almost nothing.
## 5. NL upgrade: measured deck heights instead of solved ones
Where surveyed data exists, replace the constraint solution:
- **RWS DTB** (Digitaal Topografisch Bestand, open on PDOK): cm-grade surveyed
x/y/z on lines/points/areas covering exactly the RWS-managed motorway network
— i.e. precisely where the multi-level interchanges are. Z is in the PDOK
atom shapefiles and (since recently) the WFS as well. Join to OSM ways by
proximity+bearing, sample z along the DTB lines.
- **AHN** (open LiDAR): the **DSM keeps bridge decks, the DTM removes them**
so DSM DTM over a bridge polygon (`man_made=bridge` from OSM, or BGT
"overbruggingsdeel") ≈ deck height directly. The LAZ point clouds classify
structures explicitly if we want it from the source.
- **Kadaster 3D Basisvoorziening** (open, 3dfier-generated): ready-made 3D
terrain + LoD1 bridge decks as meshes — the ingest-meshes-instead-of-solving
option, and a validation set for our solver either way.
Europe-wide the solver result stands; NL gets true-to-survey interchanges.
Validation loop: run the solver in NL, diff against DTB/AHN, tune clearance and
clamp constants until the residuals are boring, then trust it elsewhere.
## 6. Renderer integration
- Tiles carry Δz per road vertex; the renderer adds it on top of the same
terrain drape it already uses (see map terrain/3D displacement architecture
notes). Decks lift, tunnels sink, everything else stays glued to the drape.
- 2D/top-down mode keeps using `layer` draw order exactly as today — Δz only
engages in tilted/3D camera modes, so this is purely additive.
- Depth: elevated decks get real depth-tested geometry; the existing
bridge-casing styling (open item in the carto-quality list) becomes the deck
side/edge treatment in 3D.
- What sells the effect: a drop shadow / AO blob under decks onto whatever is
below, and portal treatment at tunnel mouths (clip or fade the tunnel line
under terrain, keep a dimmed dashed hint in nav mode when the route goes
through it).
- Car-follow camera: route position includes Δz, so the chase camera rides over
the flyover instead of clipping through it; on stacked interchanges the
route's own level is unambiguous because it comes from the graph edge, not
from GPS altitude (which is too noisy to pick a deck).
## 7. Build order
- **M0 — fake it visually**: no DEM needed. Δz = `layer` × 5.5 m on
bridge/tunnel ways with cosine ease-in/out extending into approach ways.
Gets stacked interchanges reading correctly in one step; wrong absolute
heights, right relative ones.
- **M1 — solve it**: recipe of §4 in the tile build, bake Δz-above-DEM,
renderer consumes it. Requires the DEM ingest that hillshade (path B) needs
anyway.
- **M2 — measure it (NL)**: DTB/AHN override per §5 + solver validation.
- **M3 — polish**: deck shadows/AO, tunnel portals, casing-as-deck-edge,
`region.graph` climb/descent switched to the same z source.
## Sources (checked 2026-07-29)
- OSM wiki: [Altitude](https://wiki.openstreetmap.org/wiki/Altitude),
[Key:ele](https://wiki.openstreetmap.org/wiki/Key:ele),
[Key:layer](https://wiki.openstreetmap.org/wiki/Key:layer),
[Key:maxheight](https://wiki.openstreetmap.org/wiki/Key:maxheight)
- OSM2World: [OSM-3D-Terrain Prototype-Fund roadmap (2026-06-03)](https://osm2world.org/blog/2026/06/03/ptf-roadmap-2026-osm-3d-terrain/),
[elevation smoothing PR #176](https://github.com/tordanik/OSM2World/pull/176)
- Overture: [shape & connectivity](https://docs.overturemaps.org/schema/concepts/by-theme/transportation/shape-connectivity/),
[segments & connectors](https://docs.overturemaps.org/guides/transportation/segments-and-connectors/)
- NL: [RWS DTB on PDOK](https://www.pdok.nl/introductie/-/article/digitaal-topografisch-bestand-dtb-),
[DTB open data (RWS)](https://rijkswaterstaat.nl/zakelijk/open-data/digitaal-topografisch-bestand),
[AHN DTM](https://data.overheid.nl/dataset/47567-actueel-hoogtebestand-nederland--ahn--dtm),
[AHN DSM (AHN4)](https://data.overheid.nl/dataset/36461-actueel-hoogtebestand-nederland-dsm--ahn4-),
[Kadaster 3D Basisvoorziening](https://www.kadaster.nl/zakelijk/producten/geo-informatie/3d-producten/3d-basisvoorziening)
- Renderer gap: [MapLibre Terrain3D roadmap](https://maplibre.org/roadmap/maplibre-native/terrain3d/)