@meri-imperiumi/signalk-passage-briefing 0.13.0 → 0.13.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 +19 -0
- package/package.json +1 -1
- package/plugin/bulletin-engine.js +69 -0
- package/plugin/index.js +154 -13
- package/plugin/notes-publisher.js +594 -44
- package/plugin/notes-store.js +26 -5
- package/plugin/regions-store.js +440 -0
- package/public/components/conditions-here.js +0 -1
- package/public/components/map-model.mjs +99 -75
- package/public/components/map-view.js +166 -135
- package/public/components/strategic-outlook.js +0 -1
- package/public/components/tactical-dashboard.js +0 -1
- package/tests/bulletin-engine.test.js +73 -0
- package/tests/cap-source.test.js +40 -6
- package/tests/map-model.test.js +155 -72
- package/tests/notes-publisher.test.js +482 -0
- package/tests/plugin.test.js +212 -2
- package/tests/regions-store.test.js +420 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.13.2] - 2026-10-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Briefing refresh crashed with 502 `Cannot read properties of undefined (reading '1')` when the course provider reported the vessel navigating the briefed route while still heading to its first waypoint (`navigation.course.activeRoute.pointIndex` 0, forward — e.g. just after engaging a route): the progress trim read `raw[pointIndex - 1]` for a last passed waypoint that does not exist yet. Forward pointIndex 0 now trims to the vessel position with nothing trimmed off (`trimmedFromNm` 0), and without a known vessel position the full untrimmed plan serves.
|
|
9
|
+
|
|
10
|
+
## [0.13.1] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- The warnings the resources carry are now one canonical set, published against the union of the vessel's position and the active route (work doc #48): every publish pass — fetch windows and page loads alike — previously filtered AND pruned against its own trigger's track, so a here-mode page load (position only) wiped the route's warnings and a route compile wiped the position's: the boat saw one bounding-box region while the cyclone, its 300-nautical-mile wind circle and the troughs rode the console unseen, then nothing at all. The publication track is now the union of both — and a page load that serves a cached briefing (no compile ran in the process) publishes from that briefing's own waypoints, so a restart plus a page load is enough to fill the plotter. Content-addressed ids make the re-publish idempotent, and unchanged warning sets write nothing to disk. The wider-area severe warnings — the off-track context the console never shows — publish too, as `coverage: "wider"` regions: plotters show the gale 300 nautical miles off, and the briefing map draws them from the resources muted, exactly as it drew them from the payload before. The map's payload-drawn warning layers are gone: only the route ribbon, the timeline event pins and the cyclone graphic (whose per-ring thresholds and forecast track the regions schema cannot carry) remain payload-driven, and the cyclone's own region rings yield to that graphic.
|
|
14
|
+
- The published regions now split at the antimeridian (work doc #48): the parsed warning chains carry raw coordinates (166E → 180 → 170W on this week's convergence zone), and plotters that do not unwrap — Freeboard draws the raw ring — would render a seam-crossing warning across the wrong hemisphere. Every published ring — parsed areas, corridor bands, cyclone quadrant rings, CAP polygons — now unfolds into a continuous frame and clips against the seam, publishing a MultiPolygon of spec-clean parts (the convergence zone became 166..180E plus 180..152W), so both the briefing map and any other plotter draw the same honest shape. Bounds-boxes split the same way and did already.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- The NFFN storm warnings' quadrant wind fields now parse (work doc #48 session): RSMC Nadi spells the wind areas out in prose — "EXPECT WINDS OVER 47 KNOTS WITHIN 50 NAUTICAL MILES IN NE QUADRANT AND WITHIN 60 NAUTICAL MILES IN SE QUADRANT …" — a format the NHC advisory parser never accepted, so a tropical cyclone announced this way was a lone point everywhere: no wind rings on the map's cyclone graphic and no region on the chart plotter. Each threshold's four quadrant radii now build the same great-circle rings the advisory fields carry, on the same `storm.fields` structure — the map graphic and the published regions read both formats alike, and the 33-knot gale ring and the 47-knot storm ring publish as a MultiPolygon region beside the declaration's point note.
|
|
18
|
+
- Warning shapes on the chart plotter (work doc #48): the METAREA and CAP warnings already publish as georeferenced notes, but the notes schema carries a single point — the gale area, the corridor band and the CAP polygon collapsed to their nearest-vertex marker. The plugin now registers a `regions` resource provider alongside the server's built-in one (the resources API merges providers, so user-created regions are untouched) and publishes a region beside each shaped warning's note: bounding boxes as ring polygons (antimeridian-crossing boxes split into two spec-clean rectangles), parsed polygons as-is, trough axis bands expanded into the same corridor rings the briefing map draws, and tropical-cyclone advisories as the MultiPolygon of their present wind/seas quadrant rings. Each paired note links to its region via `href` — the exact link Freeboard's region panel lists related notes by — and provenance (issuer, zone, subject, derivation) rides the feature properties. GDACS hazards stay note-only (points); axis lines without a stated band and text-only storm declarations have no area on the map either.
|
|
19
|
+
- The briefing's warnings map draws the published resources (work doc #47 session): the hand-drawn warning-shapes layer — bulletin block shapes, CAP polygons, GDACS pins — is replaced by the same `/resources/notes` and `/resources/regions` the plotter sees, so the map and Freeboard render one set of warnings and any third-party resource provider's notes and regions appear for free. Severity coloring re-derives from the warning text with the console's own highlighter (ours and third-party alike); the dashed-bbox honesty cue and the console click-through stay ours-only, matched by the block text the resources carry verbatim; a note whose region draws stays hidden so a shaped warning renders once. What stays payload-driven on purpose: the route ribbon, the timeline event pins, the cyclone graphic (per-ring thresholds and the forecast track are not representable in the regions schema) and the muted wider-area set, which keeps its muting and stays off other plotters. Third-party resources never drive the fit — the view stays anchored on the vessel's waters.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- The METAREA notes pass pruned the whole shared notes store, erasing GDACS hazard notes, CAP notes and any client-written notes on every bulletin publish. All three feeds now prune scoped to their own id prefixes (`metarea-`, `hazard-`, `cap-`), so notes from the other feeds and notes written by other clients survive every pass.
|
|
23
|
+
|
|
5
24
|
## [0.13.0] - 2026-10-18
|
|
6
25
|
|
|
7
26
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1358,6 +1358,66 @@ function advisorySeverityLabel(maxWindKt) {
|
|
|
1358
1358
|
* @returns {object|null} Structured storm, null when the text is not
|
|
1359
1359
|
* the advisory family
|
|
1360
1360
|
*/
|
|
1361
|
+
/**
|
|
1362
|
+
* A tropical-cyclone declaration's quadrant wind radii (work doc #48
|
|
1363
|
+
* session): the RSMC NFFN storm warnings spell the wind fields out in
|
|
1364
|
+
* prose — "EXPECT WINDS OVER 47 KNOTS WITHIN 50 NAUTICAL MILES IN NE
|
|
1365
|
+
* QUADRANT AND WITHIN 60 NAUTICAL MILES IN SE QUADRANT …" — a format
|
|
1366
|
+
* the NHC advisory parser never sees. Each threshold's quadrant
|
|
1367
|
+
* radii build the same great-circle rings the advisory fields carry,
|
|
1368
|
+
* so the map's cyclone graphic and the published regions read the
|
|
1369
|
+
* same parsed structure for both formats. Null when the text names no
|
|
1370
|
+
* radii set.
|
|
1371
|
+
*
|
|
1372
|
+
* @param {string} text - Block text
|
|
1373
|
+
* @param {number[][]|null} coordinates - The declaration point
|
|
1374
|
+
* ([lon, lat]) the radii centre on
|
|
1375
|
+
* @returns {object|null} A partial advisory shape ({center, fields,
|
|
1376
|
+
* forecastPoints}) — the fields the map graphic and the region
|
|
1377
|
+
* publisher already consume
|
|
1378
|
+
*/
|
|
1379
|
+
function declarationStorm(text, coordinates) {
|
|
1380
|
+
if (
|
|
1381
|
+
!Array.isArray(coordinates) ||
|
|
1382
|
+
coordinates.length !== 2 ||
|
|
1383
|
+
!coordinates.every(Number.isFinite)
|
|
1384
|
+
) {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
const [lon, lat] = coordinates;
|
|
1388
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
const fields = [];
|
|
1392
|
+
// One set per threshold: "OVER 47 KNOTS" followed by its quadrant
|
|
1393
|
+
// radii chain — the chain breaks at the next "EXPECT ... KNOTS"
|
|
1394
|
+
const setRe =
|
|
1395
|
+
/OVER\s+(\d{1,3})\s*KNOTS((?:\s+AND)?\s+WITHIN\s+\d+\s*NAUTICAL\s*MILES\s+IN\s+(?:NE|SE|SW|NW)\s+QUADRANT)+/gi;
|
|
1396
|
+
const quadRe =
|
|
1397
|
+
/WITHIN\s+(\d+)\s*NAUTICAL\s*MILES\s+IN\s+(NE|SE|SW|NW)\s+QUADRANT/gi;
|
|
1398
|
+
for (const setMatch of text.matchAll(setRe)) {
|
|
1399
|
+
const threshold = Number.parseInt(setMatch[1], 10);
|
|
1400
|
+
const radiiNm = { ne: 0, se: 0, sw: 0, nw: 0 };
|
|
1401
|
+
for (const quadMatch of setMatch[0].matchAll(quadRe)) {
|
|
1402
|
+
radiiNm[quadMatch[2].toLowerCase()] = Number.parseInt(quadMatch[1], 10);
|
|
1403
|
+
}
|
|
1404
|
+
const geometry = quadrantRing({ lat, lon, radiiNm });
|
|
1405
|
+
if (geometry) {
|
|
1406
|
+
fields.push({
|
|
1407
|
+
label: `${threshold} KT`,
|
|
1408
|
+
threshold,
|
|
1409
|
+
kind: "wind",
|
|
1410
|
+
radiiNm,
|
|
1411
|
+
geometry,
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (fields.length === 0) {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
return { center: { lat, lon }, fields, forecastPoints: [] };
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1361
1421
|
function parseAdvisory(text) {
|
|
1362
1422
|
if (
|
|
1363
1423
|
typeof text !== "string" ||
|
|
@@ -1578,11 +1638,20 @@ function filterBulletin({
|
|
|
1578
1638
|
if (!geometry && !hasSevereKeyword(blockText)) {
|
|
1579
1639
|
return null;
|
|
1580
1640
|
}
|
|
1641
|
+
// A storm declaration whose text spells out quadrant wind
|
|
1642
|
+
// radii carries the same parsed fields the advisory family
|
|
1643
|
+
// does — the cyclone's wind rings become map graphic and
|
|
1644
|
+
// published region, not just a point (work doc #48 session)
|
|
1645
|
+
const storm =
|
|
1646
|
+
geometry?.type === "point"
|
|
1647
|
+
? declarationStorm(blockText, geometry.coordinates)
|
|
1648
|
+
: null;
|
|
1581
1649
|
return {
|
|
1582
1650
|
text: blockText,
|
|
1583
1651
|
subject: subject ?? null,
|
|
1584
1652
|
geometryType: geometry ? geometry.type : null,
|
|
1585
1653
|
geometry: geometry ?? null,
|
|
1654
|
+
...(storm ? { storm } : {}),
|
|
1586
1655
|
source,
|
|
1587
1656
|
};
|
|
1588
1657
|
})
|
package/plugin/index.js
CHANGED
|
@@ -59,8 +59,13 @@ const ephemeris = require("./celestial-ephemeris.js");
|
|
|
59
59
|
const { registerPlotterExtension } = require("./brief-ext.js");
|
|
60
60
|
const { registerStatusTileExamples } = require("./statustilesexamples.js");
|
|
61
61
|
const { createNotesStore, registerNotesProvider } = require("./notes-store.js");
|
|
62
|
+
const {
|
|
63
|
+
createRegionsStore,
|
|
64
|
+
registerRegionsProvider,
|
|
65
|
+
} = require("./regions-store.js");
|
|
62
66
|
const {
|
|
63
67
|
clearNotes,
|
|
68
|
+
clearRegions,
|
|
64
69
|
publishNotes,
|
|
65
70
|
publishHazardNotes,
|
|
66
71
|
publishCapAlertNotes,
|
|
@@ -316,6 +321,8 @@ module.exports = (app) => {
|
|
|
316
321
|
let teardownPlotterExt = null;
|
|
317
322
|
let teardownStatusTiles = null;
|
|
318
323
|
let teardownNotesProvider = null;
|
|
324
|
+
let regionsStore = null;
|
|
325
|
+
let teardownRegionsProvider = null;
|
|
319
326
|
let notesStore = null;
|
|
320
327
|
/** Route-list cache (bergie session perf pass): the resources
|
|
321
328
|
* listing is seconds-slow per call and the list changes rarely —
|
|
@@ -557,12 +564,25 @@ module.exports = (app) => {
|
|
|
557
564
|
) {
|
|
558
565
|
return null; // Not past the first point, or an index we cannot use
|
|
559
566
|
}
|
|
567
|
+
const target = raw[pointIndex];
|
|
568
|
+
const vessel = vesselPosition();
|
|
569
|
+
// Heading to the first waypoint (forward pointIndex 0): no leg of
|
|
570
|
+
// the plan is behind the boat, so there is no last passed waypoint
|
|
571
|
+
// to anchor the trim distance on. The plan starts at the vessel
|
|
572
|
+
// when known (nothing trimmed off, 0 nm in), else serves in full.
|
|
573
|
+
if (!reverse && pointIndex === 0) {
|
|
574
|
+
if (!vessel) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
coordinates: [[vessel.lon, vessel.lat], ...raw],
|
|
579
|
+
trimmedFromNm: 0,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
560
582
|
// Travel order decides what remains: forward navigates toward
|
|
561
583
|
// higher indices, reverse toward lower ones (the course provider's
|
|
562
584
|
// `reverse` flag). The start point is the vessel's actual position
|
|
563
585
|
// when available, else the last passed waypoint.
|
|
564
|
-
const target = raw[pointIndex];
|
|
565
|
-
const vessel = vesselPosition();
|
|
566
586
|
const start = vessel
|
|
567
587
|
? [vessel.lon, vessel.lat]
|
|
568
588
|
: reverse
|
|
@@ -572,7 +592,8 @@ module.exports = (app) => {
|
|
|
572
592
|
? raw.slice(0, pointIndex + 1).reverse()
|
|
573
593
|
: raw.slice(pointIndex);
|
|
574
594
|
// Distance along the stored plan from its start to the trim point:
|
|
575
|
-
// the boat sits between the target and the last passed point
|
|
595
|
+
// the boat sits between the target and the last passed point (the
|
|
596
|
+
// forward pointIndex 0 case returned above, so this is defined)
|
|
576
597
|
const reference = reverse ? target : raw[pointIndex - 1];
|
|
577
598
|
const anchorSlice = reverse
|
|
578
599
|
? raw.slice(0, pointIndex + 1)
|
|
@@ -933,12 +954,80 @@ module.exports = (app) => {
|
|
|
933
954
|
/**
|
|
934
955
|
* Publishes the filtered bulletin blocks as georeferenced server
|
|
935
956
|
* notes (work doc #12). Local-only: not gated on the internet
|
|
936
|
-
* state, so spool-sourced warnings publish the same way.
|
|
957
|
+
* state, so spool-sourced warnings publish the same way. Also runs
|
|
958
|
+
* at serve time (trigger "serve", work doc #48): the refiltered
|
|
959
|
+
* bulletin the console renders is what the resources must carry.
|
|
937
960
|
*
|
|
938
|
-
* @param {"here"|"route"} trigger
|
|
961
|
+
* @param {"here"|"route"|"serve"} trigger
|
|
939
962
|
* @param {object|null} bulletin - metareaBulletin with blocks
|
|
940
963
|
* @param {Array<{lat: number, lon: number}>} waypoints
|
|
941
964
|
*/
|
|
965
|
+
/**
|
|
966
|
+
* The publication track (work doc #48): the vessel's position first
|
|
967
|
+
* (it anchors the nearest-point note placement), then the route
|
|
968
|
+
* waters — the caller's track hint (the served briefing's own
|
|
969
|
+
* waypoints) when one exists, else the most recent route compile's.
|
|
970
|
+
* The union of the two is the set of waters the passage touches —
|
|
971
|
+
* the notes and regions the plotter sees must not depend on which
|
|
972
|
+
* view triggered the last publish, nor on whether a compile ran in
|
|
973
|
+
* this process at all (a cached-briefing page load never compiles).
|
|
974
|
+
*
|
|
975
|
+
* @param {Array<{lat: number, lon: number}>|null} [hint] - The
|
|
976
|
+
* served briefing's waypoints when available
|
|
977
|
+
* @returns {Array<{lat: number, lon: number}>}
|
|
978
|
+
*/
|
|
979
|
+
function publicationTrack(hint = null) {
|
|
980
|
+
const track = [];
|
|
981
|
+
const seen = new Set();
|
|
982
|
+
const push = (lat, lon) => {
|
|
983
|
+
const key = `${lat},${lon}`;
|
|
984
|
+
if (Number.isFinite(lat) && Number.isFinite(lon) && !seen.has(key)) {
|
|
985
|
+
seen.add(key);
|
|
986
|
+
track.push({ lat, lon });
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
const position = vesselPosition();
|
|
990
|
+
if (position) {
|
|
991
|
+
push(position.lat, position.lon);
|
|
992
|
+
}
|
|
993
|
+
for (const waypoint of hint ?? lastRouteWaypoints ?? []) {
|
|
994
|
+
push(waypoint.lat, waypoint.lon);
|
|
995
|
+
}
|
|
996
|
+
return track;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* The canonical publication pass (work doc #48): one set of notes
|
|
1001
|
+
* and regions for the plotter, filtered against the publication
|
|
1002
|
+
* track — the vessel's position plus the active route — regardless
|
|
1003
|
+
* of which view or fetch window triggered it. A per-trigger filter
|
|
1004
|
+
* (here: position-only, route: route-only) would have each pass
|
|
1005
|
+
* prune the other's warnings; the union track ends the fight. An
|
|
1006
|
+
* empty result clears this feed's resources: expiry is part of the
|
|
1007
|
+
* feature.
|
|
1008
|
+
*
|
|
1009
|
+
* @param {string} trigger - for the debug line
|
|
1010
|
+
* @param {Array<{lat: number, lon: number}>|null} [hint] - The
|
|
1011
|
+
* served briefing's waypoints when available
|
|
1012
|
+
*/
|
|
1013
|
+
async function publishWarningsFor(trigger, hint = null) {
|
|
1014
|
+
if (!publishNotesEnabled) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const track = publicationTrack(hint);
|
|
1018
|
+
if (track.length === 0) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const bulletin = await bulletinForTrack(track);
|
|
1022
|
+
await publishNotesFor(
|
|
1023
|
+
trigger,
|
|
1024
|
+
// Null bulletin (nothing on our waters) publishes an empty set:
|
|
1025
|
+
// the scoped prune drops this feed's stale warnings
|
|
1026
|
+
bulletin ?? { blocks: [], wider: [] },
|
|
1027
|
+
track,
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
942
1031
|
async function publishNotesFor(trigger, bulletin, waypoints) {
|
|
943
1032
|
if (!publishNotesEnabled || !bulletin) {
|
|
944
1033
|
return;
|
|
@@ -956,15 +1045,24 @@ module.exports = (app) => {
|
|
|
956
1045
|
}
|
|
957
1046
|
const result = await publishNotes({
|
|
958
1047
|
store: notesStore,
|
|
1048
|
+
regionsStore,
|
|
959
1049
|
bulletin,
|
|
960
1050
|
zone,
|
|
961
1051
|
synopticChartFor: () => chart,
|
|
962
1052
|
ref: waypoints.length > 0 ? [waypoints[0].lon, waypoints[0].lat] : null,
|
|
963
1053
|
});
|
|
964
|
-
if (
|
|
1054
|
+
if (
|
|
1055
|
+
result.published.length > 0 ||
|
|
1056
|
+
result.pruned.length > 0 ||
|
|
1057
|
+
result.regionsPublished.length > 0 ||
|
|
1058
|
+
result.regionsPruned.length > 0
|
|
1059
|
+
) {
|
|
965
1060
|
app.debug?.(
|
|
966
1061
|
`Notes ${trigger}: ${result.published.length} published, ` +
|
|
967
|
-
`${result.pruned.length} expired
|
|
1062
|
+
`${result.pruned.length} expired, ` +
|
|
1063
|
+
`${result.regionsPublished.length} regions published, ` +
|
|
1064
|
+
`${result.regionsPruned.length} regions expired, ` +
|
|
1065
|
+
`${result.widerPublished.length} wider regions`,
|
|
968
1066
|
);
|
|
969
1067
|
}
|
|
970
1068
|
}
|
|
@@ -1098,6 +1196,16 @@ module.exports = (app) => {
|
|
|
1098
1196
|
* @param {object} payload - Briefing payload, mutated in place
|
|
1099
1197
|
* @returns {Promise<void>}
|
|
1100
1198
|
*/
|
|
1199
|
+
/**
|
|
1200
|
+
* Re-publishes the metarea warnings from the canonical publication
|
|
1201
|
+
* track (work doc #48): the fetch-time publish ran against the
|
|
1202
|
+
* track as it was when the bulletin arrived — the crew has moved
|
|
1203
|
+
* since, and a page load must not wait for the next fetch window.
|
|
1204
|
+
* Content-addressed ids make the re-publish idempotent, and
|
|
1205
|
+
* unchanged sets write nothing.
|
|
1206
|
+
*
|
|
1207
|
+
* @param {{waypoints?: Array<{lat: number, lon: number}>}} payload
|
|
1208
|
+
*/
|
|
1101
1209
|
async function refilterPayloadBulletin(payload) {
|
|
1102
1210
|
if (!payload?.waypoints?.length) {
|
|
1103
1211
|
return;
|
|
@@ -1106,6 +1214,12 @@ module.exports = (app) => {
|
|
|
1106
1214
|
if (fresh) {
|
|
1107
1215
|
payload.metareaBulletin = fresh;
|
|
1108
1216
|
}
|
|
1217
|
+
// The publication track is view-independent — the payload's own
|
|
1218
|
+
// refiltered bulletin stays per-view for the console. The served
|
|
1219
|
+
// briefing's waypoints ARE the route waters: a cached-payload page
|
|
1220
|
+
// load publishes from them even when no compile ran in this
|
|
1221
|
+
// process
|
|
1222
|
+
await publishWarningsFor("serve", payload.waypoints);
|
|
1109
1223
|
}
|
|
1110
1224
|
|
|
1111
1225
|
/**
|
|
@@ -1187,12 +1301,20 @@ module.exports = (app) => {
|
|
|
1187
1301
|
if (notesStore) {
|
|
1188
1302
|
const result = await publishCapAlertNotes({
|
|
1189
1303
|
store: notesStore,
|
|
1304
|
+
regionsStore,
|
|
1190
1305
|
alerts: payload.capAlerts,
|
|
1191
1306
|
});
|
|
1192
|
-
if (
|
|
1307
|
+
if (
|
|
1308
|
+
result.published.length > 0 ||
|
|
1309
|
+
result.pruned.length > 0 ||
|
|
1310
|
+
result.regionsPublished.length > 0 ||
|
|
1311
|
+
result.regionsPruned.length > 0
|
|
1312
|
+
) {
|
|
1193
1313
|
app.debug?.(
|
|
1194
1314
|
`CAP notes: ${result.published.length} published, ` +
|
|
1195
|
-
`${result.pruned.length} expired
|
|
1315
|
+
`${result.pruned.length} expired, ` +
|
|
1316
|
+
`${result.regionsPublished.length} regions published, ` +
|
|
1317
|
+
`${result.regionsPruned.length} regions expired`,
|
|
1196
1318
|
);
|
|
1197
1319
|
}
|
|
1198
1320
|
}
|
|
@@ -1274,7 +1396,7 @@ module.exports = (app) => {
|
|
|
1274
1396
|
if (bulletin) {
|
|
1275
1397
|
payload.metareaBulletin = bulletin;
|
|
1276
1398
|
}
|
|
1277
|
-
await
|
|
1399
|
+
await publishWarningsFor("here");
|
|
1278
1400
|
// Space weather rides the same online window (work doc #3);
|
|
1279
1401
|
// degrades to an absent field when the endpoints fail. Phase-2
|
|
1280
1402
|
// ephemeris events are local computation; the visibility gate
|
|
@@ -1506,6 +1628,7 @@ module.exports = (app) => {
|
|
|
1506
1628
|
}
|
|
1507
1629
|
const waypoints = sampleRoutePoints(coordinates);
|
|
1508
1630
|
lastCompiledWaypoints = waypoints;
|
|
1631
|
+
lastRouteWaypoints = waypoints;
|
|
1509
1632
|
// Bulletins ride the same online window as the weather (SPEC §3.1
|
|
1510
1633
|
// metareaBulletin): pulled first so this briefing carries them
|
|
1511
1634
|
await refreshBulletinsOnline("briefing", waypoints);
|
|
@@ -1518,7 +1641,7 @@ module.exports = (app) => {
|
|
|
1518
1641
|
if (bulletin) {
|
|
1519
1642
|
payload.metareaBulletin = bulletin;
|
|
1520
1643
|
}
|
|
1521
|
-
await
|
|
1644
|
+
await publishWarningsFor("route");
|
|
1522
1645
|
// Space weather for the departure position (work doc #3):
|
|
1523
1646
|
// fetched Phase-1 sources plus local Phase-2 ephemeris events,
|
|
1524
1647
|
// all gated on the payload's own cloud cover; nightly moon and
|
|
@@ -1703,6 +1826,11 @@ module.exports = (app) => {
|
|
|
1703
1826
|
/** Waypoints of the most recent compile (route or here): the
|
|
1704
1827
|
* escalation's track reference between refreshes. */
|
|
1705
1828
|
let lastCompiledWaypoints = null;
|
|
1829
|
+
/** Waypoints of the most recent ROUTE compile: the publication
|
|
1830
|
+
* track's route half (work doc #48) — warnings publish for the
|
|
1831
|
+
* waters the plan crosses plus the vessel's position, whichever
|
|
1832
|
+
* view triggered the pass. */
|
|
1833
|
+
let lastRouteWaypoints = null;
|
|
1706
1834
|
|
|
1707
1835
|
async function runFetch(trigger) {
|
|
1708
1836
|
if (!isOnline()) {
|
|
@@ -2796,9 +2924,10 @@ module.exports = (app) => {
|
|
|
2796
2924
|
modelsPromise.catch(() => {});
|
|
2797
2925
|
|
|
2798
2926
|
// Notes lifecycle (work doc #12): when publication is disabled
|
|
2799
|
-
// the
|
|
2927
|
+
// the stores clear, so chart plotters stop seeing our warnings
|
|
2800
2928
|
if (!publishNotesEnabled) {
|
|
2801
2929
|
clearNotes(notesStore).catch(() => {});
|
|
2930
|
+
clearRegions(regionsStore).catch(() => {});
|
|
2802
2931
|
}
|
|
2803
2932
|
|
|
2804
2933
|
app.subscriptionmanager.subscribe(
|
|
@@ -2859,12 +2988,20 @@ module.exports = (app) => {
|
|
|
2859
2988
|
|
|
2860
2989
|
// The server has no built-in notes provider: the plugin
|
|
2861
2990
|
// registers as the notes resource provider (work doc #12) and
|
|
2862
|
-
// serves the bulletin warnings plus any client-written notes
|
|
2991
|
+
// serves the bulletin warnings plus any client-written notes.
|
|
2992
|
+
// Regions (work doc #48) ride beside the built-in provider: the
|
|
2993
|
+
// resources API merges providers per type, so user-created
|
|
2994
|
+
// regions survive untouched
|
|
2863
2995
|
notesStore = createNotesStore(app.getDataDirPath());
|
|
2864
2996
|
teardownNotesProvider = registerNotesProvider(app, {
|
|
2865
2997
|
id: PLUGIN_ID,
|
|
2866
2998
|
store: notesStore,
|
|
2867
2999
|
});
|
|
3000
|
+
regionsStore = createRegionsStore(app.getDataDirPath());
|
|
3001
|
+
teardownRegionsProvider = registerRegionsProvider(app, {
|
|
3002
|
+
id: PLUGIN_ID,
|
|
3003
|
+
store: regionsStore,
|
|
3004
|
+
});
|
|
2868
3005
|
// Acknowledge put clears the tile's NEW badge
|
|
2869
3006
|
if (typeof app.registerPutHandler === "function") {
|
|
2870
3007
|
app.registerPutHandler(
|
|
@@ -2913,6 +3050,10 @@ module.exports = (app) => {
|
|
|
2913
3050
|
teardownNotesProvider();
|
|
2914
3051
|
teardownNotesProvider = null;
|
|
2915
3052
|
}
|
|
3053
|
+
if (teardownRegionsProvider) {
|
|
3054
|
+
teardownRegionsProvider();
|
|
3055
|
+
teardownRegionsProvider = null;
|
|
3056
|
+
}
|
|
2916
3057
|
if (teardownStatusTiles) {
|
|
2917
3058
|
teardownStatusTiles();
|
|
2918
3059
|
teardownStatusTiles = null;
|