@meri-imperiumi/signalk-dead-reckoning 0.14.0 → 0.14.2
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 +17 -0
- package/package.json +1 -1
- package/public/dr-app.js +124 -66
- package/public/dr-ext-host.js +26 -0
- package/public/dr-map-view.js +409 -83
- package/public/dr-sight-panel.js +8 -3
- package/public/dr-viewmodel.js +250 -3
- package/tests/dr-ext-host-overlays.test.js +34 -0
- 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-route.test.js +109 -0
- package/tests/dr-viewmodel.test.js +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.14.2] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Routes and tracks crossing the antimeridian no longer draw a line around the planet.** An active-route leg straddling ±180° longitude (Fiji↔Samoa, Tonga's eastern islands, the Bering Strait) rendered as a horizontal stroke spanning the whole chart width — Leaflet joins two raw longitudes straight through the primary world frame instead of hopping the seam. Route lines draw unwrapped (`vm.unwrapLineLongitudes`: every point shifted ±360° onto the previous point's longitude strip) and repeat on the ±360° world copies the view can sit over (`vm.worldCopies`), so the leg renders as the short hop over the dateline with waypoint markers and the "next" emphasis intact; the ghost (DR) and GPS tracks get the same treatment, as a boat logging fixes across the seam produced the identical artifact in its track.
|
|
14
|
+
|
|
15
|
+
## [0.14.1] - 2026-10-19
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **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.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **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.
|
|
26
|
+
|
|
10
27
|
## [0.14.0] - 2026-10-18
|
|
11
28
|
|
|
12
29
|
### Added
|
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} */
|
|
@@ -906,6 +925,7 @@ class DrApp extends HTMLElement {
|
|
|
906
925
|
// The chart, once it exists — map overlays composite onto it and
|
|
907
926
|
// the map capability reads/drives its viewport.
|
|
908
927
|
getMap: () => this.map?.map ?? null,
|
|
928
|
+
getFollow: () => this.map?.follow ?? false,
|
|
909
929
|
});
|
|
910
930
|
// The host hooks the Leaflet events itself, but discovery can land
|
|
911
931
|
// before the chart exists — forward the element's own view-change
|
|
@@ -1256,15 +1276,32 @@ class DrApp extends HTMLElement {
|
|
|
1256
1276
|
async fetchNotes(view) {
|
|
1257
1277
|
try {
|
|
1258
1278
|
const query = view ? `?${vm.notesViewQuery(view)}` : "";
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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 ?? {}));
|
|
1265
1304
|
}
|
|
1266
|
-
} else {
|
|
1267
|
-
this.notesById = new Map(Object.entries(collection ?? {}));
|
|
1268
1305
|
}
|
|
1269
1306
|
this.renderNotes();
|
|
1270
1307
|
} catch {
|
|
@@ -1296,8 +1333,16 @@ class DrApp extends HTMLElement {
|
|
|
1296
1333
|
|
|
1297
1334
|
/** @returns {void} */
|
|
1298
1335
|
renderNotes() {
|
|
1299
|
-
const
|
|
1300
|
-
|
|
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);
|
|
1301
1346
|
}
|
|
1302
1347
|
|
|
1303
1348
|
/**
|
|
@@ -1964,18 +2009,21 @@ class DrApp extends HTMLElement {
|
|
|
1964
2009
|
renderOverride(active) {
|
|
1965
2010
|
if (!this.btn || !this.stateLabel) return;
|
|
1966
2011
|
this.btn.classList.toggle("engaged", active);
|
|
1967
|
-
this.
|
|
1968
|
-
this.stateLabel.textContent = active
|
|
1969
|
-
|
|
1970
|
-
|
|
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";
|
|
1971
2017
|
}
|
|
1972
2018
|
|
|
1973
2019
|
/**
|
|
1974
|
-
* Renders the DR engine state
|
|
1975
|
-
*
|
|
1976
|
-
*
|
|
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.
|
|
1977
2024
|
*
|
|
1978
|
-
* @param {{status: string, reason?: string
|
|
2025
|
+
* @param {{status: string, reason?: string, moving?: boolean,
|
|
2026
|
+
* fouled?: boolean, transient?: boolean}|null} value
|
|
1979
2027
|
* @returns {void}
|
|
1980
2028
|
*/
|
|
1981
2029
|
renderDrState(value) {
|
|
@@ -1983,43 +2031,54 @@ class DrApp extends HTMLElement {
|
|
|
1983
2031
|
const text = this.shadowRoot.querySelector("#dr-status-text");
|
|
1984
2032
|
if (!panel || !text) return;
|
|
1985
2033
|
panel.classList.remove("idle", "underway", "alert", "transient");
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
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;
|
|
1989
2050
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
if (value.moving) panel.classList.add("alert");
|
|
1993
|
-
} else if (value.status === "underway") {
|
|
1994
|
-
panel.classList.add("underway");
|
|
1995
|
-
if (value.fouled) panel.classList.add("alert");
|
|
1996
|
-
else if (value.transient) panel.classList.add("transient");
|
|
1997
|
-
} else if (value.status === "warm") {
|
|
1998
|
-
// Engine alive, boat tied up — muted, no status color crying wolf.
|
|
1999
|
-
if (value.fouled) panel.classList.add("alert");
|
|
2000
|
-
}
|
|
2001
|
-
const next = vm.drStatusText({ ...value, navState: this.vesselNavState });
|
|
2002
|
-
if (next != null) text.textContent = next;
|
|
2051
|
+
text.textContent = vm.drStatusBadge(value);
|
|
2052
|
+
panel.title = title;
|
|
2003
2053
|
}
|
|
2004
2054
|
|
|
2005
2055
|
/**
|
|
2006
|
-
* Renders the stream link state
|
|
2007
|
-
*
|
|
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.
|
|
2008
2060
|
*
|
|
2009
2061
|
* @param {{state: "connecting"|"open"|"retrying"}} status
|
|
2010
2062
|
* @returns {void}
|
|
2011
2063
|
*/
|
|
2012
2064
|
renderLinkStatus(status) {
|
|
2013
|
-
const
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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") {
|
|
2023
2082
|
// Re-seed AIS static data on (re)connect (work doc #23) — names
|
|
2024
2083
|
// and MMSIs don't ride position deltas.
|
|
2025
2084
|
this.bootstrapAis();
|
|
@@ -2028,7 +2087,6 @@ class DrApp extends HTMLElement {
|
|
|
2028
2087
|
// while offline) reappear without a page reload.
|
|
2029
2088
|
this.fetchNotes();
|
|
2030
2089
|
}
|
|
2031
|
-
// "open" — let the DR state panel take over once data flows.
|
|
2032
2090
|
}
|
|
2033
2091
|
}
|
|
2034
2092
|
|
package/public/dr-ext-host.js
CHANGED
|
@@ -89,6 +89,9 @@ export class PlotterExtHost {
|
|
|
89
89
|
* @param {() => object|null} [opts.getMap] - the Leaflet map once
|
|
90
90
|
* the chart exists (dr-app wires it to `<dr-map-view>`); null
|
|
91
91
|
* until then — the overlay machinery no-ops without a chart
|
|
92
|
+
* @param {() => boolean} [opts.getFollow] - whether center-on-vessel
|
|
93
|
+
* (follow) is engaged: while it is, the view is locked and freeform
|
|
94
|
+
* extension moves are refused
|
|
92
95
|
* @param {{depth: string}} [opts.units] - the user's preferred units
|
|
93
96
|
* (`units.get`)
|
|
94
97
|
* @param {Storage} [opts.storage]
|
|
@@ -99,6 +102,7 @@ export class PlotterExtHost {
|
|
|
99
102
|
this.stream = opts.stream;
|
|
100
103
|
this.mount = opts.mount;
|
|
101
104
|
this.getMap = opts.getMap ?? (() => null);
|
|
105
|
+
this.getFollow = opts.getFollow ?? (() => false);
|
|
102
106
|
this.units = opts.units ?? { depth: "m" };
|
|
103
107
|
this.storage = opts.storage ?? globalThis.localStorage;
|
|
104
108
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch?.bind(globalThis);
|
|
@@ -629,6 +633,10 @@ export class PlotterExtHost {
|
|
|
629
633
|
}
|
|
630
634
|
const map = this.getMap?.();
|
|
631
635
|
if (!map) throw notSupported("map");
|
|
636
|
+
// Center-on-vessel owns the view while engaged: the chart is
|
|
637
|
+
// locked on the DR position and freeform view moves (an
|
|
638
|
+
// extension's included) only happen once follow is released.
|
|
639
|
+
if (this.getFollow?.()) throw viewLocked("map.center");
|
|
632
640
|
const zoom =
|
|
633
641
|
params.zoom === undefined || params.zoom === null
|
|
634
642
|
? undefined
|
|
@@ -650,6 +658,7 @@ export class PlotterExtHost {
|
|
|
650
658
|
}
|
|
651
659
|
const map = this.getMap?.();
|
|
652
660
|
if (!map) throw notSupported("map");
|
|
661
|
+
if (this.getFollow?.()) throw viewLocked("map.fitBounds");
|
|
653
662
|
const [west, south, east, north] = b;
|
|
654
663
|
// Antimeridian form (west > east): unwrap east so Leaflet's
|
|
655
664
|
// bounds span the short way round.
|
|
@@ -1262,6 +1271,23 @@ function badParams(message) {
|
|
|
1262
1271
|
});
|
|
1263
1272
|
}
|
|
1264
1273
|
|
|
1274
|
+
/**
|
|
1275
|
+
* The chart is locked on the vessel (center-on-vessel engaged) — view
|
|
1276
|
+
* moves are refused until follow is released.
|
|
1277
|
+
*
|
|
1278
|
+
* @param {string} method - the refused affordance, for the message
|
|
1279
|
+
* @returns {RpcError}
|
|
1280
|
+
*/
|
|
1281
|
+
function viewLocked(method) {
|
|
1282
|
+
return new RpcError(
|
|
1283
|
+
`${method} refused: the chart is locked on the vessel (center-on-vessel engaged) — release follow to move the view`,
|
|
1284
|
+
{
|
|
1285
|
+
code: RPC_ERRORS.HOST_ERROR,
|
|
1286
|
+
reason: "VIEW_LOCKED",
|
|
1287
|
+
},
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1265
1291
|
/**
|
|
1266
1292
|
* @param {unknown} keys
|
|
1267
1293
|
* @returns {string[]|undefined}
|