signalk-underkeel 0.3.0 → 0.4.0
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 +28 -0
- package/README.md +23 -9
- package/lib/config-charts.js +34 -0
- package/lib/config-schema.js +4 -6
- package/lib/config.js +11 -0
- package/lib/index-loader.js +29 -19
- package/lib/index-source.js +65 -0
- package/lib/index.js +23 -2
- package/lib/setup-checks.js +8 -2
- package/lib/shared-enc.js +214 -0
- package/lib/spatial/build-source.js +26 -0
- package/lib/spatial/build-worker.js +4 -0
- package/lib/spatial/chart-index.js +14 -4
- package/lib/wfs-service.js +14 -0
- package/lib/wfs-wiring.js +45 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to signalk-underkeel are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-11-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **ENC from Charts Provider** reads the ENC cells signalk-charts-provider-simple (2.16.0 or
|
|
10
|
+
later) shares in its ENC store: one chart set's or converted chart's cells by name, or all
|
|
11
|
+
of them with `*`, in place of **ENC cells directory**. The plugin restarts itself and
|
|
12
|
+
rebuilds the index when the charts provider writes new editions or updates of those cells,
|
|
13
|
+
and the admin form names the collections shared at the moment.
|
|
14
|
+
- When the Charts Provider collection in use has no cells, the reason says why, as
|
|
15
|
+
signalk-charts-provider-simple tells it: a chart set not downloaded yet or downloading, its
|
|
16
|
+
cells being written, or no such collection.
|
|
17
|
+
- **Ask Charts Provider for the ENC around the vessel** (expert, off by default): while all
|
|
18
|
+
of the charts provider's shared ENC is read, each new index box asks it to download the free
|
|
19
|
+
ENC cells of that area, when the charts provider allows such downloads. The cells arrive in
|
|
20
|
+
a chart set of their own and the index is rebuilt with them. An answer that may change
|
|
21
|
+
without the box changing is asked again later.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- The index records the ENC source it was built from: the cells directory, or the Charts
|
|
26
|
+
Provider collection by its id. A cells directory that is gone or empty keeps the saved index
|
|
27
|
+
only when that index was built from it, as an unmounted drive leaves it; switching to a
|
|
28
|
+
source with no cells yet no longer keeps using the previous source's index, and the reason
|
|
29
|
+
says why. Clearing the ENC source leaves no index in use rather than the previous source's,
|
|
30
|
+
again with the reason. An index saved by an earlier version, which records no source, is
|
|
31
|
+
kept as before.
|
|
32
|
+
|
|
5
33
|
## [0.3.0] - 2026-10-28
|
|
6
34
|
|
|
7
35
|
### Changed
|
package/README.md
CHANGED
|
@@ -106,7 +106,20 @@ its `.001`, `.002`, … updates beside it. Copy the exchange set, for example it
|
|
|
106
106
|
folder, under one directory and set **ENC cells directory** to it; `.000` files are found down
|
|
107
107
|
to three folders deep, and down to six below a folder holding the exchange set's `CATALOG.031`,
|
|
108
108
|
so an unzipped Inland ENC download (`<set>/ENC_ROOT/1R/7/1R7WAD07/1R7WAD07.000`) can be pointed
|
|
109
|
-
at as it is.
|
|
109
|
+
at as it is.
|
|
110
|
+
|
|
111
|
+
Cells downloaded with signalk-charts-provider-simple (2.16.0 or later) are read where that
|
|
112
|
+
plugin shares them, in its ENC store: set **ENC from Charts Provider** to one of the chart sets
|
|
113
|
+
or converted charts the admin form lists as shared, or to `*` for all of them. It is used
|
|
114
|
+
instead of **ENC cells directory**. When the charts provider writes new editions or updates of
|
|
115
|
+
those cells, the plugin restarts itself and rebuilds the index from them. When the collection
|
|
116
|
+
has no cells yet, the reason says why, as the charts provider tells it: a chart set not
|
|
117
|
+
downloaded or still downloading, cells being written, or no such collection. With `*` and
|
|
118
|
+
**Ask Charts Provider for the ENC around the vessel** on (an expert setting, off by default),
|
|
119
|
+
each new index box asks the charts provider to download the free ENC cells of that area, when
|
|
120
|
+
its **Download the ENC cells around the vessel** is on too; they arrive in its chart set
|
|
121
|
+
"Around the vessel" and the index is rebuilt with them. The cells are
|
|
122
|
+
read in place with GDAL compiled to WebAssembly
|
|
110
123
|
(gdal3.js), which the plugin installs with it, and the updates are applied as they are read.
|
|
111
124
|
Nothing is converted or written next to the cells. A cell without an `M_COVR` coverage layer
|
|
112
125
|
is rejected. A cell's usage band comes from the third character of its name, else from its
|
|
@@ -167,13 +180,14 @@ There are two places to set the plugin up, and both write the same configuration
|
|
|
167
180
|
|
|
168
181
|

|
|
169
182
|
|
|
170
|
-
| Setting
|
|
171
|
-
|
|
|
172
|
-
| ENC cells directory
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
183
|
+
| Setting | What it does |
|
|
184
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
185
|
+
| ENC cells directory | where the `.000` cells are |
|
|
186
|
+
| ENC from Charts Provider | or a chart set or converted chart whose cells signalk-charts-provider-simple shares, `*` for all of them; used instead of the directory |
|
|
187
|
+
| Boat | draft (empty uses Signal K `design.draft.maximum`) and safety margin under the keel (0 m); only the keel's lowest point counts, and squat and waves are added on top (below) |
|
|
188
|
+
| Water level | source (`none`, `manual`, `signalk-tide`, `fmi`, see below); only the chosen source's fields are shown |
|
|
189
|
+
| Warnings | Early, Balanced or Late, in the table below |
|
|
190
|
+
| Optional features | living chart (own soundings), Traficom WFS depths, grounding detector; each off until enabled |
|
|
177
191
|
|
|
178
192
|
| Warning style | Looks ahead | Alert at | Warn at, within | Alarm at, within | Alarm on negative mean UKC |
|
|
179
193
|
| ------------------ | ----------- | -------- | --------------- | ---------------- | -------------------------- |
|
|
@@ -200,7 +214,7 @@ Expert settings, with their defaults:
|
|
|
200
214
|
|
|
201
215
|
| Group | Settings |
|
|
202
216
|
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
203
|
-
| Chart index | index directory, rebuild at start, index leaf zoom (20), box around the vessel (10 km)
|
|
217
|
+
| Chart index | index directory, rebuild at start, index leaf zoom (20), box around the vessel (10 km), ask Charts Provider for the ENC around the vessel |
|
|
204
218
|
| Boat | length, beam (fallback: Signal K `design.*`); draft σ (0.05 m); block coefficient and heel effect (none, fin keel or a table); double squat in narrow channels; wave allowance factor (0.4 × Hs) |
|
|
205
219
|
| Look-ahead | minimum horizon (60 s); position buffer (10 m, 2σ); fan spread (COG 5°, rate of turn 0.1°/s); escape-heading rate of turn (3°/s); and the distance, probabilities and windows of the warning style, each overriding the style when set |
|
|
206
220
|
| Water level | manual height and σ; FMI refresh (600 s); chart-datum mappings for FMI |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CHART_AREA_FIELD = void 0;
|
|
4
|
+
exports.chartFields = chartFields;
|
|
5
|
+
const shared_enc_1 = require("./shared-enc");
|
|
6
|
+
/**
|
|
7
|
+
* The admin form's chart fields: a directory of S-57 cells, or the cells
|
|
8
|
+
* signalk-charts-provider-simple shares (`shared-enc.ts`).
|
|
9
|
+
*/
|
|
10
|
+
function chartFields(sharedNow) {
|
|
11
|
+
return {
|
|
12
|
+
cellsDir: {
|
|
13
|
+
type: "string",
|
|
14
|
+
title: "ENC cells directory",
|
|
15
|
+
description: "S-57 ENC cells as delivered: .000 files with their .001, .002 … updates, also in sub-folders such as ENC_ROOT. Read directly with GDAL (WebAssembly); nothing is converted."
|
|
16
|
+
},
|
|
17
|
+
chartsProviderEnc: {
|
|
18
|
+
type: "string",
|
|
19
|
+
pattern: shared_enc_1.COLLECTION_CHOICE_PATTERN,
|
|
20
|
+
title: "ENC from Charts Provider",
|
|
21
|
+
description: "Or read the ENC cells signalk-charts-provider-simple keeps: the name of an ENC Charts set there (Gulf of Finland is Gulf_of_Finland) or of a converted chart whose cells it keeps, or * for all of them. Used instead of the ENC cells directory; the index is rebuilt when the cells change." +
|
|
22
|
+
(sharedNow ? ` ${sharedNow}` : "")
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** The expert field asking Charts Provider for the cells around the vessel (`shared-enc.ts`). */
|
|
27
|
+
exports.CHART_AREA_FIELD = {
|
|
28
|
+
chartsProviderArea: {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
title: "Ask Charts Provider for the ENC around the vessel",
|
|
31
|
+
description: 'With ENC from Charts Provider set to * and Charts Provider\'s "Download the ENC cells around the vessel" on, each new index box asks it to download the free ENC cells of that area; they arrive in its chart set "Around the vessel" and the index is rebuilt with them. Off: only the cells already shared are read.',
|
|
32
|
+
default: false
|
|
33
|
+
}
|
|
34
|
+
};
|
package/lib/config-schema.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.adminSchema = adminSchema;
|
|
|
5
5
|
exports.flatten = flatten;
|
|
6
6
|
exports.formsFollowDependencies = formsFollowDependencies;
|
|
7
7
|
const lookahead_1 = require("./engine/lookahead");
|
|
8
|
+
const config_charts_1 = require("./config-charts");
|
|
8
9
|
const ADVISORY = "Advisory navigation aid. Not for navigation; does not replace official charts or a proper lookout.";
|
|
9
10
|
/** Shown under each field whose empty value follows a preset. */
|
|
10
11
|
const FOLLOWS_STYLE = "Empty follows the warning style.";
|
|
@@ -264,7 +265,8 @@ const CHART_INDEX = {
|
|
|
264
265
|
maximum: 22
|
|
265
266
|
},
|
|
266
267
|
boxRadiusKm: boxKm("Index box around the vessel (km)", 10, "Half-width of the area the index is built in around the vessel; it is rebuilt as the vessel moves. Outside it the plugin reports no coverage, and the reason says the vessel is outside the chart box. At speed the box grows past this radius, up to the largest index box, so the vessel cannot reach its edge, less the look-ahead, before the next box is built: a box must hold 1.5 times the distance run during the last build (grown with the build area) plus the look-ahead and a 2.4 km band. It grows at once when too small, and shrinks only at the next rebuild once the speed needs under 70% of it. 0 indexes every cell (slow for large chart sets); a radius under 2 km is raised to 2. With the Traficom WFS on, its radius is used, and the box grows to 25 km at most."),
|
|
267
|
-
boxMaxRadiusKm: boxKm("Largest index box at speed (km)", 0, "The box around the vessel grows up to this half-width at speed. 0 sizes it from the RAM the system reports: 10 km on a 1 GB Raspberry Pi or Cerbo GX, about 19 km on a 4 GB Raspberry Pi 4, about 27 km with 8 GB. The status says when even this box is too small for the speed and build time.")
|
|
268
|
+
boxMaxRadiusKm: boxKm("Largest index box at speed (km)", 0, "The box around the vessel grows up to this half-width at speed. 0 sizes it from the RAM the system reports: 10 km on a 1 GB Raspberry Pi or Cerbo GX, about 19 km on a 4 GB Raspberry Pi 4, about 27 km with 8 GB. The status says when even this box is too small for the speed and build time."),
|
|
269
|
+
...config_charts_1.CHART_AREA_FIELD
|
|
268
270
|
};
|
|
269
271
|
const EXPERT = {
|
|
270
272
|
...CHART_INDEX,
|
|
@@ -301,11 +303,7 @@ function adminSchema(hints = {}) {
|
|
|
301
303
|
type: "object",
|
|
302
304
|
description: `${ADVISORY}${checks}`,
|
|
303
305
|
properties: {
|
|
304
|
-
|
|
305
|
-
type: "string",
|
|
306
|
-
title: "ENC cells directory",
|
|
307
|
-
description: "S-57 ENC cells as delivered: .000 files with their .001, .002 … updates, also in sub-folders such as ENC_ROOT. Read directly with GDAL (WebAssembly); nothing is converted."
|
|
308
|
-
},
|
|
306
|
+
...(0, config_charts_1.chartFields)(hints.encCollections),
|
|
309
307
|
vessel: {
|
|
310
308
|
...VESSEL_ESSENTIAL,
|
|
311
309
|
properties: { ...VESSEL_ESSENTIAL.properties, draft: draftField(hints.signalkDraft) }
|
package/lib/config.js
CHANGED
|
@@ -10,6 +10,7 @@ const route_request_1 = require("./api/route-request");
|
|
|
10
10
|
const wfs_1 = require("./ingest/wfs");
|
|
11
11
|
const config_presets_1 = require("./config-presets");
|
|
12
12
|
const box_sizing_1 = require("./box-sizing");
|
|
13
|
+
const shared_enc_1 = require("./shared-enc");
|
|
13
14
|
var config_schema_1 = require("./config-schema");
|
|
14
15
|
Object.defineProperty(exports, "SCHEMA", { enumerable: true, get: function () { return config_schema_1.SCHEMA; } });
|
|
15
16
|
exports.DEFAULT_ROUTE_CHECK = { speed: 2.5, xteLimit: 30, probability: 0.01 };
|
|
@@ -22,6 +23,14 @@ function optionalPositive(v) {
|
|
|
22
23
|
function str(v) {
|
|
23
24
|
return typeof v === "string" && v.trim() !== "" ? v : undefined;
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* A charts provider collection id or `*`, trimmed. Anything else, a path out of the store
|
|
28
|
+
* among them, is no choice, and the ENC cells directory stays in use.
|
|
29
|
+
*/
|
|
30
|
+
function collectionChoice(v) {
|
|
31
|
+
const s = str(v)?.trim();
|
|
32
|
+
return s !== undefined && (0, shared_enc_1.isCollectionChoice)(s) ? s : undefined;
|
|
33
|
+
}
|
|
25
34
|
function record(v) {
|
|
26
35
|
return typeof v === "object" && v !== null ? v : {};
|
|
27
36
|
}
|
|
@@ -159,6 +168,8 @@ function parseConfig(raw) {
|
|
|
159
168
|
return {
|
|
160
169
|
indexDir: str(r.indexDir),
|
|
161
170
|
cellsDir: str(r.cellsDir),
|
|
171
|
+
chartsProviderEnc: collectionChoice(r.chartsProviderEnc),
|
|
172
|
+
chartsProviderArea: r.chartsProviderArea === true,
|
|
162
173
|
rebuildIndex: r.rebuildIndex === true,
|
|
163
174
|
leafZoom: Math.round(num(r.leafZoom, 20, 14, 22)),
|
|
164
175
|
boxRadiusKm: boxRadius(r.boxRadiusKm),
|
package/lib/index-loader.js
CHANGED
|
@@ -14,6 +14,8 @@ exports.savedLoad = savedLoad;
|
|
|
14
14
|
exports.sourceKeys = sourceKeys;
|
|
15
15
|
const fs_1 = __importDefault(require("fs"));
|
|
16
16
|
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const index_source_1 = require("./index-source");
|
|
18
|
+
const build_source_1 = require("./spatial/build-source");
|
|
17
19
|
const cell_cache_1 = require("./ingest/cell-cache");
|
|
18
20
|
const cells_1 = require("./ingest/cells");
|
|
19
21
|
const cell_source_1 = require("./ingest/cell-source");
|
|
@@ -139,7 +141,8 @@ async function settle(p) {
|
|
|
139
141
|
cells.push(await (0, cell_source_1.readCell)(d, i));
|
|
140
142
|
}
|
|
141
143
|
const index = (0, chart_index_1.buildChartIndex)(cells, { leafZoom: p.build.leafZoom });
|
|
142
|
-
|
|
144
|
+
const { source } = p.build;
|
|
145
|
+
(0, chart_index_1.saveChartIndex)(p.indexDir, index, source ? (dir) => (0, build_source_1.writeSource)(dir, source) : undefined);
|
|
143
146
|
return builtLoad(p, index);
|
|
144
147
|
}
|
|
145
148
|
catch (err) {
|
|
@@ -182,11 +185,11 @@ function scanCells(cellsDir, saved) {
|
|
|
182
185
|
}
|
|
183
186
|
function plan(cfg, indexDir, extraDirs, syncLoadBytes) {
|
|
184
187
|
const { cells: saved, error } = readSaved(indexDir);
|
|
185
|
-
const {
|
|
186
|
-
const fromSaved = (notes, unreadable) =>
|
|
187
|
-
?
|
|
188
|
-
:
|
|
189
|
-
const found = scanCells(cfg.cellsDir, saved);
|
|
188
|
+
const { refused, mismatch } = savedMatches(cfg, indexDir, saved, extraDirs);
|
|
189
|
+
const fromSaved = (notes, unreadable) => refused === undefined
|
|
190
|
+
? savedPlan(indexDir, notes, unreadable, syncLoadBytes)
|
|
191
|
+
: { load: { indexDir, built: false, reason: refused } };
|
|
192
|
+
const found = (0, index_source_1.cellsNow)(scanCells(cfg.cellsDir, saved), cfg, indexDir);
|
|
190
193
|
if ("away" in found) {
|
|
191
194
|
return fromSaved([`${found.away}; using the saved index`], (err) => ({
|
|
192
195
|
load: (0, exports.failedLoad)(indexDir, err)
|
|
@@ -197,7 +200,8 @@ function plan(cfg, indexDir, extraDirs, syncLoadBytes) {
|
|
|
197
200
|
let stamps;
|
|
198
201
|
const encKeys = () => (stamps ??= scan.sources.map(sourceKey));
|
|
199
202
|
const change = cellsChange(cfg.cellsDir, saved, encKeys);
|
|
200
|
-
const
|
|
203
|
+
const away = found.note === undefined ? [] : [found.note];
|
|
204
|
+
const notes = [...away, ...(change ? [change] : []), ...skippedNote(cfg.cellsDir, scan)];
|
|
201
205
|
const sources = Boolean(cfg.cellsDir) || extraDirs.length > 0;
|
|
202
206
|
const dirs = [...scan.sources, ...extraDirs];
|
|
203
207
|
const keys = () => [...encKeys(), ...extraDirs.map(sourceKey)].sort();
|
|
@@ -220,7 +224,7 @@ function plan(cfg, indexDir, extraDirs, syncLoadBytes) {
|
|
|
220
224
|
return boxPlan(cfg, indexDir, dirs, keys, {
|
|
221
225
|
mismatch,
|
|
222
226
|
fromSaved: (why) => fromSaved(why, unreadable),
|
|
223
|
-
notes: skippedNote(cfg.cellsDir, scan)
|
|
227
|
+
notes: [...away, ...skippedNote(cfg.cellsDir, scan)]
|
|
224
228
|
});
|
|
225
229
|
}
|
|
226
230
|
// With no index saved, `mismatch` says so.
|
|
@@ -235,7 +239,7 @@ function plan(cfg, indexDir, extraDirs, syncLoadBytes) {
|
|
|
235
239
|
]);
|
|
236
240
|
}
|
|
237
241
|
if (!saved) {
|
|
238
|
-
return { load: { indexDir, built: false, reason: missingReason(cfg, indexDir
|
|
242
|
+
return { load: { indexDir, built: false, reason: missingReason(cfg, indexDir) } };
|
|
239
243
|
}
|
|
240
244
|
return fromSaved(notes, unreadable);
|
|
241
245
|
}
|
|
@@ -312,23 +316,24 @@ function buildPlan(cfg, indexDir, dirs, notes, box) {
|
|
|
312
316
|
if (refused !== undefined) {
|
|
313
317
|
return { load: (0, exports.failedLoad)(indexDir, new Error(refused)) };
|
|
314
318
|
}
|
|
315
|
-
const
|
|
319
|
+
const source = (0, index_source_1.indexSource)(cfg);
|
|
320
|
+
const build = { dirs, leafZoom: cfg.leafZoom, indexDir, ...(source ? { source } : {}) };
|
|
316
321
|
return { build: box ? { ...build, box } : build, indexDir, notes };
|
|
317
322
|
}
|
|
318
|
-
function missingReason(cfg, indexDir
|
|
319
|
-
if (stale) {
|
|
320
|
-
return "the saved index holds Traficom WFS depths and that source is off; set cellsDir";
|
|
321
|
-
}
|
|
323
|
+
function missingReason(cfg, indexDir) {
|
|
322
324
|
return cfg.wfs ? exports.WFS_WAITING_REASON : `no chart index in ${indexDir}; set cellsDir`;
|
|
323
325
|
}
|
|
324
326
|
/**
|
|
325
|
-
* `
|
|
326
|
-
*
|
|
327
|
-
*
|
|
327
|
+
* `refused`, why the saved index is not used even with nothing to build another from: it holds
|
|
328
|
+
* WFS depths and that source is off, or it records an ENC source and none is set now.
|
|
329
|
+
* `mismatch`, why the saved index is not used as is: none is saved, those, the source is on
|
|
330
|
+
* and the saved index lacks its cell, or the index was saved by an older version of its
|
|
331
|
+
* pyramid or hazard file.
|
|
328
332
|
*/
|
|
329
333
|
function savedMatches(cfg, indexDir, saved, extraDirs) {
|
|
330
334
|
const holds = saved?.some((c) => c.source === wfs_1.WFS_SOURCE) ?? false;
|
|
331
335
|
const stale = holds && !cfg.wfs;
|
|
336
|
+
const dropped = saved && (0, index_source_1.sourceDropped)(cfg, indexDir);
|
|
332
337
|
const mismatch = saved === undefined
|
|
333
338
|
? `no index saved in ${indexDir}`
|
|
334
339
|
: stale
|
|
@@ -337,8 +342,13 @@ function savedMatches(cfg, indexDir, saved, extraDirs) {
|
|
|
337
342
|
? "the saved one is from an older index version or its header is damaged"
|
|
338
343
|
: extraDirs.length > 0 && !holds
|
|
339
344
|
? "the saved one lacks the Traficom WFS cell"
|
|
340
|
-
:
|
|
341
|
-
|
|
345
|
+
: dropped;
|
|
346
|
+
const refused = stale
|
|
347
|
+
? "the saved index holds Traficom WFS depths and that source is off; set cellsDir"
|
|
348
|
+
: dropped && extraDirs.length === 0
|
|
349
|
+
? `not using the saved index: ${dropped}; set cellsDir`
|
|
350
|
+
: undefined;
|
|
351
|
+
return { refused, mismatch };
|
|
342
352
|
}
|
|
343
353
|
function savedCells(indexDir) {
|
|
344
354
|
return (0, cells_1.parseCatalogue)(JSON.parse(fs_1.default.readFileSync(path_1.default.join(indexDir, "cells.json"), "utf8")))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.indexSource = indexSource;
|
|
7
|
+
exports.sourceChanged = sourceChanged;
|
|
8
|
+
exports.cellsNow = cellsNow;
|
|
9
|
+
exports.sourceDropped = sourceDropped;
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const build_source_1 = require("./spatial/build-source");
|
|
12
|
+
/**
|
|
13
|
+
* The ENC source an index was built from, as the settings name it: a cells directory, or a
|
|
14
|
+
* collection signalk-charts-provider-simple shares. Recorded with the index, so a source left
|
|
15
|
+
* without cells can be told apart from the source the index came from: a cells directory that
|
|
16
|
+
* is gone or empty most likely sits on a drive that is not mounted, and its saved index is
|
|
17
|
+
* used meanwhile, while a source the user chose since was never the saved index's.
|
|
18
|
+
*
|
|
19
|
+
* A collection is recorded by its id, not the directory it resolves to: the charts provider
|
|
20
|
+
* may keep its store elsewhere than the default path read while it is still starting.
|
|
21
|
+
*/
|
|
22
|
+
/** The source `cfg` reads its S-57 cells from; undefined without one. */
|
|
23
|
+
function indexSource(cfg) {
|
|
24
|
+
if (cfg.chartsProviderEnc !== undefined) {
|
|
25
|
+
return `charts-provider:${cfg.chartsProviderEnc}`;
|
|
26
|
+
}
|
|
27
|
+
return cfg.cellsDir ? `dir:${path_1.default.resolve(cfg.cellsDir)}` : undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Why the index in `indexDir` was not built from the source `cfg` reads, or undefined when it
|
|
31
|
+
* was, or records no source (an index saved by an older version).
|
|
32
|
+
*/
|
|
33
|
+
function sourceChanged(cfg, indexDir) {
|
|
34
|
+
const saved = (0, build_source_1.readSource)(indexDir);
|
|
35
|
+
const now = indexSource(cfg);
|
|
36
|
+
return saved !== undefined && saved !== now
|
|
37
|
+
? `the saved index was built from ${describe(saved)}, not ${now === undefined ? "the cells now set" : describe(now)}`
|
|
38
|
+
: undefined;
|
|
39
|
+
}
|
|
40
|
+
function describe(source) {
|
|
41
|
+
return source.startsWith("charts-provider:")
|
|
42
|
+
? `Charts Provider's ${source.slice("charts-provider:".length)}`
|
|
43
|
+
: source.slice(source.indexOf(":") + 1);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `found`, a scan of the cells directory or why it says nothing of the saved index, except
|
|
47
|
+
* that cells away from a source the saved index was not built from are none: an unmounted
|
|
48
|
+
* drive keeps the saved index, but only one built from the cells now set.
|
|
49
|
+
*/
|
|
50
|
+
function cellsNow(found, cfg, indexDir) {
|
|
51
|
+
const moved = "away" in found ? sourceChanged(cfg, indexDir) : undefined;
|
|
52
|
+
return "away" in found && moved !== undefined
|
|
53
|
+
? {
|
|
54
|
+
scan: { sources: [], skipped: [] },
|
|
55
|
+
note: `${found.away}; not using the saved index: ${moved}`
|
|
56
|
+
}
|
|
57
|
+
: found;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Why the index in `indexDir` is not this configuration's when `cfg` sets no ENC source at
|
|
61
|
+
* all, as when the source is cleared, though the index records one; undefined otherwise.
|
|
62
|
+
*/
|
|
63
|
+
function sourceDropped(cfg, indexDir) {
|
|
64
|
+
return indexSource(cfg) === undefined ? sourceChanged(cfg, indexDir) : undefined;
|
|
65
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const block_keeper_1 = require("./block-keeper");
|
|
|
23
23
|
const livechart_service_1 = require("./livechart-service");
|
|
24
24
|
const wfs_service_1 = require("./wfs-service");
|
|
25
25
|
const wfs_wiring_1 = require("./wfs-wiring");
|
|
26
|
+
const shared_enc_1 = require("./shared-enc");
|
|
26
27
|
const fs_1 = __importDefault(require("fs"));
|
|
27
28
|
const path_1 = __importDefault(require("path"));
|
|
28
29
|
// lib/index.js and src/index.ts both sit one level below package.json.
|
|
@@ -61,6 +62,20 @@ function stopPlugin(app, clock, state) {
|
|
|
61
62
|
]
|
|
62
63
|
});
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Restarts the plugin once the shared ENC cells it reads changed: a start compares them with the
|
|
67
|
+
* saved index and rebuilds it when they did. A server without a restart callback logs instead.
|
|
68
|
+
*/
|
|
69
|
+
function watchShared(app, clock, collection, restart) {
|
|
70
|
+
return (0, shared_enc_1.watchSharedCells)(collection, clock, () => {
|
|
71
|
+
if (restart) {
|
|
72
|
+
restart();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
app.error("the shared ENC cells changed; restart the plugin to index them");
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
64
79
|
/**
|
|
65
80
|
* The admin form for this server: the essentials with expert settings
|
|
66
81
|
* behind a checkbox where the admin UI forwards `dependencies`, every field
|
|
@@ -87,6 +102,7 @@ module.exports = function underkeel(app, clock = monitor_1.SYSTEM_CLOCK, fetch =
|
|
|
87
102
|
requests: new route_service_1.RequestQueue()
|
|
88
103
|
};
|
|
89
104
|
let startup;
|
|
105
|
+
let shared;
|
|
90
106
|
return {
|
|
91
107
|
id: manifest_1.PLUGIN_ID,
|
|
92
108
|
name: "Under-Keel Awareness",
|
|
@@ -96,10 +112,10 @@ module.exports = function underkeel(app, clock = monitor_1.SYSTEM_CLOCK, fetch =
|
|
|
96
112
|
start(options, restart) {
|
|
97
113
|
const migration = (0, config_migrate_1.migrateConfig)(options);
|
|
98
114
|
(0, settings_service_1.persistMigration)(app, options, migration);
|
|
99
|
-
const config = (0, config_1.parseConfig)(migration.config);
|
|
115
|
+
const config = (0, shared_enc_1.withSharedCells)((0, config_1.parseConfig)(migration.config), app.getDataDirPath());
|
|
100
116
|
const wfs = (0, wfs_wiring_1.startBox)({ app, clock, state, config, fetch, build });
|
|
101
117
|
const plan = (0, index_loader_1.planIndex)(config, app.getDataDirPath(), wfs?.savedCell() ? [wfs.cellDir] : []);
|
|
102
|
-
const load = "load" in plan ? plan.load : (0, index_startup_1.pendingLoad)(plan);
|
|
118
|
+
const load = "load" in plan ? (0, shared_enc_1.noteSharedCells)(plan.load, config) : (0, index_startup_1.pendingLoad)(plan);
|
|
103
119
|
const monitor = new monitor_1.Monitor(app, manifest_1.PLUGIN_ID, config, load.index, load.reason, clock, fetch);
|
|
104
120
|
const acks = new ack_store_1.AckStore(path_1.default.join(app.getDataDirPath(), "acks.json"));
|
|
105
121
|
if (acks.error) {
|
|
@@ -127,10 +143,15 @@ module.exports = function underkeel(app, clock = monitor_1.SYSTEM_CLOCK, fetch =
|
|
|
127
143
|
state.zoneErrors = errors;
|
|
128
144
|
errors.forEach((e) => app.error(e));
|
|
129
145
|
});
|
|
146
|
+
shared = config.chartsProviderEnc
|
|
147
|
+
? watchShared(app, clock, config.chartsProviderEnc, restart && (() => restart(migration.config)))
|
|
148
|
+
: undefined;
|
|
130
149
|
(0, assets_1.mountAssets)(app);
|
|
131
150
|
app.registerResourceProvider((0, manifest_1.manifestProvider)(VERSION));
|
|
132
151
|
},
|
|
133
152
|
stop() {
|
|
153
|
+
shared?.stop();
|
|
154
|
+
shared = undefined;
|
|
134
155
|
startup?.abort();
|
|
135
156
|
startup = undefined;
|
|
136
157
|
stopPlugin(app, clock, state);
|
package/lib/setup-checks.js
CHANGED
|
@@ -14,6 +14,7 @@ const memory_1 = require("./foundation/memory");
|
|
|
14
14
|
const public_text_1 = require("./foundation/public-text");
|
|
15
15
|
const water_level_1 = require("./model/water-level");
|
|
16
16
|
const nav_inputs_1 = require("./nav-inputs");
|
|
17
|
+
const shared_enc_1 = require("./shared-enc");
|
|
17
18
|
function hostFacts() {
|
|
18
19
|
const memory = (0, memory_1.usableMemory)();
|
|
19
20
|
return { memory, available: Math.min(os_1.default.freemem(), memory), cpus: os_1.default.availableParallelism() };
|
|
@@ -64,7 +65,11 @@ function chartsCheck(s, facts) {
|
|
|
64
65
|
return { id: "charts", level: "ok", text: `Charts: ${facts.cells} cells indexed` };
|
|
65
66
|
}
|
|
66
67
|
if (!s.config?.cellsDir && !s.config?.wfs) {
|
|
67
|
-
return {
|
|
68
|
+
return {
|
|
69
|
+
id: "charts",
|
|
70
|
+
level: "todo",
|
|
71
|
+
text: "Charts: set the ENC cells directory or ENC from Charts Provider"
|
|
72
|
+
};
|
|
68
73
|
}
|
|
69
74
|
const reason = (0, public_text_1.publicReason)(s.load?.reason) ?? "no chart index loaded";
|
|
70
75
|
return { id: "charts", level: "warn", text: `Charts: ${reason}` };
|
|
@@ -177,6 +182,7 @@ function schemaHints(app, s) {
|
|
|
177
182
|
return {
|
|
178
183
|
checks: setupChecks(s, facts).map((c) => `${MARK[c.level]} ${c.text}`),
|
|
179
184
|
signalkDraft: facts.signalk.draft,
|
|
180
|
-
source: sourceHint(facts)
|
|
185
|
+
source: sourceHint(facts),
|
|
186
|
+
encCollections: (0, shared_enc_1.collectionsHint)()
|
|
181
187
|
};
|
|
182
188
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.COLLECTION_CHOICE_PATTERN = exports.ALL_COLLECTIONS = exports.CHARTS_PROVIDER_ID = void 0;
|
|
7
|
+
exports.isCollectionChoice = isCollectionChoice;
|
|
8
|
+
exports.sharedEncApi = sharedEncApi;
|
|
9
|
+
exports.sharedEncRoot = sharedEncRoot;
|
|
10
|
+
exports.sharedCellsDir = sharedCellsDir;
|
|
11
|
+
exports.withSharedCells = withSharedCells;
|
|
12
|
+
exports.collectionsHint = collectionsHint;
|
|
13
|
+
exports.sharedCellsNote = sharedCellsNote;
|
|
14
|
+
exports.noteSharedCells = noteSharedCells;
|
|
15
|
+
exports.requestSharedArea = requestSharedArea;
|
|
16
|
+
exports.watchSharedCells = watchSharedCells;
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
/**
|
|
19
|
+
* ENC cells shared by signalk-charts-provider-simple. Its ENC store keeps the
|
|
20
|
+
* cells behind its ENC charts as delivered, one folder per collection (an
|
|
21
|
+
* ENC Charts set's cells, or a converted chart's):
|
|
22
|
+
* `<root>/<collection>/<CELL>/<CELL>.000` with the updates beside it. The
|
|
23
|
+
* plugin reads a collection, or the whole store, as its cells directory,
|
|
24
|
+
* and is told when the cells change. See that plugin's docs/enc-store.md.
|
|
25
|
+
*/
|
|
26
|
+
exports.CHARTS_PROVIDER_ID = "signalk-charts-provider-simple";
|
|
27
|
+
/** Every collection the charts provider shares. */
|
|
28
|
+
exports.ALL_COLLECTIONS = "*";
|
|
29
|
+
const COLLECTION_ID = /^[A-Za-z0-9_-]{1,80}$/;
|
|
30
|
+
/** A collection choice for the admin form, with the space around it that parsing trims. */
|
|
31
|
+
exports.COLLECTION_CHOICE_PATTERN = "^\\s*(\\*|[A-Za-z0-9_-]{1,80})?\\s*$";
|
|
32
|
+
/** A collection's id, or ALL_COLLECTIONS. */
|
|
33
|
+
function isCollectionChoice(value) {
|
|
34
|
+
return value === exports.ALL_COLLECTIONS || COLLECTION_ID.test(value);
|
|
35
|
+
}
|
|
36
|
+
/** The charts provider's ENC store API, while that plugin runs in this server. */
|
|
37
|
+
function sharedEncApi(scope = globalThis) {
|
|
38
|
+
const api = scope.__signalk_chartsProviderEnc;
|
|
39
|
+
if (typeof api !== "object" || api === null) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const a = api;
|
|
43
|
+
return a.apiVersion === 1 &&
|
|
44
|
+
typeof a.root === "string" &&
|
|
45
|
+
typeof a.collections === "function" &&
|
|
46
|
+
typeof a.onChange === "function"
|
|
47
|
+
? a
|
|
48
|
+
: undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The charts provider's ENC store: where its API says, else where that plugin keeps it,
|
|
52
|
+
* beside this plugin's data directory, so the cells are found while it is still starting.
|
|
53
|
+
*/
|
|
54
|
+
function sharedEncRoot(dataDir, api = sharedEncApi()) {
|
|
55
|
+
return api?.root ?? path_1.default.join(path_1.default.dirname(dataDir), exports.CHARTS_PROVIDER_ID, "enc-store");
|
|
56
|
+
}
|
|
57
|
+
/** The directory of a collection's cells; the whole store for ALL_COLLECTIONS. */
|
|
58
|
+
function sharedCellsDir(dataDir, collection, api = sharedEncApi()) {
|
|
59
|
+
const root = sharedEncRoot(dataDir, api);
|
|
60
|
+
return collection === exports.ALL_COLLECTIONS ? root : path_1.default.join(root, collection);
|
|
61
|
+
}
|
|
62
|
+
/** `cfg` reading its cells from the charts provider's collection when it names one. */
|
|
63
|
+
function withSharedCells(cfg, dataDir, api = sharedEncApi()) {
|
|
64
|
+
return cfg.chartsProviderEnc === undefined
|
|
65
|
+
? cfg
|
|
66
|
+
: { ...cfg, cellsDir: sharedCellsDir(dataDir, cfg.chartsProviderEnc, api) };
|
|
67
|
+
}
|
|
68
|
+
/** One line naming the collections the charts provider shares, for the admin form. */
|
|
69
|
+
function collectionsHint(api = sharedEncApi()) {
|
|
70
|
+
if (!api) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
let collections;
|
|
74
|
+
try {
|
|
75
|
+
collections = api.collections();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return collections.length === 0
|
|
81
|
+
? "Charts Provider shares no ENC cells yet: download an ENC Charts set there."
|
|
82
|
+
: `Shared now: ${collections
|
|
83
|
+
.map((c) => `${c.id} (${c.kind === "chart-set" ? "chart set" : "converted chart"}, ${c.cellCount} cells)`)
|
|
84
|
+
.join(", ")}.`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Why the charts provider's `collection` holds no cells to read, as it tells it; undefined
|
|
88
|
+
* when it is ready, or the provider cannot tell.
|
|
89
|
+
*/
|
|
90
|
+
function sharedCellsNote(collection, api = sharedEncApi()) {
|
|
91
|
+
if (!api) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
if (collection === exports.ALL_COLLECTIONS) {
|
|
95
|
+
return collectionsHint(api)?.startsWith("Charts Provider shares no")
|
|
96
|
+
? "Charts Provider shares no ENC cells yet: download an ENC Charts set there"
|
|
97
|
+
: undefined;
|
|
98
|
+
}
|
|
99
|
+
let found;
|
|
100
|
+
try {
|
|
101
|
+
found = api.state?.(collection);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
switch (found?.state) {
|
|
107
|
+
case "writing":
|
|
108
|
+
return `Charts Provider is writing the cells of ${collection}; the index is built once they are in place`;
|
|
109
|
+
case "pending":
|
|
110
|
+
return found.detail === "downloading"
|
|
111
|
+
? `Charts Provider is downloading ${collection}; the index is built once its cells are shared`
|
|
112
|
+
: `${collection} is a Charts Provider chart set not downloaded yet: download it in its ENC Charts tab`;
|
|
113
|
+
case "absent":
|
|
114
|
+
return `Charts Provider shares no ${collection}: check ENC from Charts Provider`;
|
|
115
|
+
default:
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** `load` with the charts provider's word on why the collection in use has no cells. */
|
|
120
|
+
function noteSharedCells(load, cfg, api = sharedEncApi()) {
|
|
121
|
+
const note = load.reason !== undefined && cfg.chartsProviderEnc !== undefined
|
|
122
|
+
? sharedCellsNote(cfg.chartsProviderEnc, api)
|
|
123
|
+
: undefined;
|
|
124
|
+
return note === undefined ? load : { ...load, reason: `${load.reason ?? ""}; ${note}` };
|
|
125
|
+
}
|
|
126
|
+
/** Asks the charts provider for the cells around `bbox`; undefined when it cannot be asked. */
|
|
127
|
+
function requestSharedArea(bbox, api = sharedEncApi()) {
|
|
128
|
+
try {
|
|
129
|
+
return api?.requestArea?.(bbox, "signalk-underkeel");
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
return { accepted: false, started: false, reason: err.message };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Calls `onChange` once the cells of `collection` (any, for ALL_COLLECTIONS) changed and
|
|
137
|
+
* settled. While the charts provider has not published its API, looks for it every `pollMs`,
|
|
138
|
+
* and after `waitMs` every `slowPollMs` for as long as the watch runs, since it may start any
|
|
139
|
+
* time; meanwhile the cells are read as found at start. An API that appears later counts as
|
|
140
|
+
* a change: the provider may have written cells meanwhile, or keep its store elsewhere than
|
|
141
|
+
* the default path the cells were read from, and the restart rebuilds the index only when
|
|
142
|
+
* the cells it finds differ.
|
|
143
|
+
*/
|
|
144
|
+
function watchSharedCells(collection, clock, onChange, opts = {}) {
|
|
145
|
+
const settleMs = opts.settleMs ?? 5_000;
|
|
146
|
+
const waitMs = opts.waitMs ?? 120_000;
|
|
147
|
+
const pollMs = opts.pollMs ?? 5_000;
|
|
148
|
+
const slowPollMs = opts.slowPollMs ?? 60_000;
|
|
149
|
+
const started = clock.now();
|
|
150
|
+
let unsubscribe;
|
|
151
|
+
let changedAt;
|
|
152
|
+
let timer;
|
|
153
|
+
let stopped = false;
|
|
154
|
+
let slow = false;
|
|
155
|
+
const stopTimer = () => {
|
|
156
|
+
if (timer !== undefined) {
|
|
157
|
+
clock.clearInterval(timer);
|
|
158
|
+
timer = undefined;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const changed = () => {
|
|
162
|
+
changedAt = clock.now();
|
|
163
|
+
timer ??= clock.setInterval(tick, pollMs);
|
|
164
|
+
};
|
|
165
|
+
const subscribe = (late) => {
|
|
166
|
+
const api = sharedEncApi(opts.scope);
|
|
167
|
+
if (!api) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
unsubscribe = api.onChange((id) => {
|
|
171
|
+
if (collection === exports.ALL_COLLECTIONS || id === collection) {
|
|
172
|
+
changed();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (late) {
|
|
176
|
+
stopTimer();
|
|
177
|
+
changed();
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
};
|
|
181
|
+
function tick() {
|
|
182
|
+
if (stopped) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!unsubscribe && !subscribe(true)) {
|
|
186
|
+
if (!slow && clock.now() - started >= waitMs) {
|
|
187
|
+
slow = true;
|
|
188
|
+
stopTimer();
|
|
189
|
+
timer = clock.setInterval(tick, slowPollMs);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (changedAt !== undefined && clock.now() - changedAt >= settleMs) {
|
|
194
|
+
changedAt = undefined;
|
|
195
|
+
stopTimer();
|
|
196
|
+
onChange();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (unsubscribe && changedAt === undefined) {
|
|
200
|
+
stopTimer();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!subscribe(false)) {
|
|
204
|
+
timer = clock.setInterval(tick, pollMs);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
stop() {
|
|
208
|
+
stopped = true;
|
|
209
|
+
stopTimer();
|
|
210
|
+
unsubscribe?.();
|
|
211
|
+
unsubscribe = undefined;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SOURCE_FILE = void 0;
|
|
7
|
+
exports.writeSource = writeSource;
|
|
8
|
+
exports.readSource = readSource;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const durable_file_1 = require("../foundation/durable-file");
|
|
12
|
+
/** Where an index records the ENC source it was built from (`index-source.ts`). */
|
|
13
|
+
exports.SOURCE_FILE = "source.json";
|
|
14
|
+
function writeSource(staging, source) {
|
|
15
|
+
(0, durable_file_1.writeFileDurable)(path_1.default.join(staging, exports.SOURCE_FILE), JSON.stringify({ version: 1, source }));
|
|
16
|
+
}
|
|
17
|
+
/** The source the index in `dir` was built from; undefined when it records none. */
|
|
18
|
+
function readSource(dir) {
|
|
19
|
+
try {
|
|
20
|
+
const json = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, exports.SOURCE_FILE), "utf8"));
|
|
21
|
+
return json.version === 1 && typeof json.source === "string" ? json.source : undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -28,6 +28,7 @@ const pyramid_regions_1 = require("./pyramid-regions");
|
|
|
28
28
|
const store_1 = require("./store");
|
|
29
29
|
const geo_1 = require("../foundation/geo");
|
|
30
30
|
const build_box_1 = require("./build-box");
|
|
31
|
+
const build_source_1 = require("./build-source");
|
|
31
32
|
const build_progress_1 = require("./build-progress");
|
|
32
33
|
const parsed_cells_1 = require("./parsed-cells");
|
|
33
34
|
const region_jobs_1 = require("./region-jobs");
|
|
@@ -207,6 +208,9 @@ function stage(job, index) {
|
|
|
207
208
|
if (job.box) {
|
|
208
209
|
(0, build_box_1.writeBox)(staging, { ...job.box, leafZoom: job.leafZoom });
|
|
209
210
|
}
|
|
211
|
+
if (job.source !== undefined) {
|
|
212
|
+
(0, build_source_1.writeSource)(staging, job.source);
|
|
213
|
+
}
|
|
210
214
|
if (job.box && index.tiles) {
|
|
211
215
|
const { minZoom, leafZoom } = index.pyramid;
|
|
212
216
|
(0, tile_reuse_1.writeTiles)(staging, { bbox: job.box.bbox, minZoom, leafZoom, ...index.tiles });
|
|
@@ -78,8 +78,8 @@ function syncDirectory(dir) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
|
-
* The files an index directory holds: these three, the build's reach and
|
|
82
|
-
* (`build-reuse.ts`, `build-box.ts`) beside them, and `hazards.json` of an
|
|
81
|
+
* The files an index directory holds: these three, the build's reach, box and
|
|
82
|
+
* source (`build-reuse.ts`, `build-box.ts`, `build-source.ts`) beside them, and `hazards.json` of an
|
|
83
83
|
* index saved before `hazards.bin`, which a rebuild replaces.
|
|
84
84
|
*/
|
|
85
85
|
const INDEX_FILES = new Set([
|
|
@@ -87,6 +87,7 @@ const INDEX_FILES = new Set([
|
|
|
87
87
|
"hazards.json",
|
|
88
88
|
"reach.json",
|
|
89
89
|
"box.json",
|
|
90
|
+
"source.json",
|
|
90
91
|
"tiles.json"
|
|
91
92
|
]);
|
|
92
93
|
/** The first entry of directory `dir` that is not an index file. */
|
|
@@ -191,9 +192,18 @@ function commitChartIndex(dir, staging) {
|
|
|
191
192
|
fs_1.default.rmSync(previous, { recursive: true, force: true });
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
|
-
|
|
195
|
+
/** Saves `index` in `dir`, `extra` adding files to the staged copy first. */
|
|
196
|
+
function saveChartIndex(dir, index, extra) {
|
|
195
197
|
dir = path_1.default.resolve(dir);
|
|
196
|
-
|
|
198
|
+
const staging = stageChartIndex(dir, index);
|
|
199
|
+
try {
|
|
200
|
+
extra?.(staging);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
fs_1.default.rmSync(staging, { recursive: true, force: true });
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
commitChartIndex(dir, staging);
|
|
197
207
|
}
|
|
198
208
|
function isCompleteIndex(dir) {
|
|
199
209
|
return Object.values(FILES).every((f) => fs_1.default.existsSync(path_1.default.join(dir, f)));
|
package/lib/wfs-service.js
CHANGED
|
@@ -53,6 +53,8 @@ class WfsService {
|
|
|
53
53
|
indexed;
|
|
54
54
|
/** The box of the last index this service handed over in this run. */
|
|
55
55
|
handed;
|
|
56
|
+
/** The box `onBox` is to be told of again, and when. */
|
|
57
|
+
again;
|
|
56
58
|
/** The saved WFS cell was missing or unreadable at start: the box is downloaded again. */
|
|
57
59
|
cellLost = false;
|
|
58
60
|
busy = false;
|
|
@@ -263,6 +265,15 @@ class WfsService {
|
|
|
263
265
|
this.ceilingKm = Math.max(min, Math.floor((radiusKm * 0.75) / box_sizing_1.STEP_KM) * box_sizing_1.STEP_KM);
|
|
264
266
|
}
|
|
265
267
|
}
|
|
268
|
+
announce(bbox, now) {
|
|
269
|
+
const ms = this.deps.onBox?.(bbox);
|
|
270
|
+
this.again = ms === undefined ? undefined : { bbox, at: now + ms };
|
|
271
|
+
}
|
|
272
|
+
announceAgain(now) {
|
|
273
|
+
if (this.again && now >= this.again.at) {
|
|
274
|
+
this.announce(this.again.bbox, now);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
266
277
|
/** Where the vessel will be when a refresh started now is handed over, kept inside the box. */
|
|
267
278
|
centreAhead(pos, radiusKm, refreshMs) {
|
|
268
279
|
const m = this.deps.motion();
|
|
@@ -275,6 +286,7 @@ class WfsService {
|
|
|
275
286
|
async check() {
|
|
276
287
|
const pos = this.deps.position();
|
|
277
288
|
const now = this.deps.clock.now();
|
|
289
|
+
this.announceAgain(now);
|
|
278
290
|
if (!pos || !this.due(pos, now) || this.refused(now)) {
|
|
279
291
|
return;
|
|
280
292
|
}
|
|
@@ -287,6 +299,7 @@ class WfsService {
|
|
|
287
299
|
try {
|
|
288
300
|
const centre = this.centreAhead(pos, radiusKm, run.estimateMs ?? this.refreshMs());
|
|
289
301
|
const bbox = (0, wfs_1.boxAround)(centre, radiusKm);
|
|
302
|
+
this.announce(bbox, now);
|
|
290
303
|
const config = this.deps.config;
|
|
291
304
|
const { inBox, cellsDir } = this.deps;
|
|
292
305
|
// With WFS depths alone in use an empty cells directory loses nothing.
|
|
@@ -309,6 +322,7 @@ class WfsService {
|
|
|
309
322
|
leafZoom: this.deps.leafZoom,
|
|
310
323
|
indexDir: this.deps.indexDir,
|
|
311
324
|
...(this.deps.cacheDir === undefined ? {} : { cacheDir: this.deps.cacheDir }),
|
|
325
|
+
source: this.deps.source,
|
|
312
326
|
...(inBox ? {} : { changed: [this.cellDir] })
|
|
313
327
|
}
|
|
314
328
|
}, controller.signal, (progress) => {
|
package/lib/wfs-wiring.js
CHANGED
|
@@ -3,12 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AREA_ASK_MAX_MS = exports.AREA_ASK_MIN_MS = void 0;
|
|
6
7
|
exports.startBox = startBox;
|
|
8
|
+
exports.areaRequests = areaRequests;
|
|
7
9
|
exports.startWfs = startWfs;
|
|
8
10
|
const fs_1 = __importDefault(require("fs"));
|
|
9
11
|
const path_1 = __importDefault(require("path"));
|
|
10
12
|
const geo_1 = require("./foundation/geo");
|
|
11
13
|
const index_loader_1 = require("./index-loader");
|
|
14
|
+
const index_source_1 = require("./index-source");
|
|
15
|
+
const shared_enc_1 = require("./shared-enc");
|
|
12
16
|
const wfs_1 = require("./ingest/wfs");
|
|
13
17
|
const livechart_service_1 = require("./livechart-service");
|
|
14
18
|
const rebind_1 = require("./livechart/rebind");
|
|
@@ -79,6 +83,45 @@ function startBox(w) {
|
|
|
79
83
|
? startWfs(w, { radiusKm: boxRadiusKm }, "chart-box")
|
|
80
84
|
: undefined;
|
|
81
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* What each new box tells the charts provider: asks for its cells with `chartsProviderArea`
|
|
88
|
+
* on, and only while the source is `*`, as they arrive in a chart set of their own.
|
|
89
|
+
*/
|
|
90
|
+
function areaRequests(config, app) {
|
|
91
|
+
return config.chartsProviderArea && config.chartsProviderEnc === shared_enc_1.ALL_COLLECTIONS
|
|
92
|
+
? askForArea(app)
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
|
95
|
+
/** How soon, and how late, a box is asked about again: Charts Provider starting, or busy. */
|
|
96
|
+
exports.AREA_ASK_MIN_MS = 60_000;
|
|
97
|
+
exports.AREA_ASK_MAX_MS = 30 * 60_000;
|
|
98
|
+
const ASK_STARTING_MS = 5 * 60_000;
|
|
99
|
+
/**
|
|
100
|
+
* Asks the charts provider for each box's cells, logging its answer when it changes, and says
|
|
101
|
+
* when to ask again: when the provider says so, or is not running yet.
|
|
102
|
+
*/
|
|
103
|
+
function askForArea(app) {
|
|
104
|
+
let last;
|
|
105
|
+
return (bbox) => {
|
|
106
|
+
const answer = (0, shared_enc_1.requestSharedArea)(bbox);
|
|
107
|
+
const line = answer === undefined
|
|
108
|
+
? "Charts Provider cannot be asked for the ENC around the vessel: it is not running, or too old"
|
|
109
|
+
: `Charts Provider, asked for the ENC around the vessel: ${answer.reason}`;
|
|
110
|
+
if (line !== last) {
|
|
111
|
+
last = line;
|
|
112
|
+
if (answer?.accepted === true) {
|
|
113
|
+
app.debug(line);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
app.error(line);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const ms = answer === undefined ? ASK_STARTING_MS : answer.retryAfterMs;
|
|
120
|
+
return typeof ms === "number" && Number.isFinite(ms)
|
|
121
|
+
? Math.min(exports.AREA_ASK_MAX_MS, Math.max(exports.AREA_ASK_MIN_MS, ms))
|
|
122
|
+
: undefined;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
82
125
|
function startWfs(w, config, dirName = "wfs") {
|
|
83
126
|
const dir = path_1.default.join(w.app.getDataDirPath(), dirName);
|
|
84
127
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
@@ -88,6 +131,8 @@ function startWfs(w, config, dirName = "wfs") {
|
|
|
88
131
|
indexDir: w.config.indexDir ?? path_1.default.join(w.app.getDataDirPath(), "index"),
|
|
89
132
|
cacheDir: (0, index_loader_1.cellCacheDir)(w.app.getDataDirPath()),
|
|
90
133
|
leafZoom: w.config.leafZoom,
|
|
134
|
+
source: (0, index_source_1.indexSource)(w.config),
|
|
135
|
+
onBox: areaRequests(w.config, w.app),
|
|
91
136
|
cellsDir: w.config.cellsDir,
|
|
92
137
|
inBox: w.config.boxRadiusKm > 0,
|
|
93
138
|
index: () => w.state.load?.index,
|
package/package.json
CHANGED