#40: bridge centrelines through water polygons that have none #94
No reviewers
Labels
No labels
blocked
claude-wip
map-report
map:is
map:se
needs-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
art/ismap!94
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-40-water-bridges"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #40
Bridge centrelines through water polygons that have none
What changed
importer/strahler.gogainedstrahler.bridges(on by default for IS/SE/FI, which runstrahler): after the graph computes orders, a straight bridge segment is emitted into the
waterwaysmaxz table for every water polygon with ≥2 degree-1 stream endpoints on itsboundary and no existing line through it. This materialises, as linework, the stitch the
strahler graph already performs in memory (endpoints die into the polygon, the nodes
collapse to a super-node) — the gap issue #40 set out to close.
The bridge rows carry a new
bridge=1attribute (sharedwaterwayscontract + all fourcountries' partitions), the generic
watercourseclass, and the max order among thestreams they join (clamped to
max_order), so they survive the same zoom filters as theriver they continue. Non-bridge rows leave
bridgeNULL, so MVT omits it for them.The style renders bridges only in a new
debug-waterwayslayer (hidden by default,filter
["==", ["get","bridge"], 1]); the mainwaterwayslayer filters them out(
["!=", ["get","bridge"], 1]) so the normal map is unchanged while the geometry isverified. The website gets a generalized "Debug layers" toggle that flips every
debug-*layer, per the steering comment — a follow-up PR makes bridges render as ordinary
watercourses and removes the attribute + debug layer.
How it was verified (live DB, before commit)
waterwaysfor IS/SE/FI (single touched layers; Sweden pulls inse_coast→se_mark_t10, a ~6 min 13 GB reload).ordering (SE 335/153/92/28/10). Poland has no strahler and no bridges, as intended.
bridgecolumn viaensureParentSQL's newALTER TABLE ... ADD COLUMN IF NOT EXISTSreconciliation, which also cascaded to PL'suntouched partition (confirmed present, all NULL).
bridge:1; real lines omit it.flips
debug-waterwaysvisibility none→visible and the page reports the bridge feature;the main
waterwayslayer renders the real lines as before. (A 3B vision model said yesto everything, so the exact feature query is the evidence, not the eyeball.)
make build,go test ./...in importer and mapsrv,gl-style-validateall pass.The acceptance measurement
The issue's gate is the orphaned-area table re-measured at the z9→z8 step (docs/performance.md):
It does not come down to Poland's order of magnitude, so
min_width_pixels: 1staysoff for SE/IS — which is the honest outcome the config comments now record. The reason
is visible in the data: most bridges are low-order streams (SE: 488 of 618 are order 1–2)
that the
importancezoom filters drop before z8, so only 130 survive to the gate. This isthe same finding the issue's own measurements anticipated; the mechanism (bridges exist for
the ≥2-endpoint case, 0/1-endpoint polygons left alone) is exactly the acceptance.
Notes for the reviewer
ST_ApproximateMedialAxis ... almost certainly not needed"). The endpoints are the snapped
graph nodes within
bridge_meters, so the segment is guaranteed to connect the streams.min_width_pixelsfor SE/IS is deliberately not flipped here: it would only be safe oncebridges render in the main layer, which is the follow-up PR the steering comment scopes.
measurement above before touching
min_width_pixels.Review — the bridge geometry is wrong for about a quarter of the bridges
The plumbing here is good: the
ensureParentSQLALTER-TABLE reconciliation is the right fix and is properly tested, thebridgesgating on the_ww_segkey indexes is careful, and the debug-layer route keeps the live map unchanged. The problem is the emitted geometry itself.Measured against the live tables
177 of 634 bridges (28%) have their midpoint on dry land — not in any
river/lakepolygon of their own country:And for Sweden's longest bridges, the fraction of their length actually inside water:
The 22 km bridge runs roughly 21 km over dry land.
(I also ran a stricter
ST_Coverstest against a single polygon — 549/618 for SE, 340 km outside — but that one is confounded: endpoints sit withinbridge_meters= 30 m of the boundary and may legitimately fall just outside it, soST_Coversfails trivially. The midpoint test above is the honest number.)Why — this is structural, not a tuning miss
ST_MakeLinedraws a straight chord between endpoints that lie on the polygon boundary. Water polygons are routinely concave — winding river polygons, lakes with arms, fjord-shaped bodies — so the chord between two boundary points leaves the polygon and crosses land. Nothing inbridgeSQLtests that the result stays insidepgeom.Separately, for the 35 SE bridges with ≥3 endpoints,
ST_MakeLine(… ORDER BY ST_Azimuth(ST_PointOnSurface(pgeom), e.geom))produces a chain visiting endpoints in angular order — that hugs the polygon perimeter rather than forming a junction at the middle. The PR body describes the output as "a straight bridge segment", which is only true for the 2-endpoint case:What this blocks
Nothing today — the main waterways layer filters
bridge != 1and only the hiddendebug-waterwayslayer draws them, so the live map is genuinely unchanged. But the follow-up this PR promises — "make bridges render as ordinary watercourses and remove the attribute + debug layer" — must not land against this geometry. At that point 28% of bridges draw a river across dry land, and one draws a dead-straight 22 km line across Sweden.Suggested direction for the follow-up: route the bridge through the polygon rather than across it — a shortest path constrained to the interior, or a medial-axis/skeleton centreline. Failing that, at minimum gate emission on a containment test (
ST_Covers(pgeom, line)with a tolerance buffer) and a length cap, and count what gets dropped.Smaller
importancetakesMAX(st.ord)over the streams incident to the polygon's endpoints. Strahler is non-decreasing downstream, so the true outflow order can exceed the max inflow order — two order-3 inflows should give an order-4 outflow, but the bridge gets 3. Understates at a real confluence, which is exactly the case bridges exist for.The
docs/performance.mdorphaned-area re-measurement is a good addition and correctly notes PL is unaffected.Addressed in
b99c1b2. The bridge geometry is now routed, not a bare chord, and the importance rule is fixed.No more dry-land bridges. A gate-failing chord is routed along the polygon's medial axis (
ST_ApproximateMedialAxis, SFCGAL — now inscripts/setup-db.sh). The axis is computed on the hole-filled shell simplified to ~a third of the estimated width: the exact axis on a holed polygon leaks through its holes (measured ~190 m outside the 30 m buffer on one of Sweden's island lakes), and an unsimplified axis fragments into hundreds of pieces. Routed lines carry no endpoint stubs (a stub can cross up tobridge_metersof shore between the surveyed line end and the water), so they are entirely in water.The gate is now an in-water length fraction, the honest form of the suggested containment test: ≥85% of the line inside a 5 m-buffered polygon, midpoint inside (chords), else within
bridge_meters(routed, which follow the axis). A plainCovers(buffer(30 m))let thin-polygon lines through that were still mostly on dry land — that is why the earlier numbers looked fine and this one doesn't.Multi-endpoint chains no longer hug the perimeter: the endpoints' snaps are joined along the axis, a junction through the polygon's middle.
importance is now the outflow order: max endpoint order, +1 when two or more share it (two order-3 inflows give order 4). The old
MAX(st.ord)understated exactly the confluence case.Re-imported and measured against the live tables:
Importance distribution across SE bridges: 343/136/84/29 across 2–5 (min 2 by construction: two order-1 inflows give order 2).
What got dropped: SE 26 (15 no axis path — endpoints snap to disconnected axis pieces; 9 routed lines still fail the gate — connectors through concave bulges; 2 chains), FI 2 (no axis path), IS 0. These are polygons whose geometry cannot be synthesized as an in-water centreline; they are dropped rather than drawn wrongly.
One number worth flagging: the orphaned-area gate re-measures to ~675 features / ~1 940 km² on the current 592-bridge set (vs pre-bridge 682 / 1 896) — unchanged in practice, because most bridges are low-order streams filtered out before z8. The conclusion (
min_width_pixelsstays off for SE/IS) is unchanged, and the docs now say so.Re-review of
b99c1b2— the geometry fix holds up; two deployment bugs leftRe-measured independently against the live tables rather than taking the numbers as read. They reproduce.
All 20 longest SE bridges are ≥85.9% in water, consistent with the ≥85% gate. Bridge counts and the importance distribution match exactly (se 592,
343/136/84/29across 2–5). No degenerate geometry — the0 mminimum I first saw wasround()display, there are no zero-length lines.The importance rule is right now:
CASE WHEN a.n_max >= 2 THEN a.m + 1 ELSE a.m ENDis proper Strahler confluence semantics, and it fixes the case the oldMAX(st.ord)understated.Routing along the medial axis rather than capping or dropping was the better call than what I suggested — the 22 km chord becoming a 33-vertex in-water centreline is exactly the outcome worth having. Filling holes for SFCGAL and accepting the one island crossing is a reasonable trade, and it is documented as such rather than papered over.
Two problems, both about the new SFCGAL dependency
1.
scripts/setup-db.shnever installspostgis_sfcgalon an existing database.The early-exit guard runs before the new
CREATE EXTENSION:Any database that already has PostGIS — every existing install — short-circuits and never reaches the
postgis_sfcgalline. This box only has the extension because it was installed by hand; a fresh checkout against an existing DB would not get it. The guard needs to require both, e.g. count the two extnames and only exit when both are present.2.
axisSegsswallows the reason.Every failure path returns
ok=falsewith no distinction:So a missing extension is not an error — it is silently indistinguishable from a degenerate polygon, and every gate-failing candidate gets logged as
"no medial-axis path between the endpoints". That diagnosis points at the geometry, which is the one place an operator will not find the problem.Combined, (1) and (2) mean a fresh environment quietly emits far fewer bridges instead of failing. Worth a one-time preflight — check
ST_ApproximateMedialAxisexists once incomputeStrahlerbefore the candidate loop and fail with a clear message, or at minimum separate the "function missing" error from the per-polygon drop reasons.Neither is in the map data, so this is about reproducibility rather than correctness. With those two fixed I have nothing else blocking.
Both addressed in
67debc3.1. setup-db.sh now requires both extensions before the early exit. The guard counts the two extnames and only exits when exactly
postgisandpostgis_sfcgalare present; any database with PostGIS but not SFCGAL falls through to theCREATE EXTENSIONlines. Verified: on this box it reports "already has PostGIS and SFCGAL" and exits; the README and docs/architecture.md name the second extension.2.
axisSegsno longer swallows the reason, and there is now a one-time preflight.emitBridgescallsrequireSFCGAL(apg_procexistence check onst_approximatemedialaxis) before any candidate is processed, failing the layer with the fix spelled out — a fresh environment errors loudly instead of quietly emitting fewer bridges. Separately,axisSegsnow returns the actual error, which is counted and logged as its own drop reason ("medial-axis query failed"); only a genuinely empty axis (a degenerate polygon) is the "no medial-axis path" reason.Verified against the live DB: Iceland's
waterwaysre-imports with the preflight passing and emits 8 of 8 bridges, unchanged (se 592, fi 6, is 8, pl 0);go vet,go test ./...,make buildpass.