Skip to content

feat(maps): live data layers — setLayerSource, glyph points and satellites in orbit - #44

Merged
apresmoi merged 4 commits into
mainfrom
feat/maps-live-layers
Sep 11, 2026
Merged

apresmoi merged 4 commits into
mainfrom
feat/maps-live-layers

Conversation

@apresmoi

Copy link
Copy Markdown
Owner

A Live card on /maps: earthquakes, disasters, launch sites and satellites, drawn as glyphs in the grid and refreshed on their own clocks.

One new package primitive: map.setLayerSource(id, source). Every layer was static once mounted, so a live feed had nowhere to go. The clock stays on the page, deliberately — a refreshMs option counted in widgetBusy() would make map.idle() never resolve, and excluded from it would be state idle() hides. setLayerSource dispatches through the same trackUpdate counter addLayer uses, so idle() keeps its exact meaning. It rejects the field-source kinds (raster, contour, background, model) with a RangeError naming the id.

A third point layer, type: "glyph". circle and symbol are DOM by contract — textAnchor is a CSS percentage, radiusScale writes style.width — so a mode flag on either would be two disjoint runtimes inside one interface. The new runtime is the line runtime with a point where the polyline was: stamped through the same composed transformCells hook, sharing the stroke's depth test (lifted into one glyphMapSurfaceOccludes rather than copied), cell ownership across detail <pre>s, and the globe horizon. So a live feature is occluded by terrain, survives Copy ASCII and appears in an SVG export. The four live rows use it; every existing symbol/circle consumer is untouched.

Size is a radius in cell rows, rasterised as a disc, each cell's glyph picked by its own coverage — one rule, no threshold. The ramp is · • ●: one shape at three sizes, all centred in the cell, all present in the font atlas and in system monospace stacks.

Satellites orbit at their real altitude. SGP4's altitude is a true-metre quantity like a building's height, so it is exempt from the terrain's exaggeration — at the page default the exempt answer is 8.6% of a radius and the un-exempt one is 207%, two radii past the far side. Limb occlusion needed no new code: the raised-point silhouette test already existed for extrusion walls, so a satellite round the back is correctly hidden while one just past the horizon still shows.

Earthquake labels are rationed by magnitude and recencymag + 1.5·exp(−ageHours/12) fed to the existing declutter arbiter as a priority, so labels thin smoothly as the view zooms out while every quake keeps its mark. The weight is checkable rather than magic: at 1.5 a fresh quake beats one up to 1.5 magnitudes larger from earlier in the week; at 2.0 an M4.1 from two hours ago outranks every M5.5 on the planet. Clicking a quake opens its USGS page — hit-tested against the marks the last render actually drew, inside the existing "did not drag" guard, with the URL's scheme checked before window.open.

A time window per row, which swaps the feed rather than filtering the payload. USGS publishes each window separately, so "last hour" is an 11 KB download, not a filtered 265 KB one. The magnitude floor rides with the window because it is the price of it — all_month is 7.8 MB and 10,988 events — so each button's tooltip names the floor it carries. Launch sites take the same control pointing forwards, where net__lte genuinely shrinks the response. Disasters and satellites get no control: measured on a real capture, not one GDACS event began in the previous seven days, so every window a reader would pick empties the row, and a live position has no recency axis at all.

Cadence follows the window and the publishers' measured limits — Launch Library allows 15 requests an hour and this uses one.

Failure degrades and says why. A failed refresh publishes nothing, so the previous frame stands with its count and a second line explains. Several of these services omit CORS headers on error responses, so a readable 429 reaches the browser as an opaque network failure; an unexplained failure is reported as probably rate limiting rather than silently blank. fetch has no timeout, so there is a 45 s deadline reported as its own failure.

Also fixed along the way: destroy() never disposed feature-kind runtimes, leaking every hotspot and both registry entries; a placement transform persisted across rebuilds so markers created afterwards never received it; and the OSM label-anchor toggle used bubble-phase preventDefault, which React's root delegation runs after the event has already reached the <label>, so clicking a button flipped the row's own switch.

# Conflicts:
#	website/src/components/MapsWorkbench/LayersPanel.contourLabels.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.contourWindow.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.dockRows.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.glyphPalette.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.modelShape.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.osm.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.osmDensity.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.osmLabels.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.osmTooltips.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.renderMode.test.tsx
#	website/src/components/MapsWorkbench/LayersPanel.terrainWindow.test.tsx
#	website/src/components/MapsWorkbench/MapsWorkbench.tsx
#	website/src/components/MapsWorkbench/mapsKit.tsx
#	website/src/components/MapsWorkbench/mapsUrlState.layers.test.ts
#	website/src/components/MapsWorkbench/mapsUrlState.legacyLayerLink.test.ts
#	website/src/components/MapsWorkbench/mapsUrlState.ts
@apresmoi
apresmoi merged commit c420c2c into main Sep 11, 2026
1 check passed
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