- 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)
9.6 KiB
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
levelproperty 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
layercrosses 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.
- Init: every road/rail vertex gets z = DEM height at (x,y)
(Mapterhorn PMTiles / AHN, per
datasources.md§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.graphfor EV routing (datasources.md, EV thread). - 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 bymaxheightwhere tagged;tunnel⇒ z ≤ terrain − cover (default ~2 m below surface at portals, deeper mid-tunnel is fine);- respect
incline=*where mapped.
- 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.
- 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.
- 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=bridgefrom 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
layerdraw 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.graphclimb/descent switched to the same z source.
Sources (checked 2026-07-29)
- OSM wiki: Altitude, Key:ele, Key:layer, Key:maxheight
- OSM2World: OSM-3D-Terrain Prototype-Fund roadmap (2026-06-03), elevation smoothing PR #176
- Overture: shape & connectivity, segments & connectors
- NL: RWS DTB on PDOK, DTB open data (RWS), AHN DTM, AHN DSM (AHN4), Kadaster 3D Basisvoorziening
- Renderer gap: MapLibre Terrain3D roadmap