- 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)
161 lines
9.6 KiB
Markdown
161 lines
9.6 KiB
Markdown
# 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/)
|