signalk-underkeel 0.3.0 → 0.5.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 +69 -0
- package/README.md +40 -23
- package/lib/config-charts.js +26 -0
- package/lib/config-migrate.js +24 -2
- package/lib/config-schema.js +2 -5
- package/lib/config.js +2 -1
- package/lib/engine/chart-box.js +7 -5
- package/lib/index-loader.js +38 -24
- package/lib/index-source.js +72 -0
- package/lib/index.js +25 -2
- package/lib/ingest/cell-source.js +60 -6
- package/lib/ingest/enc-wasm.js +144 -14
- package/lib/settings-service.js +13 -3
- package/lib/setup-checks.js +15 -4
- package/lib/shared-enc.js +252 -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 +31 -1
- package/lib/wfs-wiring.js +89 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to signalk-underkeel are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.5.0] - 2026-11-09
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Breaking:** ENC cells come only from a chart provider service. **ENC cells directory**,
|
|
10
|
+
**ENC from Charts Provider** and **Ask Charts Provider for the ENC around the vessel** are
|
|
11
|
+
replaced by one pulldown, **Charts provider**, which chooses the service:
|
|
12
|
+
signalk-charts-provider-simple. Every ENC cell it shares is read, and the pulldown marks it
|
|
13
|
+
when it is not running and says what it shares. Cells of your own reach the index by
|
|
14
|
+
converting them in the charts provider with its **Keep the ENC cells of converted charts**
|
|
15
|
+
on.
|
|
16
|
+
- Each new index box asks the chosen charts provider for the ENC cells of its area, without a
|
|
17
|
+
setting of its own; its **Download the ENC cells around the vessel** decides whether it
|
|
18
|
+
downloads them (signalk-charts-provider-simple 2.18.0 or later).
|
|
19
|
+
- Where the vessel has no chart coverage, the reason adds what the charts provider answered
|
|
20
|
+
for the box: it is downloading the area, it has no ENC for this area (by the cells it has
|
|
21
|
+
stored there, with its reason), it is not running, or it is too old to be asked.
|
|
22
|
+
- The configuration moves to version 3. The migration drops the cells directory and the two
|
|
23
|
+
Charts Provider settings, chooses the charts provider when a collection was named, and
|
|
24
|
+
keeps the version 2 configuration as `config-v2.json` in the plugin data directory. When it
|
|
25
|
+
drops a cells directory and no charts provider takes its place, the server log says so, and
|
|
26
|
+
the setup check says no ENC is read until one is chosen.
|
|
27
|
+
|
|
28
|
+
- Of a cell two charts provider collections hold, the index reads the copy of the newest
|
|
29
|
+
edition and update, as the cell's DSID and its update files give them, rather than the first
|
|
30
|
+
in path order. A copy counts only the updates GDAL applies to it, and a copy GDAL cannot
|
|
31
|
+
read ranks below every copy it can.
|
|
32
|
+
- An index saved from one Charts Provider collection is the whole charts provider's, so it
|
|
33
|
+
stays in use after the upgrade while the charts provider's store is away.
|
|
34
|
+
- With no index yet, a failed box build's reason adds what the charts provider said of the
|
|
35
|
+
box, such as that it is downloading the area.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- A failing area request to the charts provider is logged and no longer fails the box
|
|
40
|
+
refresh that made it, and is asked again after 5 minutes rather than when the box moves.
|
|
41
|
+
An area request that fails in the plugin itself is asked again after 5 minutes too.
|
|
42
|
+
- The no-coverage reason says the charts provider is not running once it stops, rather than
|
|
43
|
+
repeating its last answer; and with no index, the reason says so when it was not running at
|
|
44
|
+
start.
|
|
45
|
+
|
|
46
|
+
## [0.4.0] - 2026-11-02
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **ENC from Charts Provider** reads the ENC cells signalk-charts-provider-simple (2.16.0 or
|
|
51
|
+
later) shares in its ENC store: one chart set's or converted chart's cells by name, or all
|
|
52
|
+
of them with `*`, in place of **ENC cells directory**. The plugin restarts itself and
|
|
53
|
+
rebuilds the index when the charts provider writes new editions or updates of those cells,
|
|
54
|
+
and the admin form names the collections shared at the moment.
|
|
55
|
+
- When the Charts Provider collection in use has no cells, the reason says why, as
|
|
56
|
+
signalk-charts-provider-simple tells it: a chart set not downloaded yet or downloading, its
|
|
57
|
+
cells being written, or no such collection.
|
|
58
|
+
- **Ask Charts Provider for the ENC around the vessel** (expert, off by default): while all
|
|
59
|
+
of the charts provider's shared ENC is read, each new index box asks it to download the free
|
|
60
|
+
ENC cells of that area, when the charts provider allows such downloads. The cells arrive in
|
|
61
|
+
a chart set of their own and the index is rebuilt with them. An answer that may change
|
|
62
|
+
without the box changing is asked again later.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- The index records the ENC source it was built from: the cells directory, or the Charts
|
|
67
|
+
Provider collection by its id. A cells directory that is gone or empty keeps the saved index
|
|
68
|
+
only when that index was built from it, as an unmounted drive leaves it; switching to a
|
|
69
|
+
source with no cells yet no longer keeps using the previous source's index, and the reason
|
|
70
|
+
says why. Clearing the ENC source leaves no index in use rather than the previous source's,
|
|
71
|
+
again with the reason. An index saved by an earlier version, which records no source, is
|
|
72
|
+
kept as before.
|
|
73
|
+
|
|
5
74
|
## [0.3.0] - 2026-10-28
|
|
6
75
|
|
|
7
76
|
### Changed
|
package/README.md
CHANGED
|
@@ -102,11 +102,25 @@ shows the RAM and CPUs it finds and says the same; it never changes a setting.
|
|
|
102
102
|
## Prepare the charts
|
|
103
103
|
|
|
104
104
|
The plugin reads unencrypted S-57 ENC cells as they are delivered: each `.000` base cell with
|
|
105
|
-
its `.001`, `.002`, … updates beside it.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
its `.001`, `.002`, … updates beside it. It reads them from a chart provider service, chosen in
|
|
106
|
+
**Charts provider**: signalk-charts-provider-simple, which keeps the cells behind its ENC
|
|
107
|
+
charts in its ENC store (2.16.0 or later; 2.18.0 or later for the area requests below). Every
|
|
108
|
+
cell it shares is read: its ENC Charts sets, and the charts it converted from an upload or a
|
|
109
|
+
folder on the server while its **Keep the ENC cells of converted charts** is on, so an
|
|
110
|
+
exchange set or an Inland ENC download goes in by converting it there. The plugin keeps no
|
|
111
|
+
cells directory of its own. The pulldown marks a provider that is not running and says what
|
|
112
|
+
it shares now. When the charts provider writes new editions or updates of those cells, the
|
|
113
|
+
plugin restarts itself and rebuilds the index from them.
|
|
114
|
+
|
|
115
|
+
Each new index box asks the charts provider for the ENC cells of its area. With its
|
|
116
|
+
**Download the ENC cells around the vessel** on, it downloads the free ENC cells there into its
|
|
117
|
+
chart set "Around the vessel" and the index is rebuilt with them. Where the vessel has no
|
|
118
|
+
chart coverage, the reason says what the charts provider answered: it is downloading the
|
|
119
|
+
area, it has no ENC for this area (with its reason, such as no free ENC covering it), it is
|
|
120
|
+
not running, or it is too old to be asked. Traficom WFS depths (below) are downloaded
|
|
121
|
+
whatever ENC the charts provider has, and with none at all.
|
|
122
|
+
The cells are
|
|
123
|
+
read in place with GDAL compiled to WebAssembly
|
|
110
124
|
(gdal3.js), which the plugin installs with it, and the updates are applied as they are read.
|
|
111
125
|
Nothing is converted or written next to the cells. A cell without an `M_COVR` coverage layer
|
|
112
126
|
is rejected. A cell's usage band comes from the third character of its name, else from its
|
|
@@ -147,7 +161,7 @@ rebuilds in 12–15 s instead of 30–34 s on x86. Deleting the folder is safe;
|
|
|
147
161
|
read again.
|
|
148
162
|
|
|
149
163
|
Folders of GeoJSON layers exported by `scripts/enc-to-geojson.sh` for earlier versions are no
|
|
150
|
-
longer read:
|
|
164
|
+
longer read: convert the original `.000` cells in the charts provider instead. Such a folder
|
|
151
165
|
named like a cell is listed in `GET /status` as skipped.
|
|
152
166
|
|
|
153
167
|
## Settings
|
|
@@ -167,13 +181,13 @@ There are two places to set the plugin up, and both write the same configuration
|
|
|
167
181
|
|
|
168
182
|

|
|
169
183
|
|
|
170
|
-
| Setting
|
|
171
|
-
|
|
|
172
|
-
|
|
|
173
|
-
| Boat
|
|
174
|
-
| Water level
|
|
175
|
-
| Warnings
|
|
176
|
-
| Optional features
|
|
184
|
+
| Setting | What it does |
|
|
185
|
+
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
186
|
+
| Charts provider | the chart provider service whose ENC cells are read: signalk-charts-provider-simple; empty: no ENC |
|
|
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
|
| ------------------ | ----------- | -------- | --------------- | ---------------- | -------------------------- |
|
|
@@ -211,9 +225,12 @@ Expert settings, with their defaults:
|
|
|
211
225
|
The saved configuration carries a version, `configVersion`. At the first start after an update
|
|
212
226
|
from a configuration without one, the plugin keeps a copy as `config-v1.json` in its data
|
|
213
227
|
directory, removes the look-ahead values the old admin form had filled in with their
|
|
214
|
-
defaults, so the warning style can set them, and saves the result. The
|
|
215
|
-
|
|
216
|
-
style.
|
|
228
|
+
defaults, so the warning style can set them, and saves the result. The warnings then
|
|
229
|
+
behave exactly as before; a value that differed from its default stays and overrides the
|
|
230
|
+
style. A configuration of version 2 is kept as `config-v2.json` and loses its ENC settings:
|
|
231
|
+
one that named a Charts Provider collection reads the whole charts provider from then on,
|
|
232
|
+
and one with an ENC cells directory reads no ENC until its cells are converted in the
|
|
233
|
+
charts provider and **Charts provider** is chosen, which the setup check asks for.
|
|
217
234
|
|
|
218
235
|
## Water level
|
|
219
236
|
|
|
@@ -337,13 +354,13 @@ datum mapping. Absolute paths in its reasons, in 5xx replies and in notification
|
|
|
337
354
|
as `<path>` (an S-57 cell file keeps its name); the plugin status line and the server log keep
|
|
338
355
|
them whole.
|
|
339
356
|
|
|
340
|
-
| Reason shown | Cause and fix
|
|
341
|
-
| ------------------------------------------------------------- |
|
|
342
|
-
| `vessel draft not configured` | set **Draft** under Boat, or `design.draft.maximum` in Signal K
|
|
343
|
-
| `position stale` | no recent `navigation.position`; check the GNSS source
|
|
344
|
-
| `course or speed stale: only the current position is checked` | no recent COG or SOG
|
|
345
|
-
| `no chart index loaded` |
|
|
346
|
-
| `no chart coverage` | the boat or its footprints are outside every ENC cell
|
|
357
|
+
| Reason shown | Cause and fix |
|
|
358
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
359
|
+
| `vessel draft not configured` | set **Draft** under Boat, or `design.draft.maximum` in Signal K |
|
|
360
|
+
| `position stale` | no recent `navigation.position`; check the GNSS source |
|
|
361
|
+
| `course or speed stale: only the current position is checked` | no recent COG or SOG |
|
|
362
|
+
| `no chart index loaded` | choose the charts provider; an index error is shown as the plugin's error |
|
|
363
|
+
| `no chart coverage` | the boat or its footprints are outside every ENC cell |
|
|
347
364
|
|
|
348
365
|
# For developers
|
|
349
366
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CHART_PROVIDERS = void 0;
|
|
4
|
+
exports.chartFields = chartFields;
|
|
5
|
+
const shared_enc_1 = require("./shared-enc");
|
|
6
|
+
/** The services whose ENC store the plugin reads. */
|
|
7
|
+
exports.CHART_PROVIDERS = [
|
|
8
|
+
{ id: shared_enc_1.CHARTS_PROVIDER_ID, name: "Charts Provider (signalk-charts-provider-simple)" }
|
|
9
|
+
];
|
|
10
|
+
const DESCRIPTION = "The chart provider service whose ENC cells the index is built from. Every ENC cell it shares is read in place with GDAL, and the index is rebuilt when its cells change. Empty: no charts.";
|
|
11
|
+
/**
|
|
12
|
+
* The chart field: a pulldown of CHART_PROVIDERS. With `choices`, each is marked running or
|
|
13
|
+
* not, and the description says what Charts Provider shares now.
|
|
14
|
+
*/
|
|
15
|
+
function chartFields(choices) {
|
|
16
|
+
const running = new Set(choices?.running ?? []);
|
|
17
|
+
return {
|
|
18
|
+
chartsProvider: {
|
|
19
|
+
type: "string",
|
|
20
|
+
title: "Charts provider",
|
|
21
|
+
description: choices?.shared === undefined ? DESCRIPTION : `${DESCRIPTION} ${choices.shared}`,
|
|
22
|
+
enum: exports.CHART_PROVIDERS.map((p) => p.id),
|
|
23
|
+
enumNames: exports.CHART_PROVIDERS.map((p) => choices === undefined || running.has(p.id) ? p.name : `${p.name}: not running`)
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
package/lib/config-migrate.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.CONFIG_VERSION = void 0;
|
|
9
9
|
exports.migrateConfig = migrateConfig;
|
|
10
|
-
|
|
10
|
+
const shared_enc_1 = require("./shared-enc");
|
|
11
|
+
exports.CONFIG_VERSION = 3;
|
|
11
12
|
/**
|
|
12
13
|
* Values the version 1 form filled into every saved configuration as
|
|
13
14
|
* defaults. Version 2 leaves these fields empty so the warning style sets
|
|
@@ -49,8 +50,29 @@ function v1ToV2(c, dropped) {
|
|
|
49
50
|
}
|
|
50
51
|
return out;
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Version 3 reads ENC only from a chart provider service, every cell it shares, and asks it
|
|
55
|
+
* for the cells around the vessel unasked: the cells directory and the asking switch are gone,
|
|
56
|
+
* and a Charts Provider collection chosen in version 2 becomes the choice of that service.
|
|
57
|
+
*/
|
|
58
|
+
function v2ToV3(c, dropped) {
|
|
59
|
+
const { cellsDir, chartsProviderEnc, chartsProviderArea, ...out } = c;
|
|
60
|
+
if (cellsDir !== undefined) {
|
|
61
|
+
dropped.push("cellsDir");
|
|
62
|
+
}
|
|
63
|
+
if (chartsProviderArea !== undefined) {
|
|
64
|
+
dropped.push("chartsProviderArea");
|
|
65
|
+
}
|
|
66
|
+
if (chartsProviderEnc !== undefined) {
|
|
67
|
+
dropped.push("chartsProviderEnc");
|
|
68
|
+
if (typeof chartsProviderEnc === "string" && chartsProviderEnc.trim() !== "") {
|
|
69
|
+
out.chartsProvider ??= shared_enc_1.CHARTS_PROVIDER_ID;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
52
74
|
/** Step n takes version n to n + 1. */
|
|
53
|
-
const STEPS = { 1: v1ToV2 };
|
|
75
|
+
const STEPS = { 1: v1ToV2, 2: v2ToV3 };
|
|
54
76
|
function migrateConfig(raw) {
|
|
55
77
|
const saved = record(raw);
|
|
56
78
|
const stated = saved.configVersion;
|
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.";
|
|
@@ -301,11 +302,7 @@ function adminSchema(hints = {}) {
|
|
|
301
302
|
type: "object",
|
|
302
303
|
description: `${ADVISORY}${checks}`,
|
|
303
304
|
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
|
-
},
|
|
305
|
+
...(0, config_charts_1.chartFields)(hints.enc),
|
|
309
306
|
vessel: {
|
|
310
307
|
...VESSEL_ESSENTIAL,
|
|
311
308
|
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 };
|
|
@@ -158,7 +159,7 @@ function parseConfig(raw) {
|
|
|
158
159
|
const style = (0, config_presets_1.warningStyle)(r.warningStyle);
|
|
159
160
|
return {
|
|
160
161
|
indexDir: str(r.indexDir),
|
|
161
|
-
|
|
162
|
+
chartsProviderEnc: r.chartsProvider === shared_enc_1.CHARTS_PROVIDER_ID ? shared_enc_1.ALL_COLLECTIONS : undefined,
|
|
162
163
|
rebuildIndex: r.rebuildIndex === true,
|
|
163
164
|
leafZoom: Math.round(num(r.leafZoom, 20, 14, 22)),
|
|
164
165
|
boxRadiusKm: boxRadius(r.boxRadiusKm),
|
package/lib/engine/chart-box.js
CHANGED
|
@@ -41,18 +41,20 @@ function boxState(box) {
|
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* `reason` with what the chart box has to do with it, when it is a no-coverage reason
|
|
44
|
-
* (`verdict.ts`) and the footprint it names reaches the box edge;
|
|
45
|
-
*
|
|
44
|
+
* (`verdict.ts`) and the footprint it names reaches the box edge; with what the charts
|
|
45
|
+
* provider last answered when no box edge explains it; otherwise `reason`. The footprint is
|
|
46
|
+
* the first at or after the time the reason gives.
|
|
46
47
|
*/
|
|
47
48
|
function boxCoverage(reason, fps, box) {
|
|
48
49
|
const m = COVERAGE_REASON.exec(reason);
|
|
49
|
-
if (!m
|
|
50
|
+
if (!m) {
|
|
50
51
|
return reason;
|
|
51
52
|
}
|
|
52
53
|
const t = m[1] === undefined ? 0 : Number(m[1]);
|
|
53
54
|
const f = fps.find((x) => x.t >= t - 0.5) ?? fps[fps.length - 1];
|
|
54
|
-
if (!f || !atEdge(f, box.bbox)) {
|
|
55
|
-
|
|
55
|
+
if (!f || !box.bbox || !atEdge(f, box.bbox)) {
|
|
56
|
+
// No box edge explains it: the charts have none there, and the provider may say why.
|
|
57
|
+
return box.providerNote === undefined ? reason : `${reason}: ${box.providerNote}`;
|
|
56
58
|
}
|
|
57
59
|
const where = insideBox(f.center, box.bbox) ? "at the chart box edge" : "outside the chart box";
|
|
58
60
|
return `${reason}: ${where}, ${boxState(box)}`;
|
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");
|
|
@@ -71,7 +73,7 @@ function skippedNote(cellsDir, scan) {
|
|
|
71
73
|
return scan.skipped.length === 0
|
|
72
74
|
? []
|
|
73
75
|
: [
|
|
74
|
-
`skipped ${scan.skipped.join(", ")} in ${cellsDir ?? "
|
|
76
|
+
`skipped ${scan.skipped.join(", ")} in ${cellsDir ?? "the charts provider's ENC"}: ` +
|
|
75
77
|
"no S-57 cell (.000) in it, a dangling link, or a cell name met twice"
|
|
76
78
|
];
|
|
77
79
|
}
|
|
@@ -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) {
|
|
@@ -177,16 +180,16 @@ function scanCells(cellsDir, saved) {
|
|
|
177
180
|
}
|
|
178
181
|
const scan = (0, cell_source_1.scanCellSources)(cellsDir);
|
|
179
182
|
return cellsMissing(cellsDir, scan.sources, saved)
|
|
180
|
-
? { away: `no S-57 cell found in ${cellsDir ?? "
|
|
183
|
+
? { away: `no S-57 cell found in ${cellsDir ?? "the charts provider's ENC"}` }
|
|
181
184
|
: { scan };
|
|
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
|
}
|
|
@@ -286,7 +290,7 @@ function boxPlan(cfg, indexDir, dirs, keys, p) {
|
|
|
286
290
|
: (p.mismatch ??
|
|
287
291
|
(sameCells
|
|
288
292
|
? `leaf zoom ${cfg.leafZoom} differs from the saved ${box.leafZoom ?? "?"}`
|
|
289
|
-
: `cells in ${cfg.cellsDir ?? "
|
|
293
|
+
: `cells in ${cfg.cellsDir ?? "the charts provider's ENC"} changed since it was saved`));
|
|
290
294
|
const notes = [...(why ? [`rebuilt the index: ${why}`] : []), ...p.notes];
|
|
291
295
|
return buildPlan(cfg, indexDir, dirs, notes, { bbox: box.bbox, sources });
|
|
292
296
|
}
|
|
@@ -304,7 +308,11 @@ function savedPlan(indexDir, notes, unreadable, syncLoadBytes) {
|
|
|
304
308
|
}
|
|
305
309
|
function buildPlan(cfg, indexDir, dirs, notes, box) {
|
|
306
310
|
if (dirs.length === 0) {
|
|
307
|
-
const none = {
|
|
311
|
+
const none = {
|
|
312
|
+
indexDir,
|
|
313
|
+
built: false,
|
|
314
|
+
reason: `no cells in ${cfg.cellsDir ?? "the charts provider's ENC"}`
|
|
315
|
+
};
|
|
308
316
|
return { load: withNotes(none, notes) };
|
|
309
317
|
}
|
|
310
318
|
// The build would be refused only once it is done: minutes on a Pi 4.
|
|
@@ -312,23 +320,24 @@ function buildPlan(cfg, indexDir, dirs, notes, box) {
|
|
|
312
320
|
if (refused !== undefined) {
|
|
313
321
|
return { load: (0, exports.failedLoad)(indexDir, new Error(refused)) };
|
|
314
322
|
}
|
|
315
|
-
const
|
|
323
|
+
const source = (0, index_source_1.indexSource)(cfg);
|
|
324
|
+
const build = { dirs, leafZoom: cfg.leafZoom, indexDir, ...(source ? { source } : {}) };
|
|
316
325
|
return { build: box ? { ...build, box } : build, indexDir, notes };
|
|
317
326
|
}
|
|
318
|
-
function missingReason(cfg, indexDir
|
|
319
|
-
|
|
320
|
-
return "the saved index holds Traficom WFS depths and that source is off; set cellsDir";
|
|
321
|
-
}
|
|
322
|
-
return cfg.wfs ? exports.WFS_WAITING_REASON : `no chart index in ${indexDir}; set cellsDir`;
|
|
327
|
+
function missingReason(cfg, indexDir) {
|
|
328
|
+
return cfg.wfs ? exports.WFS_WAITING_REASON : `no chart index in ${indexDir}; choose the charts provider`;
|
|
323
329
|
}
|
|
324
330
|
/**
|
|
325
|
-
* `
|
|
326
|
-
*
|
|
327
|
-
*
|
|
331
|
+
* `refused`, why the saved index is not used even with nothing to build another from: it holds
|
|
332
|
+
* WFS depths and that source is off, or it records an ENC source and none is set now.
|
|
333
|
+
* `mismatch`, why the saved index is not used as is: none is saved, those, the source is on
|
|
334
|
+
* and the saved index lacks its cell, or the index was saved by an older version of its
|
|
335
|
+
* pyramid or hazard file.
|
|
328
336
|
*/
|
|
329
337
|
function savedMatches(cfg, indexDir, saved, extraDirs) {
|
|
330
338
|
const holds = saved?.some((c) => c.source === wfs_1.WFS_SOURCE) ?? false;
|
|
331
339
|
const stale = holds && !cfg.wfs;
|
|
340
|
+
const dropped = saved && (0, index_source_1.sourceDropped)(cfg, indexDir);
|
|
332
341
|
const mismatch = saved === undefined
|
|
333
342
|
? `no index saved in ${indexDir}`
|
|
334
343
|
: stale
|
|
@@ -337,8 +346,13 @@ function savedMatches(cfg, indexDir, saved, extraDirs) {
|
|
|
337
346
|
? "the saved one is from an older index version or its header is damaged"
|
|
338
347
|
: extraDirs.length > 0 && !holds
|
|
339
348
|
? "the saved one lacks the Traficom WFS cell"
|
|
340
|
-
:
|
|
341
|
-
|
|
349
|
+
: dropped;
|
|
350
|
+
const refused = stale
|
|
351
|
+
? "the saved index holds Traficom WFS depths and that source is off; choose the charts provider"
|
|
352
|
+
: dropped && extraDirs.length === 0
|
|
353
|
+
? `not using the saved index: ${dropped}; choose the charts provider`
|
|
354
|
+
: undefined;
|
|
355
|
+
return { refused, mismatch };
|
|
342
356
|
}
|
|
343
357
|
function savedCells(indexDir) {
|
|
344
358
|
return (0, cells_1.parseCatalogue)(JSON.parse(fs_1.default.readFileSync(path_1.default.join(indexDir, "cells.json"), "utf8")))
|
|
@@ -0,0 +1,72 @@
|
|
|
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. The
|
|
21
|
+
* whole store holds every collection, so an index of one collection, as saved before version
|
|
22
|
+
* 3 of the configuration, is the whole store's too.
|
|
23
|
+
*/
|
|
24
|
+
const PROVIDER = "charts-provider:";
|
|
25
|
+
/** Whether an index saved from `saved` was built from the cells `now` names. */
|
|
26
|
+
function sameSource(saved, now) {
|
|
27
|
+
return saved === now || (now === `${PROVIDER}*` && saved.startsWith(PROVIDER));
|
|
28
|
+
}
|
|
29
|
+
/** The source `cfg` reads its S-57 cells from; undefined without one. */
|
|
30
|
+
function indexSource(cfg) {
|
|
31
|
+
if (cfg.chartsProviderEnc !== undefined) {
|
|
32
|
+
return `${PROVIDER}${cfg.chartsProviderEnc}`;
|
|
33
|
+
}
|
|
34
|
+
return cfg.cellsDir ? `dir:${path_1.default.resolve(cfg.cellsDir)}` : undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Why the index in `indexDir` was not built from the source `cfg` reads, or undefined when it
|
|
38
|
+
* was, or records no source (an index saved by an older version).
|
|
39
|
+
*/
|
|
40
|
+
function sourceChanged(cfg, indexDir) {
|
|
41
|
+
const saved = (0, build_source_1.readSource)(indexDir);
|
|
42
|
+
const now = indexSource(cfg);
|
|
43
|
+
return saved !== undefined && !sameSource(saved, now)
|
|
44
|
+
? `the saved index was built from ${describe(saved)}, not ${now === undefined ? "the cells now set" : describe(now)}`
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
47
|
+
function describe(source) {
|
|
48
|
+
return source.startsWith(PROVIDER)
|
|
49
|
+
? `Charts Provider's ${source.slice(PROVIDER.length)}`
|
|
50
|
+
: source.slice(source.indexOf(":") + 1);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `found`, a scan of the cells directory or why it says nothing of the saved index, except
|
|
54
|
+
* that cells away from a source the saved index was not built from are none: an unmounted
|
|
55
|
+
* drive keeps the saved index, but only one built from the cells now set.
|
|
56
|
+
*/
|
|
57
|
+
function cellsNow(found, cfg, indexDir) {
|
|
58
|
+
const moved = "away" in found ? sourceChanged(cfg, indexDir) : undefined;
|
|
59
|
+
return "away" in found && moved !== undefined
|
|
60
|
+
? {
|
|
61
|
+
scan: { sources: [], skipped: [] },
|
|
62
|
+
note: `${found.away}; not using the saved index: ${moved}`
|
|
63
|
+
}
|
|
64
|
+
: found;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Why the index in `indexDir` is not this configuration's when `cfg` sets no ENC source at
|
|
68
|
+
* all, as when the source is cleared, though the index records one; undefined otherwise.
|
|
69
|
+
*/
|
|
70
|
+
function sourceDropped(cfg, indexDir) {
|
|
71
|
+
return indexSource(cfg) === undefined ? sourceChanged(cfg, indexDir) : undefined;
|
|
72
|
+
}
|
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
|
|
@@ -76,6 +91,8 @@ function formSchema(app, state) {
|
|
|
76
91
|
catch (err) {
|
|
77
92
|
app.error(`setup checks failed: ${err.message}`);
|
|
78
93
|
}
|
|
94
|
+
// The chart pulldown lists what Charts Provider shares also while this plugin is off.
|
|
95
|
+
hints.enc ??= (0, shared_enc_1.runningProviders)();
|
|
79
96
|
const schema = (0, config_schema_1.adminSchema)(hints);
|
|
80
97
|
const version = app.config?.version;
|
|
81
98
|
return (0, config_schema_1.formsFollowDependencies)(version) ? schema : (0, config_schema_1.flatten)(schema);
|
|
@@ -87,6 +104,7 @@ module.exports = function underkeel(app, clock = monitor_1.SYSTEM_CLOCK, fetch =
|
|
|
87
104
|
requests: new route_service_1.RequestQueue()
|
|
88
105
|
};
|
|
89
106
|
let startup;
|
|
107
|
+
let shared;
|
|
90
108
|
return {
|
|
91
109
|
id: manifest_1.PLUGIN_ID,
|
|
92
110
|
name: "Under-Keel Awareness",
|
|
@@ -96,10 +114,10 @@ module.exports = function underkeel(app, clock = monitor_1.SYSTEM_CLOCK, fetch =
|
|
|
96
114
|
start(options, restart) {
|
|
97
115
|
const migration = (0, config_migrate_1.migrateConfig)(options);
|
|
98
116
|
(0, settings_service_1.persistMigration)(app, options, migration);
|
|
99
|
-
const config = (0, config_1.parseConfig)(migration.config);
|
|
117
|
+
const config = (0, shared_enc_1.withSharedCells)((0, config_1.parseConfig)(migration.config), app.getDataDirPath());
|
|
100
118
|
const wfs = (0, wfs_wiring_1.startBox)({ app, clock, state, config, fetch, build });
|
|
101
119
|
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);
|
|
120
|
+
const load = "load" in plan ? (0, shared_enc_1.noteSharedCells)(plan.load, config) : (0, index_startup_1.pendingLoad)(plan);
|
|
103
121
|
const monitor = new monitor_1.Monitor(app, manifest_1.PLUGIN_ID, config, load.index, load.reason, clock, fetch);
|
|
104
122
|
const acks = new ack_store_1.AckStore(path_1.default.join(app.getDataDirPath(), "acks.json"));
|
|
105
123
|
if (acks.error) {
|
|
@@ -127,10 +145,15 @@ module.exports = function underkeel(app, clock = monitor_1.SYSTEM_CLOCK, fetch =
|
|
|
127
145
|
state.zoneErrors = errors;
|
|
128
146
|
errors.forEach((e) => app.error(e));
|
|
129
147
|
});
|
|
148
|
+
shared = config.chartsProviderEnc
|
|
149
|
+
? watchShared(app, clock, config.chartsProviderEnc, restart && (() => restart(migration.config)))
|
|
150
|
+
: undefined;
|
|
130
151
|
(0, assets_1.mountAssets)(app);
|
|
131
152
|
app.registerResourceProvider((0, manifest_1.manifestProvider)(VERSION));
|
|
132
153
|
},
|
|
133
154
|
stop() {
|
|
155
|
+
shared?.stop();
|
|
156
|
+
shared = undefined;
|
|
134
157
|
startup?.abort();
|
|
135
158
|
startup = undefined;
|
|
136
159
|
stopPlugin(app, clock, state);
|