Skip to content

feat(live): ownship 6-min COG/SOG projection vector (#141)#148

Merged
DocGerd merged 1 commit into
developfrom
feat/141-ownship-vector
Jul 23, 2026
Merged

feat(live): ownship 6-min COG/SOG projection vector (#141)#148
DocGerd merged 1 commit into
developfrom
feat/141-ownship-vector

Conversation

@DocGerd

@DocGerd DocGerd commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Implements #141: the ownship (GPS) boat marker now renders the same 6-minute COG/SOG projection vector the AIS targets got in #25 — the skipper's own vessel is no longer the only one on the Live map without one.

What

  • lib/ownshipVector.ts (new): pure GeoJSON builder for the ownship vector, built on the shared projectionLine from lib/projectionVector.ts — never inlined (design pin). Returns an empty collection (suppress) when the device reports no COG or no SOG, or SOG is below the noise floor.
  • Noise floor: the AIS: live online traffic overlay with ownship recognition #25 AIS vectors draw at sog > 0 — quantized shipborne SOG, where any nonzero value is real motion — which is no usable threshold for raw device GPS that jitters a small nonzero SOG (with a noise course) at rest. Per the issue, picked and documented OWNSHIP_VECTOR_MIN_SOG_KN = 0.5 (inclusive: exactly 0.5 kn renders).
  • PROJECTION_VECTOR_MINUTES = 6 in lib/projectionVector.ts is now the single shared length convention; AIS_VECTOR_MINUTES aliases it — parity by construction, pinned by a test, so the two vectors can never drift apart.
  • BoatMarker: dedicated sc-boat-vector GeoJSON source + sc-boat-vector-line line layer — the AIS vector line-style family (line-width 1.5, line-opacity 0.85) but the existing ownship blue (BOAT_COLOR), so it cannot read as a traffic vector. No minzoom: a single own vector needs no decluttering, and the skipper's own projection matters most. A per-fix unconditional setData both draws and clears.
  • OwnshipMarker forwards fix.sogKn. Since the AIS: live online traffic overlay with ownship recognition #25 refactor this is the single structural marker render site, so the vector applies on the Live view AND the standalone show-my-position toggle (feat(map): standalone ownship marker toggle, decoupled from Live View (#25) #104) with no duplication.
  • GPS-only, zero AIS client/key/network coupling; fully offline. Per-second GPS data keeps the existing useOwnshipGps publish path — no new AppState churn.
  • CHANGELOG [Unreleased] entry. No new UI strings (line layer only).

Tests (TDD, red first)

  • lib/ownshipVector.test.ts: projection geometry with hand-derived great-circle endpoint literals (computed independently in Python from the movable-type destination formula with R = 3440.065 nm, cross-checked against flat-earth approximations — not read off the implementation); every suppression case; the inclusive 0.5 kn boundary; the shared-constant parity pins.
  • components/BoatMarker.test.tsx (new): render-site wiring against a fake map — source/layer setup with the pinned paint literals, draw → suppress → re-draw transitions per fix, teardown. The pre-existing marker/accuracy-circle behavior stays "not unit-tested" per the component's own note.
  • components/OwnshipMarker.test.tsx: sogKn forwarded verbatim (incl. null).

Verification

  • npm --prefix app run typecheck / lint: clean.
  • Focused vitest (ownshipVector, BoatMarker, OwnshipMarker, projectionVector, aisGeoJson): 5 files, 30 passed.
  • Full npm --prefix app run test: green (see PR checks).
  • Real-browser pass (dev server on :5175, dedicated Playwright Chromium, monkeypatched navigator.geolocation): vector renders SE at COG 135 @ 6.0 kn, follows COG to due east at 90, disappears at 0.19 kn while the marker stays, disappears when the device reports no COG (marker falls back to north), and renders identically on the Live tab surface (AIS off, key-less — zero sockets).

Deferred-minor assessment (dual watchPosition, from the #104 review)

Assessed, not consolidated: with Live view and the ownship toggle both active there are two navigator.geolocation.watchPosition subscriptions (LiveView's readout watch + useOwnshipGps). Consolidating needs a refcounted shared watch in services/geolocation.ts plus rewiring both consumers' injectable test seams (~3 source + 3 test files) — a medium-size, lifecycle-sensitive change for negligible benefit (the browser runs one position engine regardless of watcher count). Left as is.

Closes #141

🤖 Generated with Claude Code

https://claude.ai/code/session_01UXvHWVP7iALk2rScg3CtkT

Give the ownship marker the same 6-minute COG/SOG projection vector the
AIS targets got in #25, so the skipper's own vessel is no longer the only
one on the Live map without one.

- lib/ownshipVector.ts: pure GeoJSON builder on the shared projectionLine
  (never inlined); suppresses on missing COG/SOG and below a documented
  0.5 kn noise floor (AIS uses sog > 0 — quantized shipborne data — but
  raw device GPS jitters a nonzero SOG with a noise course at rest).
- lib/projectionVector.ts: PROJECTION_VECTOR_MINUTES = 6 as the single
  shared length convention; aisGeoJson's AIS_VECTOR_MINUTES now aliases
  it so the two vectors can never drift apart.
- BoatMarker: dedicated sc-boat-vector GeoJSON source + line layer in the
  AIS vector line-style family (width 1.5, opacity 0.85) but the ownship
  blue; per-fix setData draws and clears via the empty-collection
  suppression. Applies at the single structural marker render site
  (OwnshipMarker), covering both the Live view and the standalone
  show-my-position toggle (#104).
- OwnshipMarker forwards fix.sogKn; GPS-only, zero AIS coupling.

Tests pin hand-derived great-circle endpoint literals (computed
independently in Python, cross-checked against flat-earth approximations)
plus every suppression case at both the lib and the render-site level.

Closes #141

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXvHWVP7iALk2rScg3CtkT
@DocGerd

DocGerd commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Review: APPROVE (base cb5f7c8)

Verified against issue #141 and the repo's tautology/domain laws. Evidence run at head 43d74d6: npm --prefix app run typecheck + lint clean; focused tests ownshipVector BoatMarker OwnshipMarker aisGeoJson = 27 passed.

Pinned-literal re-derivation (mandatory) — hand-derived all three endpoints independently in Python from the movable-type great-circle destination formula (R = 3440.065 nm), not from the implementation. All matched to 6 dp:

  • ownshipVector.test.ts A: 0.6 nm @45° → 54.807066, 9.512261 ✓ (also the BoatMarker.test.tsx moving case)
  • B: 0.5 nm @90° → 54.789999, 9.444443
  • C (threshold): 0.05 nm @180° @ 0.5 kn → 54.499167, 10.000000

The 0.5 kn floor is pinned inclusive (sogKn >= OWNSHIP_VECTOR_MIN_SOG_KN; the threshold test renders at exactly 0.5, the 0.4/0 tests suppress) — matches the code and the documented issue-#141 intent. PROJECTION_VECTOR_MINUTES = 6 and AIS_VECTOR_MINUTES aliasing it keep the 6-min convention parity-by-construction; the alias value is unchanged (6), so no AIS behavior shift and the aisGeoJson tests still pass.

Confirmed: shared projectionLine reused (zero inlined projection geometry); ownship reuses the existing BOAT_COLOR constant (no new hardcoded color); suppression on no-fix / no-COG / near-zero SOG; zero AIS coupling (imports only projectionVector + types, fully offline); no CPA/TCPA; single structural render site (OwnshipMarker is the only production BoatMarker render site — the vector inherits its Live/planning/no-plan coverage); sogKn is a required number | null prop (no exactOptionalPropertyTypes hazard); explicit vitest imports; CHANGELOG [Unreleased] entry present and well-formed; no i18n strings needed.

Non-blocking minors (waved through — no fix requested)

  1. The new sc-boat-vector line layer is added without a beforeId, so it renders above the whole GL stack (route + AIS), whereas AisLayer inserts COG vectors below the route stack. This is intentional per the inline comment ("the skipper's own projection matters most") and matches the sibling accuracy-fill layer's existing behavior.
  2. Like the pre-existing accuracy circle, the vector source/layer is added in a [map]-keyed mount effect with no whenStyleReady/styledata re-add (unlike AisLayer), so a mid-session style reload would drop it. Pre-existing pattern, unlikely in the ownship context, out of scope here.

No blockers, no majors. Approving.

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.

Live view: ownship 6-minute COG/SOG projection vector (parity with AIS target vectors)

1 participant