Skip to content

feat(map): add seamarks / aids-to-navigation overlay (#7)#105

Merged
DocGerd merged 5 commits into
developfrom
feat/7-seamarks
Jul 22, 2026
Merged

feat(map): add seamarks / aids-to-navigation overlay (#7)#105
DocGerd merged 5 commits into
developfrom
feat/7-seamarks

Conversation

@DocGerd

@DocGerd DocGerd commented Jul 22, 2026

Copy link
Copy Markdown
Owner

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 (pipeline/build_seamarks.mjs, new): one Overpass query for
    seamark:type nodes in the app bbox, filtered client-side to core AtoN
    (buoy_*/beacon_*/light_*). Writes a committed
    app/public/data/seamarks.json1,794 features, ~23 KB gzipped
    (spec estimate was ~14 KB; same order of magnitude, not a blocker).
    npm run seamarks added to pipeline/package.json alongside
    harbors/mask/polars/icons.
  • Rendering (app/src/lib/seamarkGlyphs.ts, new, parallel to
    windBarbs.ts): pure per-family glyph geometry (lateral can/cone/pillar
    red/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 exact
    barbSegments()/registerBarbImages() convention.
  • Layer + toggle: a sc-seamarks symbol layer in DataLayers.tsx,
    anchored on ROUTE_STACK_BOTTOM_LAYER like the harbor/depth layers.
    Default OFF (usePersistedToggle('sc-seamarks-visible', false)), wired
    exactly like sc-depth-visible (hidden at creation, synced before first
    paint). Labeled via map.seamarks.toggle.
  • Popover: click/tap a glyph opens a small MapLibre Popup (built via
    document.createElement/textContent, no innerHTML) 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's INTERACTIVE_MAP_LAYER_IDS so a glyph
    click never also fires the generic tap-to-pick.
  • Attribution + About: one more ODbL clause in MapView.tsx's
    ATTRIBUTION, and an about.sources.seamarks entry noting the overlay is
    a point-in-time OSM extract (seamark data as of 22 July 2026), not
    continuously verified.
  • i18n: every new string in both dict.de.ts and dict.en.ts
    (map.seamarks.toggle, 6 popover field labels, about.sources.seamarks).

Deviations / interpretation calls (flagging per repo convention, not silent)

  1. Harbor-marker "popover" doesn't exist today. The spec/brief describe
    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 Popup for seamarks instead (seamarks aren't
    route-pickable points).
  2. Popover field values are humanized, not translated. Field labels
    (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.
  3. Light "character" shown without the (group) qualifier. The spec's
    own trimmed-property list is character/period/colour only (no
    group), so the popover shows e.g. Oc rather than the illustrative
    Oc(2) from the spec's own example — reproducing that exactly would need
    adding group to the trimmed fields, which the spec text doesn't list.
  4. isolated_danger glyph family added. The spec's rendering bullet
    lists 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 glyph
    too, so it got one (black/red banding + two-sphere topmark, the standard
    IALA-A isolated-danger convention) rather than falling into the
    seamark-unknown safety-net glyph.

Test plan

  • npm --prefix app run typecheck — clean
  • npm --prefix app run lint — clean
  • npm --prefix app run test — 66 files / 681 tests passed (~234 s)
  • npm --prefix app run test -- realmask.repro — 6/6 green (real
    committed mask/polars untouched)
  • Real-browser pass (Vite dev server + Playwright MCP,
    ?windFixture=test-fixtures/wind-sw12.json): toggle default OFF
    confirmed → 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

Patrick Kuhn and others added 4 commits July 22, 2026 20:34
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>
@DocGerd

DocGerd commented Jul 22, 2026

Copy link
Copy Markdown
Owner Author

Self-review (author) — findings and resolution

Two-lens review (offline-PWA + general) plus a CI-caught e2e regression; all resolved:

  1. Major (general review) — dark-mode popover illegible: the seamark popover set color: var(--sc-fg) inside MapLibre's hardcoded-white .maplibregl-popup-content → near-white text on white in dark mode. Fixed in 7941630: popup gets className: 'seamark-popup'; app.css themes .maplibregl-popup-content background and both tip borders with --sc-bg. Verified in a real browser (dark emulation): bg rgb(13,14,16), text rgb(236,238,241), tip matched; light mode unchanged.
  2. CI e2e (real regression, not flake) — duplicate OSM attribution anchor: the seamarks credit added a second <a>OpenStreetMap</a> (identical href + accessible name) → Playwright strict-mode violation at plan.spec.ts:53. Fixed in e3141b1 production-side: single OSM anchor covering (basemap; seamarks ODbL) — two same-named adjacent links were an a11y defect, so the spec was left untouched. Failing spec re-run locally: passed.
  3. Minor (general review) — diff noise: ~150 lines of pure reformatting in usePlanFlow.test.tsx reverted; diff now shows only the one added seamarks fixture line (also in 7941630).

Offline-PWA review: Approve with built-artifact evidence — data/seamarks.json is auto-precached by the existing wildcard glob (confirmed present in dist/sw.js's manifest, 25 entries), SW route order unchanged (pmtiles-206 first), Open-Meteo never cached, asset fetch resolves via BASE_URL (works at /uat/ too), no runtime Overpass access.

Interpretation calls reviewed and accepted: new self-contained Popup (no harbor popover exists to reuse); humanized-not-translated OSM enum values; group omitted per the spec's trimmed field list; isolated_danger glyph family added (6 real nodes in the data — required by the scope's buoy_*/beacon_*/light_* filter).

Merged-tree verification: typecheck/lint clean, focused suites green, realmask.repro untouched and green. Asset: 1,794 features, ~25 KB gzip.

@DocGerd
DocGerd merged commit 497bd8f into develop Jul 22, 2026
6 checks passed
@DocGerd
DocGerd deleted the feat/7-seamarks branch July 22, 2026 20:15
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.

Chart features: buoys, beacons and other seamarks on the map (and optionally in routing)

1 participant