@meri-imperiumi/signalk-dead-reckoning 0.13.4 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/public/dr-app.js +135 -66
- package/public/dr-ext-host-core.js +454 -6
- package/public/dr-ext-host-overlays.js +996 -0
- package/public/dr-ext-host.js +187 -14
- package/public/dr-map-view.js +351 -37
- package/public/dr-sight-panel.js +8 -3
- package/public/dr-viewmodel.js +200 -3
- package/public/index.html +2 -0
- package/public/vendor/plotterext-bus/README.md +10 -6
- package/public/vendor/plotterext-bus/{chunk-RED55KML.js → chunk-UZTRWLMT.js} +29 -2
- package/public/vendor/plotterext-bus/{chunk-4W6N34SD.js → chunk-WV66EF3N.js} +74 -17
- package/public/vendor/plotterext-bus/{chunk-7XRFPDQL.js → chunk-WWNNVR3K.js} +141 -6
- package/public/vendor/plotterext-bus/extension.js +8 -2
- package/public/vendor/plotterext-bus/host.js +11 -3
- package/tests/dr-ext-host-core.test.js +6 -1
- package/tests/dr-ext-host-overlays.test.js +738 -0
- package/tests/dr-ext-host.test.js +23 -2
- package/tests/dr-form-submit.test.js +23 -0
- package/tests/dr-notes.test.js +227 -0
- package/tests/dr-plotter-map.test.js +123 -0
- package/tests/dr-recenter.test.js +44 -11
- package/tests/dr-viewmodel.test.js +49 -0
- package/tests/plotterext.test.js +2 -2
- package/tests/replay-passage.test.js +8 -1
- package/tools/replay-passage.js +58 -17
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.14.1] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Warning region shapes on the chart (signalk-passage-briefing work doc #48).** The passage-briefing plugin now publishes METAREA and CAP warning areas as `regions` resources (native Polygon/MultiPolygon features paired with their point notes) — the chart draws them: fetched with the same view-scoped query as the notes (`/signalk/v2/api/resources/regions`, matched by geometry-bbox distance), rendered on a "Regions" layers-control toggle mounted by default, in the plugin console's visual language — severity red for `GALE`/`HURRICANE`/`DENSE FOG`/… keyword texts (the shared highlighter), bulletin orange otherwise, heavier stroke for severe, and a dashed outline for the briefing plugin's own bbox-approximate areas (the honesty cue that the shape is the queried bbox). Rings unwrap against the view's longitude frame so a METAREA spanning the dateline draws as one polygon, and both published geometry shapes are accepted (standard-nested and the bulletin engine's flat coordinate chains). A note whose region shape draws stays hidden — the extent renders once — and clicking a shape opens the pick menu at the clicked point with the href-paired note's detail (title, body, Edit/Delete), the same surface as clicking the note marker.
|
|
14
|
+
- **The chart pick menu shows the clicked coordinates** (helm request): the picked position renders in the watchkeeper's own position format (decimal/DM/DMS, shared with the sight panel) above the bearing and distance rows.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Radar-range chart picks seed the clicked object's coordinates again.** `seedObjectPosition` looked the coord fieldset up at shadow-root level, and both the vertical (distance-CPL) and radar forms carry a `data-prefix="center"` fieldset — the query returned the vertical form's (first in document order), so a "Radar range at" pick switched to the radar tab but filled the hidden distance-CPL form, leaving the radar form's Object position empty. The lookup is now scoped to the switched-to form.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Chartplotter zoom and follow-mode handling (helm feedback).** Wheel zoom is now continuous and fast: Leaflet's stock ScrollWheelZoom — which quantizes each debounced wheel burst to whole zoom levels and plays an animation per level (a jerky staircase), and whose sigmoid curve is meaningless under fractional zoom, making a detent move the chart ~0.01 levels — is replaced by a custom rAF-batched handler: wheel deltas accumulate per animation frame and apply as an un-animated fractional zoom at stock Leaflet speed (~one level per detent) with no jumps and no debounce lag. The anchor is follow-aware across every zoom gesture and locked to the BOAT'S POSITION rather than the map center: the wheel handler re-centers each animation frame on the DR position (the center can drift from the vessel between ticks — keyboard pans never fire dragstart, extensions may recenter — and anchoring the center would freeze that drift for the whole gesture, which read as "zoom snaps to whatever was the map center"), while pinch snaps the boat to center at touchstart (before Leaflet captures the gesture anchor) and double-click snaps back after Leaflet's center jump; with follow off the gestures anchor at the cursor as before, and follow is authoritative over the view while engaged: the ◎ re-center pans to the follow target un-animated (immediately) — the live DR position, falling back to the GNSS fix while the DR engine is idle so the button never engages into a silent no-op, with the camera locked onto DR once its positions flow (`vm.followTarget`: a live DR position always wins, so GPS fixes ticking faster than the DR engine cannot drag the view between the two marks; a stale DR position transfers to the fix exactly once, and DR resuming takes the camera back), the keyboard pan handler stands down while following (arrow pans never fire dragstart, so the view would otherwise drift off the vessel and snap back at the next DR tick), and the plotter-extension `map.center`/`map.fitBounds` affordances are refused with a `VIEW_LOCKED` error while follow is engaged (the host reads the lock through a dedicated `getFollow` accessor — `getMap` keeps returning the raw Leaflet map the overlay machinery hooks) — freeform view moves happen only once follow is released. Also moved `customElements.define("dr-map-view")` after the `NauticalScaleControl` class declaration — the synchronous upgrade of an in-DOM element runs `connectedCallback` → `initMap` → `new NauticalScaleControl()`, which threw "Cannot access before initialization" on any page where the element exists before the module evaluates (found by driving the map headless). Follow mode is now **off by default** — the map still opens centered on the vessel (one-shot fit on the first GPS fix, or the first DR position when no fix has arrived) but the helm keeps control of the view. The ◎ button is a real toggle: click to engage follow (and pan to the DR position, clearing the settle window so it always takes effect at once), click again to let go; dragging the chart lets go as before. The zoom-dependent re-renders (range rings, laylines, downstream note refetch) run on a trailing debounce instead of per zoom step.
|
|
21
|
+
|
|
22
|
+
## [0.14.0] - 2026-10-18
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **The DR webapp hosts chart layers: plotter-extension map overlays (work doc #27, chart-layer phase).** The webapp's plotter-extension host — so far widgets and panels only — gains the surface a map layer needs, with [signalk-livedepth-plugin](https://github.com/night199uk/signalk-livedepth-plugin) as the first consumer: soundings recorded by the LiveDepth plugin draw as a shaded, isobath-contoured depth surface on the DR chart, live-extended while sailing. New host capabilities: `background.iframe` (headless 1×1 sandboxed iframes running extension runtimes off-viewport, torn down with the extension), `overlays.image` (the `overlay.request` → `overlay.frame` cycle — the host sends the viewport's EPSG:3857 extent, bounds, pixel ratio clamped to the overlay's `preferredPixelRatio`, resolution, zoom and `liveInterval`; the runtime answers with a transferred `ImageBitmap` the host composites into a pane above the charts and below the chartwork/vessel, refusing `overlays.superseded` frames and pacing live ones at `overlays.throttled`; frames stay georeferenced through panning via their own extent, and world copies on either side of the antimeridian are covered), `map` (`map.getView`/`map.center`/`map.fitBounds` plus settled `map.view` events), `units` (`units.get` with the preferred depth unit, default `m`), and `buttons` (`mapToolbar` entries rendered into a stack above the bottom-left controls, toggling panels). Layer affordances: a ▤ **Plugin layers** control opens a dialog with per-layer show/hide, opacity, and the layer's settings panel (`configPanel`) — layer state persists in `dr.plotterext.overlays.v1` like the chart layers' own prefs, extensions' `state.get/set`/`state.changed` storage shared with their settings panel, and `events.publish` routing (scope `all`/`extension`). The vendored `signalk-plotterext-bus` moves 0.11.0 → 0.21.0 for the `overlay.*` and `map.view` wire surface (0.12–0.18 lack it). Nothing is needed server-side: LiveDepth registers its own `plotterExtensions` provider, and the host discovers it through the same collection polling; the same `overlays.image` surface is what a future radar-overlay provider would plug into for DR fixes.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **The GPS/override bar no longer sits in the middle of a phone screen** (`public/dr-ext-host.js`): on the ≤600 px phone layout the top band stacks vertically, and every `<dr-ext-widget-area>` reserves its full 2×2 widget footprint even while empty — so the empty top-left area inflated the tools column to ~323 px and pushed the GPS status + Engage OVERRIDE panel down to y≈337 of an 844 px viewport, mid-screen. The reservation exists only for the chart context-menu placement hit-test, so it now collapses inside the area's own phone media query; a placed grid still sizes itself through its rows, and placement simply isn't offered on an empty phone area. Phone top band drops from ~656 px to ~260 px with the GPS bar back at the top. `doc/dr-mobile.png` regenerated with the fixed layout.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **Passage replay gains a blind variant — the GPS-loss backtest the tier-2 circularity demanded** (`tools/replay-passage.js`): the derived tier-2 current is fitted to the very GPS track a replay scores against, so "derived beats everything" over a fully-GPS-fed passage is a tautology, not a result. The new `--blind-from ISO` variant simulates the actual use case — GPS dies mid-passage: fixes stop feeding the derived-current state at the cutoff while snapshots keep being taken, so the shadow boat navigates on the carried EWMA current decaying at its 24 h time constant and lapsing to tier 5 after the TTL, with divergence still scored against the hidden GPS track. First blind run on the Niue→Vava'u passage (2026-09-11→14, 253 nm, GPS lost at hour 24 of 60): DR stayed within ~10 nm of truth through the entire 24 h carry window, then degraded at ~1 nm/h once the current support vanished — 22.5 nm final vs 10.0 nm with GPS live and 38.2 nm cold. Also fixed on the way: the derived-current provider returned no `tier` field, so the §6.2 training gate read every tick as unresolved and the learning variant silently never trained (0 matrix bins, learn ≡ derived); it now stamps `tier: 2`, the leeway-synthetic test's threshold is recalibrated to the post-gate semantics (the old bound was met by the current, not by learning), and the `scudLearn` variant is vestigial by design — the gate deliberately blocks training on model currents.
|
|
32
|
+
|
|
10
33
|
## [0.13.4] - 2026-10-16
|
|
11
34
|
|
|
12
35
|
### Added
|
package/README.md
CHANGED
|
@@ -6,13 +6,14 @@ An offline-first dead reckoning and sensor fusion engine for Signal K that maint
|
|
|
6
6
|
|
|
7
7
|
## Why
|
|
8
8
|
|
|
9
|
-
Planning for life without GPS is no longer hypothetical: the UK Maritime and Coastguard Agency's Guidance Note 719 ([reported by Yachting Monthly](https://www.yachtingmonthly.com/news/mca-tells-maritime-sector-to-plan-for-life-without-gps-106566), 1 Oct
|
|
9
|
+
Planning for life without GPS is no longer hypothetical: the UK Maritime and Coastguard Agency's [Guidance Note 719](https://www.gov.uk/government/publications/mgn-719-mf-operating-in-gnss-denied-and-degraded-environments/mgn-719-mf-operating-in-gnss-denied-and-degraded-environments) ([reported by Yachting Monthly](https://www.yachtingmonthly.com/news/mca-tells-maritime-sector-to-plan-for-life-without-gps-106566), 1 Oct 2026) reports a marked increase in GNSS jamming and spoofing worldwide and asks mariners to review contingency arrangements, maintain resilient navigation practices, and practice navigating without satellite positioning — naming dead reckoning, visual bearings, and celestial fixing as valid methods. This plugin implements a contingency for Signal K vessels.
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
13
|
* This plugin installed and configured
|
|
14
14
|
* Some chart source available to Signal K
|
|
15
15
|
* Hand bearing compass and/or sextant
|
|
16
|
+
* A heading source independent of GNSS — a fluxgate or inertial compass feeding `navigation.headingMagnetic`, not a satellite (GNSS) compass. A satellite compass shares the jamming and spoofing failure mode of the position fix, so a vessel whose only heading source is satellite-derived cannot dead-reckon through GNSS denial. Be careful checking this: on N2K networks a single physical device often provides both compass and position data, and display list entries do not necessarily reflect that (a device may publish both PGN 127257 heading and PGN 129029 position under one source)
|
|
16
17
|
|
|
17
18
|
## Getting started
|
|
18
19
|
|
package/package.json
CHANGED
package/public/dr-app.js
CHANGED
|
@@ -140,10 +140,7 @@ template.innerHTML = /* html */ `
|
|
|
140
140
|
look, the glance gets the gist. */
|
|
141
141
|
.dr-status {
|
|
142
142
|
margin: 0;
|
|
143
|
-
max-width: 22rem;
|
|
144
143
|
white-space: nowrap;
|
|
145
|
-
overflow: hidden;
|
|
146
|
-
text-overflow: ellipsis;
|
|
147
144
|
font-family: ui-monospace, "Fira Code", monospace;
|
|
148
145
|
font-size: 0.75rem;
|
|
149
146
|
color: var(--text-muted);
|
|
@@ -158,8 +155,7 @@ template.innerHTML = /* html */ `
|
|
|
158
155
|
--theme-color: var(--color-green);
|
|
159
156
|
color: var(--color-green);
|
|
160
157
|
}
|
|
161
|
-
.dr-status.alert
|
|
162
|
-
.dr-status.retrying {
|
|
158
|
+
.dr-status.alert {
|
|
163
159
|
--theme-color: var(--color-red);
|
|
164
160
|
color: var(--color-red);
|
|
165
161
|
font-weight: 600;
|
|
@@ -179,29 +175,44 @@ template.innerHTML = /* html */ `
|
|
|
179
175
|
background: var(--color-teal);
|
|
180
176
|
color: var(--bg-base);
|
|
181
177
|
}
|
|
182
|
-
/*
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
178
|
+
/* Stream link badge — [ONLINE]/[OFFLINE], color-coded; the full
|
|
179
|
+
connection detail (reconnecting…, which endpoint) rides the
|
|
180
|
+
badge's tooltip. */
|
|
181
|
+
.dr-link {
|
|
182
|
+
font-family: ui-monospace, "Fira Code", monospace;
|
|
183
|
+
font-size: 0.75rem;
|
|
184
|
+
color: var(--color-orange);
|
|
185
|
+
}
|
|
186
|
+
.dr-link.online {
|
|
187
|
+
color: var(--color-green);
|
|
188
|
+
}
|
|
189
|
+
.dr-link.offline {
|
|
190
|
+
color: var(--color-red);
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
}
|
|
193
|
+
/* Failover control — top-right panel, one compact badge row:
|
|
194
|
+
[ONLINE] DR[ON] GNSS[OK] [OVERRIDE]. Alternate-power semantics:
|
|
195
|
+
orange, red when engaged (DR authoritative). The button stays
|
|
196
|
+
prominent (SPEC §14.1: always human-initiated) without eating
|
|
197
|
+
the top bar. */
|
|
187
198
|
.dr-gps {
|
|
188
199
|
display: flex;
|
|
189
200
|
flex-direction: row;
|
|
190
201
|
align-items: center;
|
|
191
202
|
justify-content: flex-end;
|
|
192
|
-
gap: 0.
|
|
203
|
+
gap: 0.5rem;
|
|
193
204
|
flex-wrap: wrap;
|
|
194
205
|
}
|
|
195
206
|
.dr-override {
|
|
196
207
|
display: flex;
|
|
197
208
|
align-items: center;
|
|
198
209
|
justify-content: flex-end;
|
|
199
|
-
gap: 0.
|
|
210
|
+
gap: 0.4rem;
|
|
200
211
|
flex-wrap: wrap;
|
|
201
212
|
}
|
|
202
213
|
.dr-override button {
|
|
203
214
|
--theme-color: var(--color-orange);
|
|
204
|
-
min-width:
|
|
215
|
+
min-width: 5.5rem;
|
|
205
216
|
}
|
|
206
217
|
.dr-override button.engaged,
|
|
207
218
|
.dr-override button.engaged:hover,
|
|
@@ -218,6 +229,9 @@ template.innerHTML = /* html */ `
|
|
|
218
229
|
letter-spacing: 0.08em;
|
|
219
230
|
color: var(--text-muted);
|
|
220
231
|
}
|
|
232
|
+
.dr-override #dr-override-state.off {
|
|
233
|
+
color: var(--color-orange);
|
|
234
|
+
}
|
|
221
235
|
/* Pending-observations drawer: docked left, below the entry
|
|
222
236
|
tools; the list scrolls inside, the panel never grows past the
|
|
223
237
|
viewport. The whole box (toggle included) disappears when there
|
|
@@ -357,7 +371,6 @@ template.innerHTML = /* html */ `
|
|
|
357
371
|
}
|
|
358
372
|
.dr-status {
|
|
359
373
|
font-size: 0.7rem;
|
|
360
|
-
max-width: 14rem;
|
|
361
374
|
}
|
|
362
375
|
.dr-override button {
|
|
363
376
|
min-width: 11rem;
|
|
@@ -421,19 +434,20 @@ template.innerHTML = /* html */ `
|
|
|
421
434
|
<div class="dr-top-col">
|
|
422
435
|
<section class="sk-floating dr-tools">
|
|
423
436
|
<button id="btn-sight">⊕ Sight / LOP</button>
|
|
424
|
-
<button id="btn-coord-fix" title="
|
|
437
|
+
<button id="btn-coord-fix" title="Fix at coordinates — prefilled from the current GNSS position, editable for offline/known-position fixes">⊙ FIX AT</button>
|
|
425
438
|
<button id="btn-pending" aria-expanded="false" aria-controls="dr-pending-drawer" hidden>◧ Pending</button>
|
|
426
439
|
</section>
|
|
427
440
|
<dr-ext-widget-area anchor="top-left"></dr-ext-widget-area>
|
|
428
441
|
</div>
|
|
429
442
|
<div class="dr-top-col dr-top-col-right">
|
|
430
443
|
<section class="sk-floating dr-gps">
|
|
431
|
-
<
|
|
432
|
-
|
|
444
|
+
<span class="dr-link" id="dr-link" title="Connecting to Signal K…">[CONNECTING…]</span>
|
|
445
|
+
<div class="dr-status" id="dr-status-panel" title="No dead-reckoning data">
|
|
446
|
+
<span id="dr-status-text">DR[—]</span>
|
|
433
447
|
</div>
|
|
434
448
|
<div class="dr-override">
|
|
435
|
-
<
|
|
436
|
-
<
|
|
449
|
+
<span id="dr-override-state" title="Normal — the GNSS fix is authoritative">GNSS[OK]</span>
|
|
450
|
+
<button id="dr-override-btn" title="Toggle DR-authoritative override — while engaged the dead-reckoned position steers the chart and the GNSS fix is not trusted">OVERRIDE</button>
|
|
437
451
|
</div>
|
|
438
452
|
</section>
|
|
439
453
|
<dr-ext-widget-area anchor="top-right"></dr-ext-widget-area>
|
|
@@ -783,6 +797,11 @@ class DrApp extends HTMLElement {
|
|
|
783
797
|
* source of truth is the server's v2 resources API; the cache lets
|
|
784
798
|
* saves update markers without a full refetch. */
|
|
785
799
|
this.notesById = new Map();
|
|
800
|
+
/** Signal K regions cache (passage-briefing work doc #48): id →
|
|
801
|
+
* region resource. The warning shapes published alongside the
|
|
802
|
+
* notes — fetched with the same view-scoped query, drawn on the
|
|
803
|
+
* chart as polygon extents. */
|
|
804
|
+
this.regionsById = new Map();
|
|
786
805
|
/** Bounds the last in-view notes fetch covered (fetchNotesInView
|
|
787
806
|
* margin guard) — null until the first view-driven fetch.
|
|
788
807
|
* @type {{west: number, south: number, east: number, north: number}|null} */
|
|
@@ -903,7 +922,19 @@ class DrApp extends HTMLElement {
|
|
|
903
922
|
this.extHost = new PlotterExtHost({
|
|
904
923
|
stream: window.drSignalkStream,
|
|
905
924
|
mount: root,
|
|
925
|
+
// The chart, once it exists — map overlays composite onto it and
|
|
926
|
+
// the map capability reads/drives its viewport.
|
|
927
|
+
getMap: () => this.map?.map ?? null,
|
|
928
|
+
getFollow: () => this.map?.follow ?? false,
|
|
906
929
|
});
|
|
930
|
+
// The host hooks the Leaflet events itself, but discovery can land
|
|
931
|
+
// before the chart exists — forward the element's own view-change
|
|
932
|
+
// event and kick once so the first overlay frames are requested
|
|
933
|
+
// even without a user pan.
|
|
934
|
+
this.map?.addEventListener("dr-view-changed", () =>
|
|
935
|
+
this.extHost.viewChanged(),
|
|
936
|
+
);
|
|
937
|
+
setTimeout(() => this.extHost.viewChanged(), 250);
|
|
907
938
|
for (const el of root.querySelectorAll("dr-ext-widget-area")) {
|
|
908
939
|
this.extHost.attachArea(el);
|
|
909
940
|
}
|
|
@@ -1245,15 +1276,32 @@ class DrApp extends HTMLElement {
|
|
|
1245
1276
|
async fetchNotes(view) {
|
|
1246
1277
|
try {
|
|
1247
1278
|
const query = view ? `?${vm.notesViewQuery(view)}` : "";
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1279
|
+
const [notesRes, regionsRes] = await Promise.all([
|
|
1280
|
+
fetch(`/signalk/v2/api/resources/notes${query}`),
|
|
1281
|
+
fetch(`/signalk/v2/api/resources/regions${query}`),
|
|
1282
|
+
]);
|
|
1283
|
+
if (notesRes.ok) {
|
|
1284
|
+
const collection = await notesRes.json();
|
|
1285
|
+
if (view) {
|
|
1286
|
+
for (const [id, note] of Object.entries(collection ?? {})) {
|
|
1287
|
+
this.notesById.set(id, note);
|
|
1288
|
+
}
|
|
1289
|
+
} else {
|
|
1290
|
+
this.notesById = new Map(Object.entries(collection ?? {}));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
// Regions ride the same view-scoped query: the regions provider
|
|
1294
|
+
// matches by geometry-bbox distance (a region has no position
|
|
1295
|
+
// of its own — the paired note is the queryable anchor).
|
|
1296
|
+
if (regionsRes.ok) {
|
|
1297
|
+
const collection = await regionsRes.json();
|
|
1298
|
+
if (view) {
|
|
1299
|
+
for (const [id, region] of Object.entries(collection ?? {})) {
|
|
1300
|
+
this.regionsById.set(id, region);
|
|
1301
|
+
}
|
|
1302
|
+
} else {
|
|
1303
|
+
this.regionsById = new Map(Object.entries(collection ?? {}));
|
|
1254
1304
|
}
|
|
1255
|
-
} else {
|
|
1256
|
-
this.notesById = new Map(Object.entries(collection ?? {}));
|
|
1257
1305
|
}
|
|
1258
1306
|
this.renderNotes();
|
|
1259
1307
|
} catch {
|
|
@@ -1285,8 +1333,16 @@ class DrApp extends HTMLElement {
|
|
|
1285
1333
|
|
|
1286
1334
|
/** @returns {void} */
|
|
1287
1335
|
renderNotes() {
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1336
|
+
const notes = Object.fromEntries(this.notesById);
|
|
1337
|
+
const regions = Object.fromEntries(this.regionsById);
|
|
1338
|
+
const referenceLon = this.map?.map?.getCenter()?.lng ?? null;
|
|
1339
|
+
const shapes = vm.regionShapes(regions, referenceLon);
|
|
1340
|
+
this.map?.renderRegions(shapes, this.regionsById);
|
|
1341
|
+
// A note whose region shape draws stays hidden: the region carries
|
|
1342
|
+
// the warning's true extent, the note is its anchored detail — the
|
|
1343
|
+
// pair renders once (href-linked, `*-area-` ids).
|
|
1344
|
+
const drawn = new Set(shapes.map((shape) => shape.id));
|
|
1345
|
+
this.map?.renderNotes(vm.notesRenderSpecs(notes, drawn), this.notesById);
|
|
1290
1346
|
}
|
|
1291
1347
|
|
|
1292
1348
|
/**
|
|
@@ -1953,18 +2009,21 @@ class DrApp extends HTMLElement {
|
|
|
1953
2009
|
renderOverride(active) {
|
|
1954
2010
|
if (!this.btn || !this.stateLabel) return;
|
|
1955
2011
|
this.btn.classList.toggle("engaged", active);
|
|
1956
|
-
this.
|
|
1957
|
-
this.stateLabel.textContent = active
|
|
1958
|
-
|
|
1959
|
-
|
|
2012
|
+
this.stateLabel.classList.toggle("off", active);
|
|
2013
|
+
this.stateLabel.textContent = active ? "GNSS[OFF]" : "GNSS[OK]";
|
|
2014
|
+
this.stateLabel.title = active
|
|
2015
|
+
? "Override engaged — the dead-reckoned position is authoritative; the GNSS fix is not trusted"
|
|
2016
|
+
: "Normal — the GNSS fix is authoritative";
|
|
1960
2017
|
}
|
|
1961
2018
|
|
|
1962
2019
|
/**
|
|
1963
|
-
* Renders the DR engine state
|
|
1964
|
-
*
|
|
1965
|
-
*
|
|
2020
|
+
* Renders the DR engine state as a compact badge (DR[ON], DR[IDLE],
|
|
2021
|
+
* DR[FOULED]…) with the full explanatory text on the badge's
|
|
2022
|
+
* tooltip — the helm reads the row at a glance and hovers for the
|
|
2023
|
+
* why.
|
|
1966
2024
|
*
|
|
1967
|
-
* @param {{status: string, reason?: string
|
|
2025
|
+
* @param {{status: string, reason?: string, moving?: boolean,
|
|
2026
|
+
* fouled?: boolean, transient?: boolean}|null} value
|
|
1968
2027
|
* @returns {void}
|
|
1969
2028
|
*/
|
|
1970
2029
|
renderDrState(value) {
|
|
@@ -1972,43 +2031,54 @@ class DrApp extends HTMLElement {
|
|
|
1972
2031
|
const text = this.shadowRoot.querySelector("#dr-status-text");
|
|
1973
2032
|
if (!panel || !text) return;
|
|
1974
2033
|
panel.classList.remove("idle", "underway", "alert", "transient");
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2034
|
+
let title = "No dead-reckoning data";
|
|
2035
|
+
if (value) {
|
|
2036
|
+
if (value.status === "idle") {
|
|
2037
|
+
panel.classList.add("idle");
|
|
2038
|
+
if (value.moving) panel.classList.add("alert");
|
|
2039
|
+
} else if (value.status === "underway") {
|
|
2040
|
+
panel.classList.add("underway");
|
|
2041
|
+
if (value.fouled) panel.classList.add("alert");
|
|
2042
|
+
else if (value.transient) panel.classList.add("transient");
|
|
2043
|
+
} else if (value.status === "warm" && value.fouled) {
|
|
2044
|
+
// The engine runs warm on a tied-up boat (SPEC §5) — muted,
|
|
2045
|
+
// unless the speed sensor is crying wolf.
|
|
2046
|
+
panel.classList.add("alert");
|
|
2047
|
+
}
|
|
2048
|
+
title =
|
|
2049
|
+
vm.drStatusText({ ...value, navState: this.vesselNavState }) ?? title;
|
|
1978
2050
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
if (value.moving) panel.classList.add("alert");
|
|
1982
|
-
} else if (value.status === "underway") {
|
|
1983
|
-
panel.classList.add("underway");
|
|
1984
|
-
if (value.fouled) panel.classList.add("alert");
|
|
1985
|
-
else if (value.transient) panel.classList.add("transient");
|
|
1986
|
-
} else if (value.status === "warm") {
|
|
1987
|
-
// Engine alive, boat tied up — muted, no status color crying wolf.
|
|
1988
|
-
if (value.fouled) panel.classList.add("alert");
|
|
1989
|
-
}
|
|
1990
|
-
const next = vm.drStatusText({ ...value, navState: this.vesselNavState });
|
|
1991
|
-
if (next != null) text.textContent = next;
|
|
2051
|
+
text.textContent = vm.drStatusBadge(value);
|
|
2052
|
+
panel.title = title;
|
|
1992
2053
|
}
|
|
1993
2054
|
|
|
1994
2055
|
/**
|
|
1995
|
-
* Renders the stream link state
|
|
1996
|
-
*
|
|
2056
|
+
* Renders the stream link state as a compact [ONLINE]/[OFFLINE]
|
|
2057
|
+
* badge (color-coded) with the full connection detail on its
|
|
2058
|
+
* tooltip — a dropped connection is visible immediately without
|
|
2059
|
+
* eating the top bar.
|
|
1997
2060
|
*
|
|
1998
2061
|
* @param {{state: "connecting"|"open"|"retrying"}} status
|
|
1999
2062
|
* @returns {void}
|
|
2000
2063
|
*/
|
|
2001
2064
|
renderLinkStatus(status) {
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2065
|
+
const link = this.shadowRoot.querySelector("#dr-link");
|
|
2066
|
+
if (link) {
|
|
2067
|
+
link.classList.remove("online", "offline");
|
|
2068
|
+
if (status.state === "open") {
|
|
2069
|
+
link.textContent = "[ONLINE]";
|
|
2070
|
+
link.classList.add("online");
|
|
2071
|
+
link.title = "Signal K stream connected";
|
|
2072
|
+
} else if (status.state === "retrying") {
|
|
2073
|
+
link.textContent = "[OFFLINE]";
|
|
2074
|
+
link.classList.add("offline");
|
|
2075
|
+
link.title = "Signal K link lost — reconnecting…";
|
|
2076
|
+
} else {
|
|
2077
|
+
link.textContent = "[CONNECTING…]";
|
|
2078
|
+
link.title = "Connecting to Signal K…";
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
if (status.state === "open") {
|
|
2012
2082
|
// Re-seed AIS static data on (re)connect (work doc #23) — names
|
|
2013
2083
|
// and MMSIs don't ride position deltas.
|
|
2014
2084
|
this.bootstrapAis();
|
|
@@ -2017,7 +2087,6 @@ class DrApp extends HTMLElement {
|
|
|
2017
2087
|
// while offline) reappear without a page reload.
|
|
2018
2088
|
this.fetchNotes();
|
|
2019
2089
|
}
|
|
2020
|
-
// "open" — let the DR state panel take over once data flows.
|
|
2021
2090
|
}
|
|
2022
2091
|
}
|
|
2023
2092
|
|