feat(map): add seamarks / aids-to-navigation overlay (#7)#105
Conversation
Records the user-approved Option B design (baked seamarks.json + canvas IALA glyphs + opt-in sc-seamarks layer) as a spec addendum before implementation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwEfjZyZiSy2NvrRhJHqUF
Adds a build-time Overpass pull of core aids-to-navigation (buoy_*/ beacon_*/light_* nodes, 1,794 in the app bbox) as a committed static asset, rendered as canvas-drawn simplified IALA-A glyphs on a new opt-in `sc-seamarks` MapLibre layer (default OFF, persisted toggle) mirroring the harbor-marker/depth-overlay pattern. Presentation-only: no routing/solver change. - pipeline/build_seamarks.mjs (new): one Overpass query, client-side core-AtoN filter, writes app/public/data/seamarks.json (~23 KB gzipped). - app/src/lib/seamarkGlyphs.ts (new): pure per-family glyph geometry (lateral/cardinal/safe-water/special-purpose/isolated-danger/lights) -> canvas -> map.addImage(), mirroring windBarbs.ts's convention. - app/src/lib/seamarkGeoJson.ts, seamarkPopover.ts (new): FeatureCollection + icon-id attachment, and the info-popover row builder (type/category/ colour/light character-colour-period). - app/src/components/DataLayers.tsx: sc-seamarks source/layer, toggle checkbox, click -> MapLibre Popup (own popup, not a route pick — seamarks aren't route-pickable points like harbors). - App.tsx: sc-seamarks added to the interactive-layer gate so a glyph click never also fires the generic tap-to-pick. - MapView.tsx ATTRIBUTION, AboutDialog.tsx sources list, dict.de/en.ts: ODbL clause, About entry, and every new user-facing string in both languages. - Unit tests: family/id/geometry pinning (seamarkGlyphs.test.ts), popover row logic (seamarkPopover.test.ts), icon attachment (seamarkGeoJson.test.ts). Closes #7 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwEfjZyZiSy2NvrRhJHqUF
MapLibre's default .maplibregl-popup-content hardcodes a white background with no dark variant, leaving the seamark click popover's near-white text (--sc-fg) illegible on first click in dark mode. Give the Popup a 'seamark-popup' className and theme its content/tip chrome with --sc-bg (both tip directions, since the popup may anchor above or below the clicked glyph). Also reverts ~150 lines of unrelated cosmetic reformatting that had crept into usePlanFlow.test.tsx, leaving only the seamarks fixture field addition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # docs/superpowers/specs/2026-07-14-sail-command-design.md
CI e2e plan.spec.ts hit a Playwright strict-mode violation: the seamarks ODbL credit added a second <a>OpenStreetMap</a> anchor with the same href and accessible name as the existing basemap credit. Merge both into one anchor instead of adding a test .first() workaround — two adjacent links with identical accessible names is an a11y anti-pattern regardless of CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-review (author) — findings and resolutionTwo-lens review (offline-PWA + general) plus a CI-caught e2e regression; all resolved:
Offline-PWA review: Approve with built-artifact evidence — Interpretation calls reviewed and accepted: new self-contained Popup (no harbor popover exists to reuse); humanized-not-translated OSM enum values; Merged-tree verification: typecheck/lint clean, focused suites green, |
Summary
Adds a seamarks / aids-to-navigation map overlay per the user-approved spec
addendum (
docs/superpowers/specs/2026-07-14-sail-command-design.md,"Addendum 2026-07-22: Seamarks / aids-to-navigation overlay (#7)").
Presentation-only — no routing/solver change.
pipeline/build_seamarks.mjs, new): one Overpass query forseamark:typenodes in the app bbox, filtered client-side to core AtoN(
buoy_*/beacon_*/light_*). Writes a committedapp/public/data/seamarks.json— 1,794 features, ~23 KB gzipped(spec estimate was ~14 KB; same order of magnitude, not a blocker).
npm run seamarksadded topipeline/package.jsonalongsideharbors/mask/polars/icons.
app/src/lib/seamarkGlyphs.ts, new, parallel towindBarbs.ts): pure per-family glyph geometry (lateral can/cone/pillarred/green, cardinal double-cone topmark oriented by category, safe-water/
special-purpose/isolated-danger by colour band, a ray/star for lights sized
major vs. minor) → canvas →
map.addImage(), the exactbarbSegments()/registerBarbImages()convention.sc-seamarkssymbol layer inDataLayers.tsx,anchored on
ROUTE_STACK_BOTTOM_LAYERlike the harbor/depth layers.Default OFF (
usePersistedToggle('sc-seamarks-visible', false)), wiredexactly like
sc-depth-visible(hidden at creation, synced before firstpaint). Labeled via
map.seamarks.toggle.Popup(built viadocument.createElement/textContent, noinnerHTML) showing type,category, colour, and light character/colour/period when tagged, plus the
app's existing "planning aid, not a navigation device" disclaimer text.
Seamarks are gated into
App.tsx'sINTERACTIVE_MAP_LAYER_IDSso a glyphclick never also fires the generic tap-to-pick.
MapView.tsx'sATTRIBUTION, and anabout.sources.seamarksentry noting the overlay isa point-in-time OSM extract (seamark data as of 22 July 2026), not
continuously verified.
dict.de.tsanddict.en.ts(
map.seamarks.toggle, 6 popover field labels,about.sources.seamarks).Deviations / interpretation calls (flagging per repo convention, not silent)
reusing "the same small info popover pattern as harbor markers", but
clicking a harbor marker actually picks it as origin/destination — no
info popover exists in the codebase to mirror. Implemented a new,
self-contained MapLibre
Popupfor seamarks instead (seamarks aren'troute-pickable points).
(Type/Category/Colour/Light character/colour/period) are full de/en i18n
keys. Field values (raw OSM tag values like
buoy_lateral,no_entry;foul_ground) are humanized (underscores/semicolons → spaces)but not run through the dict — the category tag alone has dozens of
possible values plus semicolon-joined compounds in live data, an unbounded
surface a closed translation table can't cover for a v1 overlay.
(group)qualifier. The spec'sown trimmed-property list is
character/period/colouronly (nogroup), so the popover shows e.g.Ocrather than the illustrativeOc(2)from the spec's own example — reproducing that exactly would needadding
groupto the trimmed fields, which the spec text doesn't list.isolated_dangerglyph family added. The spec's rendering bulletlists lateral/cardinal/safe-water/special-purpose/lights as examples, but
the Scope bullet includes all
buoy_*/beacon_*/light_*nodes —buoy_isolated_danger/beacon_isolated_danger(6 nodes) needed a glyphtoo, so it got one (black/red banding + two-sphere topmark, the standard
IALA-A isolated-danger convention) rather than falling into the
seamark-unknownsafety-net glyph.Test plan
npm --prefix app run typecheck— cleannpm --prefix app run lint— cleannpm --prefix app run test— 66 files / 681 tests passed (~234 s)npm --prefix app run test -- realmask.repro— 6/6 green (realcommitted mask/polars untouched)
?windFixture=test-fixtures/wind-sw12.json): toggle default OFFconfirmed → toggled ON → glyphs render (lateral/cardinal/special-
purpose/isolated-danger/light glyphs all visually distinct) → clicked a
glyph → popover showed
Typ: buoy lateral / Kategorie: port / Farbe: red / Kennung: Oc / Lichtfarbe: red / Wiederkehr: 9 s+ the disclaimer→ close button worked → toggle state survived a de↔en language switch
→ About dialog shows the new Seamarks source line → attribution string
includes the new ODbL clause → 390 px narrow layout: both toggles stack
without overflow. Console: 0 errors (one pre-existing, unrelated
"townhall" sprite warning from the basemap style, not introduced by
this change).
Closes #7
https://claude.ai/code/session_01GwEfjZyZiSy2NvrRhJHqUF