Skip to content

Client-side contour rendering - #13

Draft
bkeepers wants to merge 1 commit into
mainfrom
client-contours
Draft

Client-side contour rendering#13
bkeepers wants to merge 1 commit into
mainfrom
client-contours

Conversation

@bkeepers

@bkeepers bkeepers commented Jul 5, 2026

Copy link
Copy Markdown
Member

What this adds

An opt-in client-side contour mode for the style package. clientContourSource(demSource) wraps a maplibre-contour DemSource into a vector source spec using the standard INT isobath levels (INT_ISOBATHS_M, mirrored from CONTOUR_LEVELS in pipelines/config.py), and style({ tilesBase, clientContours }) switches the contour lines and labels onto it. Soundings, drying areas, and coverage always come from the embedded vector tiles. The demo viewer A/Bs the two modes with ?contours=client.

The isolines come from the openwatersio/maplibre-contour fork, pinned as a git dependency. Stock maplibre-contour only accepts uniform [minor, major] intervals per zoom, which cannot express the INT ladder (2/5/10/20/30...). The fork adds lineLevels (fixed level lists), spot soundings, and isoband polygons. openwatersio/maplibre-contour#1 adds a prepare script and a canvas upgrade so the fork installs as a git dependency on modern node.

Verified live at NY harbor: client mode renders isolines on the depth-band edges with ele properties, soundings are identical in both modes, and no page errors. What you gain over the embedded tiles: contours at any zoom (the Worker's overzoomed DEM feeds them), and level changes without a pipeline rebuild.

Why this is a draft

Client mode works, but it is not chart-grade yet. Three gaps, smallest first.

Jagged lines

Marching squares emits vertices quantized to the DEM grid, so lines stairstep. The DEM itself is already smoothed (the Worker serves the same slope-smoothed merged DEM the embedded contours are traced from), so the fix is post-trace geometry smoothing, the same job the pipeline's Chaikin pass does. Upstream has a draft PR adding smoothing options (onthegomap/maplibre-contour#416) that could be adapted into the fork. Seams need care: maplibre-contour traces in buffered tile space from shared neighbor data, so a deterministic smoother with a slightly larger buffer should produce identical geometry in the overlap. Same trick the pipeline uses: buffer the input, restrict the output.

No true fathom curves

ft/fm labels currently unit-convert the metric levels. The embedded tiles carry a second geometry set at the classic fathom curves (sys=ft); client mode has nothing equivalent, so band edges in ft/fm mode do not land on isolines. The fix needs no fork change: generate a second contour protocol URL with fathom-depth lineLevels from the same DemSource (shared DEM cache, tiles generate on demand), and have applyState swap the source tiles on unit change via VectorTileSource.setTiles.

Shoal safety

Chart generalization must never cut across or delete the shallow side of a line. Plain Chaikin is depth-neutral and can shave a shoal spur; naive speckle removal can delete an isolated shoal entirely. Marching squares knows which side of each line is shallow at trace time, which makes shoal-safe variants tractable: cut corners only into deep water, and drop small rings only when they are deep pockets. Notably this is the same median/ring-drop/safety work that is still deferred on the pipeline side. Building it in the fork would put client mode ahead of the embedded contours, and the logic could migrate back into the pipeline later.

Where the fork-side work should live

Two options for gaps 1 and 3.

Upstream (onthegomap/maplibre-contour). The maintainer is active and friendly to this ecosystem: Mapterhorn support merged recently, and slope/aspect and smoothing PRs are open from other contributors. Spot elevations have appeal beyond bathymetry, since mountain spot heights are the same feature. The friction: dependabot's canvas 3.x bumps were closed unmerged four times (their CI pins node 18/20), and the fork's lineLevels is a second, slower marching-squares implementation sitting next to the native tracer. A maintainer whose README leads with performance will probably want fixed levels reworked into the native algorithm before accepting them. Shoal-safe smoothing is nautical enough that it may never belong upstream at all.

Permanent fork, renamed maplibre-bathymetry. Frees the API to diverge: polarity-aware smoothing, fathom level sets, and chart-specific defaults make no sense upstream. The cost is maintenance. The fork is currently 2 commits behind and cheap to rebase, but that drifts over time.

A middle path is to upstream the generic pieces (spot elevations, native-tracer fixed levels, canvas/CI fixes) and keep the chart-specific generalization in maplibre-bathymetry.

Merging this PR does not require that decision; the pin works either way. The decision shapes where the smoothing and shoal-safety work lands.

layers() and style() gain a client-contours mode: contour lines and
labels can read isolines generated in the browser from the Terrarium
DEM via the openwatersio/maplibre-contour fork, whose lineLevels option
carries the non-uniform INT ladder (stock only does uniform intervals).
Soundings, drying, and coverage stay on the embedded vector source.
ft/fm labels unit-convert the metric levels for now; there is no
fathom-curve geometry client-side yet. clientContourSource() wraps a
consumer-owned DemSource, and INT_ISOBATHS_M is exported (keep in sync
with CONTOUR_LEVELS in pipelines/config.py).

The viewer A/Bs the modes with ?contours=client. The fork is pinned as
a git devDependency; its new prepare script builds dist on install.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@bkeepers bkeepers changed the title Optional client-side contour rendering Client-side contour rendering Jul 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant