signalk-underkeel 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +40 -37
- package/lib/config-charts.js +15 -23
- package/lib/config-migrate.js +24 -2
- package/lib/config-schema.js +2 -3
- package/lib/config.js +1 -11
- package/lib/engine/chart-box.js +7 -5
- package/lib/index-loader.js +11 -7
- package/lib/index-source.js +12 -5
- package/lib/index.js +2 -0
- 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 +9 -4
- package/lib/shared-enc.js +61 -23
- package/lib/wfs-service.js +19 -3
- package/lib/wfs-wiring.js +66 -25
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to signalk-underkeel are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.5.1] - 2026-11-10
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- The package is listed in the Signal K plugin registry's **Plotter Extensions** category
|
|
10
|
+
(keyword `signalk-category-plotter-extensions`).
|
|
11
|
+
|
|
12
|
+
## [0.5.0] - 2026-11-09
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **Breaking:** ENC cells come only from a chart provider service. **ENC cells directory**,
|
|
17
|
+
**ENC from Charts Provider** and **Ask Charts Provider for the ENC around the vessel** are
|
|
18
|
+
replaced by one pulldown, **Charts provider**, which chooses the service:
|
|
19
|
+
signalk-charts-provider-simple. Every ENC cell it shares is read, and the pulldown marks it
|
|
20
|
+
when it is not running and says what it shares. Cells of your own reach the index by
|
|
21
|
+
converting them in the charts provider with its **Keep the ENC cells of converted charts**
|
|
22
|
+
on.
|
|
23
|
+
- Each new index box asks the chosen charts provider for the ENC cells of its area, without a
|
|
24
|
+
setting of its own; its **Download the ENC cells around the vessel** decides whether it
|
|
25
|
+
downloads them (signalk-charts-provider-simple 2.18.0 or later).
|
|
26
|
+
- Where the vessel has no chart coverage, the reason adds what the charts provider answered
|
|
27
|
+
for the box: it is downloading the area, it has no ENC for this area (by the cells it has
|
|
28
|
+
stored there, with its reason), it is not running, or it is too old to be asked.
|
|
29
|
+
- The configuration moves to version 3. The migration drops the cells directory and the two
|
|
30
|
+
Charts Provider settings, chooses the charts provider when a collection was named, and
|
|
31
|
+
keeps the version 2 configuration as `config-v2.json` in the plugin data directory. When it
|
|
32
|
+
drops a cells directory and no charts provider takes its place, the server log says so, and
|
|
33
|
+
the setup check says no ENC is read until one is chosen.
|
|
34
|
+
|
|
35
|
+
- Of a cell two charts provider collections hold, the index reads the copy of the newest
|
|
36
|
+
edition and update, as the cell's DSID and its update files give them, rather than the first
|
|
37
|
+
in path order. A copy counts only the updates GDAL applies to it, and a copy GDAL cannot
|
|
38
|
+
read ranks below every copy it can.
|
|
39
|
+
- An index saved from one Charts Provider collection is the whole charts provider's, so it
|
|
40
|
+
stays in use after the upgrade while the charts provider's store is away.
|
|
41
|
+
- With no index yet, a failed box build's reason adds what the charts provider said of the
|
|
42
|
+
box, such as that it is downloading the area.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- A failing area request to the charts provider is logged and no longer fails the box
|
|
47
|
+
refresh that made it, and is asked again after 5 minutes rather than when the box moves.
|
|
48
|
+
An area request that fails in the plugin itself is asked again after 5 minutes too.
|
|
49
|
+
- The no-coverage reason says the charts provider is not running once it stops, rather than
|
|
50
|
+
repeating its last answer; and with no index, the reason says so when it was not running at
|
|
51
|
+
start.
|
|
52
|
+
|
|
5
53
|
## [0.4.0] - 2026-11-02
|
|
6
54
|
|
|
7
55
|
### Added
|
package/README.md
CHANGED
|
@@ -102,23 +102,24 @@ 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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
122
123
|
read in place with GDAL compiled to WebAssembly
|
|
123
124
|
(gdal3.js), which the plugin installs with it, and the updates are applied as they are read.
|
|
124
125
|
Nothing is converted or written next to the cells. A cell without an `M_COVR` coverage layer
|
|
@@ -160,7 +161,7 @@ rebuilds in 12–15 s instead of 30–34 s on x86. Deleting the folder is safe;
|
|
|
160
161
|
read again.
|
|
161
162
|
|
|
162
163
|
Folders of GeoJSON layers exported by `scripts/enc-to-geojson.sh` for earlier versions are no
|
|
163
|
-
longer read:
|
|
164
|
+
longer read: convert the original `.000` cells in the charts provider instead. Such a folder
|
|
164
165
|
named like a cell is listed in `GET /status` as skipped.
|
|
165
166
|
|
|
166
167
|
## Settings
|
|
@@ -180,14 +181,13 @@ There are two places to set the plugin up, and both write the same configuration
|
|
|
180
181
|
|
|
181
182
|

|
|
182
183
|
|
|
183
|
-
| Setting
|
|
184
|
-
|
|
|
185
|
-
| ENC cells
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
| Optional features | living chart (own soundings), Traficom WFS depths, grounding detector; each off until enabled |
|
|
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 |
|
|
191
191
|
|
|
192
192
|
| Warning style | Looks ahead | Alert at | Warn at, within | Alarm at, within | Alarm on negative mean UKC |
|
|
193
193
|
| ------------------ | ----------- | -------- | --------------- | ---------------- | -------------------------- |
|
|
@@ -214,7 +214,7 @@ Expert settings, with their defaults:
|
|
|
214
214
|
|
|
215
215
|
| Group | Settings |
|
|
216
216
|
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
217
|
-
| 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) |
|
|
218
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) |
|
|
219
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 |
|
|
220
220
|
| Water level | manual height and σ; FMI refresh (600 s); chart-datum mappings for FMI |
|
|
@@ -225,9 +225,12 @@ Expert settings, with their defaults:
|
|
|
225
225
|
The saved configuration carries a version, `configVersion`. At the first start after an update
|
|
226
226
|
from a configuration without one, the plugin keeps a copy as `config-v1.json` in its data
|
|
227
227
|
directory, removes the look-ahead values the old admin form had filled in with their
|
|
228
|
-
defaults, so the warning style can set them, and saves the result. The
|
|
229
|
-
|
|
230
|
-
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.
|
|
231
234
|
|
|
232
235
|
## Water level
|
|
233
236
|
|
|
@@ -351,13 +354,13 @@ datum mapping. Absolute paths in its reasons, in 5xx replies and in notification
|
|
|
351
354
|
as `<path>` (an S-57 cell file keeps its name); the plugin status line and the server log keep
|
|
352
355
|
them whole.
|
|
353
356
|
|
|
354
|
-
| Reason shown | Cause and fix
|
|
355
|
-
| ------------------------------------------------------------- |
|
|
356
|
-
| `vessel draft not configured` | set **Draft** under Boat, or `design.draft.maximum` in Signal K
|
|
357
|
-
| `position stale` | no recent `navigation.position`; check the GNSS source
|
|
358
|
-
| `course or speed stale: only the current position is checked` | no recent COG or SOG
|
|
359
|
-
| `no chart index loaded` |
|
|
360
|
-
| `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 |
|
|
361
364
|
|
|
362
365
|
# For developers
|
|
363
366
|
|
package/lib/config-charts.js
CHANGED
|
@@ -1,34 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.CHART_PROVIDERS = void 0;
|
|
4
4
|
exports.chartFields = chartFields;
|
|
5
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.";
|
|
6
11
|
/**
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
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.
|
|
9
14
|
*/
|
|
10
|
-
function chartFields(
|
|
15
|
+
function chartFields(choices) {
|
|
16
|
+
const running = new Set(choices?.running ?? []);
|
|
11
17
|
return {
|
|
12
|
-
|
|
18
|
+
chartsProvider: {
|
|
13
19
|
type: "string",
|
|
14
|
-
title: "
|
|
15
|
-
description:
|
|
16
|
-
|
|
17
|
-
|
|
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}` : "")
|
|
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`)
|
|
23
24
|
}
|
|
24
25
|
};
|
|
25
26
|
}
|
|
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-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
|
@@ -265,8 +265,7 @@ const CHART_INDEX = {
|
|
|
265
265
|
maximum: 22
|
|
266
266
|
},
|
|
267
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."),
|
|
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
|
+
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.")
|
|
270
269
|
};
|
|
271
270
|
const EXPERT = {
|
|
272
271
|
...CHART_INDEX,
|
|
@@ -303,7 +302,7 @@ function adminSchema(hints = {}) {
|
|
|
303
302
|
type: "object",
|
|
304
303
|
description: `${ADVISORY}${checks}`,
|
|
305
304
|
properties: {
|
|
306
|
-
...(0, config_charts_1.chartFields)(hints.
|
|
305
|
+
...(0, config_charts_1.chartFields)(hints.enc),
|
|
307
306
|
vessel: {
|
|
308
307
|
...VESSEL_ESSENTIAL,
|
|
309
308
|
properties: { ...VESSEL_ESSENTIAL.properties, draft: draftField(hints.signalkDraft) }
|
package/lib/config.js
CHANGED
|
@@ -23,14 +23,6 @@ function optionalPositive(v) {
|
|
|
23
23
|
function str(v) {
|
|
24
24
|
return typeof v === "string" && v.trim() !== "" ? v : undefined;
|
|
25
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
|
-
}
|
|
34
26
|
function record(v) {
|
|
35
27
|
return typeof v === "object" && v !== null ? v : {};
|
|
36
28
|
}
|
|
@@ -167,9 +159,7 @@ function parseConfig(raw) {
|
|
|
167
159
|
const style = (0, config_presets_1.warningStyle)(r.warningStyle);
|
|
168
160
|
return {
|
|
169
161
|
indexDir: str(r.indexDir),
|
|
170
|
-
|
|
171
|
-
chartsProviderEnc: collectionChoice(r.chartsProviderEnc),
|
|
172
|
-
chartsProviderArea: r.chartsProviderArea === true,
|
|
162
|
+
chartsProviderEnc: r.chartsProvider === shared_enc_1.CHARTS_PROVIDER_ID ? shared_enc_1.ALL_COLLECTIONS : undefined,
|
|
173
163
|
rebuildIndex: r.rebuildIndex === true,
|
|
174
164
|
leafZoom: Math.round(num(r.leafZoom, 20, 14, 22)),
|
|
175
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
|
@@ -73,7 +73,7 @@ function skippedNote(cellsDir, scan) {
|
|
|
73
73
|
return scan.skipped.length === 0
|
|
74
74
|
? []
|
|
75
75
|
: [
|
|
76
|
-
`skipped ${scan.skipped.join(", ")} in ${cellsDir ?? "
|
|
76
|
+
`skipped ${scan.skipped.join(", ")} in ${cellsDir ?? "the charts provider's ENC"}: ` +
|
|
77
77
|
"no S-57 cell (.000) in it, a dangling link, or a cell name met twice"
|
|
78
78
|
];
|
|
79
79
|
}
|
|
@@ -180,7 +180,7 @@ function scanCells(cellsDir, saved) {
|
|
|
180
180
|
}
|
|
181
181
|
const scan = (0, cell_source_1.scanCellSources)(cellsDir);
|
|
182
182
|
return cellsMissing(cellsDir, scan.sources, saved)
|
|
183
|
-
? { away: `no S-57 cell found in ${cellsDir ?? "
|
|
183
|
+
? { away: `no S-57 cell found in ${cellsDir ?? "the charts provider's ENC"}` }
|
|
184
184
|
: { scan };
|
|
185
185
|
}
|
|
186
186
|
function plan(cfg, indexDir, extraDirs, syncLoadBytes) {
|
|
@@ -290,7 +290,7 @@ function boxPlan(cfg, indexDir, dirs, keys, p) {
|
|
|
290
290
|
: (p.mismatch ??
|
|
291
291
|
(sameCells
|
|
292
292
|
? `leaf zoom ${cfg.leafZoom} differs from the saved ${box.leafZoom ?? "?"}`
|
|
293
|
-
: `cells in ${cfg.cellsDir ?? "
|
|
293
|
+
: `cells in ${cfg.cellsDir ?? "the charts provider's ENC"} changed since it was saved`));
|
|
294
294
|
const notes = [...(why ? [`rebuilt the index: ${why}`] : []), ...p.notes];
|
|
295
295
|
return buildPlan(cfg, indexDir, dirs, notes, { bbox: box.bbox, sources });
|
|
296
296
|
}
|
|
@@ -308,7 +308,11 @@ function savedPlan(indexDir, notes, unreadable, syncLoadBytes) {
|
|
|
308
308
|
}
|
|
309
309
|
function buildPlan(cfg, indexDir, dirs, notes, box) {
|
|
310
310
|
if (dirs.length === 0) {
|
|
311
|
-
const none = {
|
|
311
|
+
const none = {
|
|
312
|
+
indexDir,
|
|
313
|
+
built: false,
|
|
314
|
+
reason: `no cells in ${cfg.cellsDir ?? "the charts provider's ENC"}`
|
|
315
|
+
};
|
|
312
316
|
return { load: withNotes(none, notes) };
|
|
313
317
|
}
|
|
314
318
|
// The build would be refused only once it is done: minutes on a Pi 4.
|
|
@@ -321,7 +325,7 @@ function buildPlan(cfg, indexDir, dirs, notes, box) {
|
|
|
321
325
|
return { build: box ? { ...build, box } : build, indexDir, notes };
|
|
322
326
|
}
|
|
323
327
|
function missingReason(cfg, indexDir) {
|
|
324
|
-
return cfg.wfs ? exports.WFS_WAITING_REASON : `no chart index in ${indexDir};
|
|
328
|
+
return cfg.wfs ? exports.WFS_WAITING_REASON : `no chart index in ${indexDir}; choose the charts provider`;
|
|
325
329
|
}
|
|
326
330
|
/**
|
|
327
331
|
* `refused`, why the saved index is not used even with nothing to build another from: it holds
|
|
@@ -344,9 +348,9 @@ function savedMatches(cfg, indexDir, saved, extraDirs) {
|
|
|
344
348
|
? "the saved one lacks the Traficom WFS cell"
|
|
345
349
|
: dropped;
|
|
346
350
|
const refused = stale
|
|
347
|
-
? "the saved index holds Traficom WFS depths and that source is off;
|
|
351
|
+
? "the saved index holds Traficom WFS depths and that source is off; choose the charts provider"
|
|
348
352
|
: dropped && extraDirs.length === 0
|
|
349
|
-
? `not using the saved index: ${dropped};
|
|
353
|
+
? `not using the saved index: ${dropped}; choose the charts provider`
|
|
350
354
|
: undefined;
|
|
351
355
|
return { refused, mismatch };
|
|
352
356
|
}
|
package/lib/index-source.js
CHANGED
|
@@ -17,12 +17,19 @@ const build_source_1 = require("./spatial/build-source");
|
|
|
17
17
|
* used meanwhile, while a source the user chose since was never the saved index's.
|
|
18
18
|
*
|
|
19
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.
|
|
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.
|
|
21
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
|
+
}
|
|
22
29
|
/** The source `cfg` reads its S-57 cells from; undefined without one. */
|
|
23
30
|
function indexSource(cfg) {
|
|
24
31
|
if (cfg.chartsProviderEnc !== undefined) {
|
|
25
|
-
return
|
|
32
|
+
return `${PROVIDER}${cfg.chartsProviderEnc}`;
|
|
26
33
|
}
|
|
27
34
|
return cfg.cellsDir ? `dir:${path_1.default.resolve(cfg.cellsDir)}` : undefined;
|
|
28
35
|
}
|
|
@@ -33,13 +40,13 @@ function indexSource(cfg) {
|
|
|
33
40
|
function sourceChanged(cfg, indexDir) {
|
|
34
41
|
const saved = (0, build_source_1.readSource)(indexDir);
|
|
35
42
|
const now = indexSource(cfg);
|
|
36
|
-
return saved !== undefined && saved
|
|
43
|
+
return saved !== undefined && !sameSource(saved, now)
|
|
37
44
|
? `the saved index was built from ${describe(saved)}, not ${now === undefined ? "the cells now set" : describe(now)}`
|
|
38
45
|
: undefined;
|
|
39
46
|
}
|
|
40
47
|
function describe(source) {
|
|
41
|
-
return source.startsWith(
|
|
42
|
-
? `Charts Provider's ${source.slice(
|
|
48
|
+
return source.startsWith(PROVIDER)
|
|
49
|
+
? `Charts Provider's ${source.slice(PROVIDER.length)}`
|
|
43
50
|
: source.slice(source.indexOf(":") + 1);
|
|
44
51
|
}
|
|
45
52
|
/**
|
package/lib/index.js
CHANGED
|
@@ -91,6 +91,8 @@ function formSchema(app, state) {
|
|
|
91
91
|
catch (err) {
|
|
92
92
|
app.error(`setup checks failed: ${err.message}`);
|
|
93
93
|
}
|
|
94
|
+
// The chart pulldown lists what Charts Provider shares also while this plugin is off.
|
|
95
|
+
hints.enc ??= (0, shared_enc_1.runningProviders)();
|
|
94
96
|
const schema = (0, config_schema_1.adminSchema)(hints);
|
|
95
97
|
const version = app.config?.version;
|
|
96
98
|
return (0, config_schema_1.formsFollowDependencies)(version) ? schema : (0, config_schema_1.flatten)(schema);
|
|
@@ -8,6 +8,7 @@ exports.sourceName = sourceName;
|
|
|
8
8
|
exports.currentInfo = currentInfo;
|
|
9
9
|
exports.sourceIdentity = sourceIdentity;
|
|
10
10
|
exports.readCell = readCell;
|
|
11
|
+
exports.cellVersionRank = cellVersionRank;
|
|
11
12
|
exports.scanCellSources = scanCellSources;
|
|
12
13
|
const crypto_1 = require("crypto");
|
|
13
14
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -64,6 +65,46 @@ function readCell(source, cellRef) {
|
|
|
64
65
|
? (0, enc_wasm_1.readEncCell)(source, cellRef)
|
|
65
66
|
: Promise.resolve((0, cell_reader_1.readCellDirectory)(source, cellRef));
|
|
66
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* A copy's edition and update as one comparable number, as GDAL's S-57 reader (3.8) applies
|
|
70
|
+
* its updates: from `.001` on, one file after another until one is missing or does not open as
|
|
71
|
+
* ISO 8211, whatever update the base cell's DSID carries. Each update's DSID must name the edition the
|
|
72
|
+
* reader holds, or 0 (as NOAA's do), and the update after the one it holds; otherwise the
|
|
73
|
+
* reader gives up on the whole cell, as it does on a cell re-issued at update 3 with a `.001`
|
|
74
|
+
* beside it. -1 when the copy would not be read so, or its edition or files cannot be read.
|
|
75
|
+
*/
|
|
76
|
+
function cellVersionRank(file) {
|
|
77
|
+
try {
|
|
78
|
+
const version = (0, enc_wasm_1.baseVersion)(file);
|
|
79
|
+
if (version === undefined) {
|
|
80
|
+
return -1;
|
|
81
|
+
}
|
|
82
|
+
const updates = new Map((0, enc_wasm_1.encFiles)(file)
|
|
83
|
+
.slice(1)
|
|
84
|
+
.map((f) => [Number(path_1.default.extname(f).slice(1)), f]));
|
|
85
|
+
let { edition, update } = version;
|
|
86
|
+
for (let n = 1;; n++) {
|
|
87
|
+
const f = updates.get(n);
|
|
88
|
+
// A file missing, or one that does not open as ISO 8211, ends the updates GDAL applies.
|
|
89
|
+
if (f === undefined || !(0, enc_wasm_1.isIso8211)(f)) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
// One that opens but whose DSID gives no edition and update in step makes it give up.
|
|
93
|
+
const own = (0, enc_wasm_1.dsidVersion)(f);
|
|
94
|
+
if (own.edition === undefined ||
|
|
95
|
+
own.update !== update + 1 ||
|
|
96
|
+
(own.edition !== 0 && own.edition !== edition)) {
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
edition = own.edition;
|
|
100
|
+
update = own.update;
|
|
101
|
+
}
|
|
102
|
+
return version.edition * 100_000 + update;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
67
108
|
/** How deep below the cells directory S-57 cells are looked for: ENC_ROOT/<cell>/<cell>.000. */
|
|
68
109
|
const MAX_DEPTH = 4;
|
|
69
110
|
/**
|
|
@@ -85,8 +126,10 @@ function isDirectory(p) {
|
|
|
85
126
|
/**
|
|
86
127
|
* The S-57 cells under `cellsDir`: every `.000` file down to MAX_DEPTH, such
|
|
87
128
|
* as in an ENC_ROOT exchange set, and down to EXCHANGE_SET_DEPTH below any
|
|
88
|
-
* directory holding a CATALOG.031 within it. A name met twice
|
|
89
|
-
*
|
|
129
|
+
* directory holding a CATALOG.031 within it. A name met twice, as one cell in two charts
|
|
130
|
+
* provider collections, keeps the copy of the newest edition and update; between copies of
|
|
131
|
+
* the same version, or whose edition cannot be read, the first in path order. One file met
|
|
132
|
+
* again through a link counts once. A
|
|
90
133
|
* directory is read again only when reached with more depth left than
|
|
91
134
|
* before, and never from within itself, so a linked loop ends where it comes
|
|
92
135
|
* back.
|
|
@@ -96,7 +139,8 @@ function scanCellSources(cellsDir) {
|
|
|
96
139
|
if (!cellsDir) {
|
|
97
140
|
return scan;
|
|
98
141
|
}
|
|
99
|
-
|
|
142
|
+
/** Each cell name's index in `scan.sources`, and the version of the copy kept there. */
|
|
143
|
+
const seen = new Map();
|
|
100
144
|
const files = new Set();
|
|
101
145
|
/** Folders named like a cell, by real path: skipped unless a cell is read from under one. */
|
|
102
146
|
const named = new Map();
|
|
@@ -110,11 +154,21 @@ function scanCellSources(cellsDir) {
|
|
|
110
154
|
}
|
|
111
155
|
files.add(real);
|
|
112
156
|
const name = sourceName(source);
|
|
113
|
-
|
|
114
|
-
|
|
157
|
+
const kept = seen.get(name);
|
|
158
|
+
if (kept) {
|
|
159
|
+
// Versions are read only for a name met twice: one header each, not a scan of every cell.
|
|
160
|
+
// A copy that cannot be read ranks -1 and never displaces one that can.
|
|
161
|
+
kept.rank ??= cellVersionRank(scan.sources[kept.at]);
|
|
162
|
+
const rank = cellVersionRank(source);
|
|
163
|
+
const older = rank > kept.rank ? scan.sources[kept.at] : source;
|
|
164
|
+
if (older !== source) {
|
|
165
|
+
scan.sources[kept.at] = source;
|
|
166
|
+
kept.rank = rank;
|
|
167
|
+
}
|
|
168
|
+
scan.skipped.push(path_1.default.relative(cellsDir, older));
|
|
115
169
|
return;
|
|
116
170
|
}
|
|
117
|
-
seen.
|
|
171
|
+
seen.set(name, { at: scan.sources.length });
|
|
118
172
|
scan.sources.push(source);
|
|
119
173
|
};
|
|
120
174
|
/** Depth left when each real directory was read, and those being read now. */
|
package/lib/ingest/enc-wasm.js
CHANGED
|
@@ -8,7 +8,10 @@ exports.encReaderId = encReaderId;
|
|
|
8
8
|
exports.sharedLayers = sharedLayers;
|
|
9
9
|
exports.readEncLayers = readEncLayers;
|
|
10
10
|
exports.readEncParts = readEncParts;
|
|
11
|
+
exports.baseVersion = baseVersion;
|
|
11
12
|
exports.baseEdition = baseEdition;
|
|
13
|
+
exports.isIso8211 = isIso8211;
|
|
14
|
+
exports.dsidVersion = dsidVersion;
|
|
12
15
|
exports.encCell = encCell;
|
|
13
16
|
exports.readEncCell = readEncCell;
|
|
14
17
|
exports.encFiles = encFiles;
|
|
@@ -226,13 +229,16 @@ async function readEncParts(file) {
|
|
|
226
229
|
parts.set(s57_geojson_1.POSITION_LAYER, Buffer.from(JSON.stringify(positionLayer(primitives))));
|
|
227
230
|
return parts;
|
|
228
231
|
}
|
|
232
|
+
/** The highest edition and update numbers read as known: an update has a 3-digit file. */
|
|
233
|
+
const MAX_EDITION = 9_999;
|
|
234
|
+
const MAX_UPDATE = 999;
|
|
229
235
|
/**
|
|
230
|
-
* The
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
236
|
+
* The EDTN and UPDN fields of a cell file's DSID record, read from the ISO 8211 file itself;
|
|
237
|
+
* each undefined when it is missing, not all digits, or out of range, and both when the record
|
|
238
|
+
* is not laid out as S-57 sets it, as a separator lost or misplaced leaves it. EDTN may be 0,
|
|
239
|
+
* as NOAA update files carry it.
|
|
234
240
|
*/
|
|
235
|
-
function
|
|
241
|
+
function dsidFields(file) {
|
|
236
242
|
const fd = fs_1.default.openSync(file, "r");
|
|
237
243
|
try {
|
|
238
244
|
const head = Buffer.alloc(65_536);
|
|
@@ -241,26 +247,150 @@ function baseEdition(file) {
|
|
|
241
247
|
// The data descriptive record comes first; the DSID is in the first data record after it.
|
|
242
248
|
const dr = num(0, 5);
|
|
243
249
|
if (!(dr > 0) || dr + 24 > size) {
|
|
244
|
-
return
|
|
250
|
+
return {};
|
|
245
251
|
}
|
|
246
252
|
const base = dr + num(dr + 12, 5);
|
|
247
253
|
const [lenSize, posSize, tagSize] = [num(dr + 20, 1), num(dr + 21, 1), num(dr + 23, 1)];
|
|
248
|
-
|
|
254
|
+
// A size of 0, or not a digit, would leave the directory walk where it is.
|
|
255
|
+
if (![lenSize, posSize, tagSize].every((n) => Number.isInteger(n) && n > 0)) {
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
const entry = tagSize + lenSize + posSize;
|
|
259
|
+
for (let e = dr + 24; head[e] !== 0x1e && e < base; e += entry) {
|
|
249
260
|
if (head.toString("latin1", e, e + tagSize) === "DSID") {
|
|
250
|
-
// RCNM (1 byte), RCID (4), EXPP (1), INTU (1), then DSNM and
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
261
|
+
// RCNM (1 byte), RCID (4), EXPP (1), INTU (1), then DSNM, EDTN and UPDN, each ended by
|
|
262
|
+
// 0x1f, then UADT and ISDT, 8 characters each with no separator, all within the field.
|
|
263
|
+
const start = base + num(e + tagSize + lenSize, posSize);
|
|
264
|
+
const stop = Math.min(size, start + num(e + tagSize, lenSize));
|
|
265
|
+
const after = (from) => {
|
|
266
|
+
const at = head.indexOf(0x1f, from);
|
|
267
|
+
return at >= 0 && at < stop ? at : -1;
|
|
268
|
+
};
|
|
269
|
+
const dsnmEnd = after(start + 7);
|
|
270
|
+
const edtnEnd = dsnmEnd < 0 ? -1 : after(dsnmEnd + 1);
|
|
271
|
+
const updnEnd = edtnEnd < 0 ? -1 : after(edtnEnd + 1);
|
|
272
|
+
// A separator lost or misplaced leaves no two dates where they belong.
|
|
273
|
+
const dates = head.toString("latin1", updnEnd + 1, updnEnd + 17);
|
|
274
|
+
if (updnEnd < 0 || updnEnd + 17 > stop || !/^[\d ]{16}$/.test(dates)) {
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
const field = (from, to, max) => {
|
|
278
|
+
const text = head.toString("latin1", from, to);
|
|
279
|
+
const n = /^\d{1,5}$/.test(text) ? Number(text) : NaN;
|
|
280
|
+
return n <= max ? n : undefined;
|
|
281
|
+
};
|
|
282
|
+
const edition = field(dsnmEnd + 1, edtnEnd, MAX_EDITION);
|
|
283
|
+
const update = field(edtnEnd + 1, updnEnd, MAX_UPDATE);
|
|
284
|
+
return {
|
|
285
|
+
...(edition === undefined ? {} : { edition }),
|
|
286
|
+
...(update === undefined ? {} : { update })
|
|
287
|
+
};
|
|
256
288
|
}
|
|
257
289
|
}
|
|
258
|
-
return
|
|
290
|
+
return {};
|
|
259
291
|
}
|
|
260
292
|
finally {
|
|
261
293
|
fs_1.default.closeSync(fd);
|
|
262
294
|
}
|
|
263
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* The edition and update numbers in the base cell's DSID record. GDAL reports the last
|
|
298
|
+
* update's DSID_EDTN once updates are applied, and NOAA updates carry 0 there: US5FL65M,
|
|
299
|
+
* edition 38, read as edition 0 with its three updates. A base cell re-issued with its updates
|
|
300
|
+
* applied carries their number in UPDN. Undefined unless both read as numbers in range, as a
|
|
301
|
+
* version that cannot be read is not known.
|
|
302
|
+
*/
|
|
303
|
+
function baseVersion(file) {
|
|
304
|
+
const { edition, update } = dsidFields(file);
|
|
305
|
+
return edition !== undefined && edition > 0 && update !== undefined
|
|
306
|
+
? { edition, update }
|
|
307
|
+
: undefined;
|
|
308
|
+
}
|
|
309
|
+
/** The edition number in the base cell's DSID record, whatever its UPDN holds. */
|
|
310
|
+
function baseEdition(file) {
|
|
311
|
+
const { edition } = dsidFields(file);
|
|
312
|
+
return edition !== undefined && edition > 0 ? edition : undefined;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Whether `file` opens as ISO 8211 as GDAL's `DDFModule::Open` (3.8) checks it: a 24-byte
|
|
316
|
+
* leader of printable characters with an interchange level of 1 to 3, the leader identifier L,
|
|
317
|
+
* a version of 1 or blank, and sizes in range; then the whole data descriptive record, each of
|
|
318
|
+
* its directory entries naming a field that lies within it. GDAL stops looking for updates at
|
|
319
|
+
* a file that does not open.
|
|
320
|
+
*/
|
|
321
|
+
function isIso8211(file) {
|
|
322
|
+
let fd;
|
|
323
|
+
try {
|
|
324
|
+
// GDAL's Open refuses a directory, and a file it cannot open ends the updates too.
|
|
325
|
+
if (!fs_1.default.statSync(file).isFile()) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
fd = fs_1.default.openSync(file, "r");
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const leader = Buffer.alloc(LEADER);
|
|
335
|
+
const sizes = fs_1.default.readSync(fd, leader, 0, LEADER, 0) === LEADER ? leaderSizes(leader) : undefined;
|
|
336
|
+
if (!sizes) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const record = Buffer.alloc(sizes.recLength);
|
|
340
|
+
return (fs_1.default.readSync(fd, record, 0, sizes.recLength, 0) === sizes.recLength &&
|
|
341
|
+
directoryFits(record, sizes));
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
fs_1.default.closeSync(fd);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const LEADER = 24;
|
|
351
|
+
/** DDFScanInt: atoi over a fixed width, 0 where no digits lead. */
|
|
352
|
+
const scanInt = (bytes, at, len) => Number.parseInt(bytes.toString("latin1", at, at + len), 10) || 0;
|
|
353
|
+
/** The record and entry sizes a leader gives, or undefined where GDAL refuses it. */
|
|
354
|
+
function leaderSizes(leader) {
|
|
355
|
+
const char = (at) => String.fromCharCode(leader[at]);
|
|
356
|
+
const valid = leader.every((b) => b >= 32 && b <= 126) &&
|
|
357
|
+
"123".includes(char(5)) &&
|
|
358
|
+
char(6) === "L" &&
|
|
359
|
+
"1 ".includes(char(8));
|
|
360
|
+
if (!valid) {
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
const sizes = {
|
|
364
|
+
recLength: scanInt(leader, 0, 5),
|
|
365
|
+
areaStart: scanInt(leader, 12, 5),
|
|
366
|
+
lenSize: scanInt(leader, 20, 1),
|
|
367
|
+
posSize: scanInt(leader, 21, 1),
|
|
368
|
+
tagSize: scanInt(leader, 23, 1)
|
|
369
|
+
};
|
|
370
|
+
const inRange = sizes.recLength >= LEADER &&
|
|
371
|
+
scanInt(leader, 10, 2) > 0 &&
|
|
372
|
+
sizes.areaStart >= LEADER &&
|
|
373
|
+
sizes.lenSize > 0 &&
|
|
374
|
+
sizes.posSize > 0 &&
|
|
375
|
+
sizes.tagSize > 0;
|
|
376
|
+
return inRange ? sizes : undefined;
|
|
377
|
+
}
|
|
378
|
+
/** Whether every directory entry of the data descriptive record names a field within it. */
|
|
379
|
+
function directoryFits(record, s) {
|
|
380
|
+
const width = s.lenSize + s.posSize + s.tagSize;
|
|
381
|
+
for (let e = LEADER; e + width <= s.recLength && record[e] !== 0x1e; e += width) {
|
|
382
|
+
const length = scanInt(record, e + s.tagSize, s.lenSize);
|
|
383
|
+
const position = scanInt(record, e + s.tagSize + s.lenSize, s.posSize);
|
|
384
|
+
if (position < 0 || length < 2 || s.recLength - (s.areaStart + position) < length) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
/** The EDTN and UPDN of any cell file's DSID record, an update file's among them; EDTN may be 0. */
|
|
391
|
+
function dsidVersion(file) {
|
|
392
|
+
return dsidFields(file);
|
|
393
|
+
}
|
|
264
394
|
/** The cell `readEncLayers` read from `file`, parsed as `readCellDirectory` parses an export. */
|
|
265
395
|
function encCell(file, layers, cellRef, bandOverride) {
|
|
266
396
|
const name = path_1.default.basename(file, path_1.default.extname(file));
|
package/lib/settings-service.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.V1_BACKUP = void 0;
|
|
7
|
+
exports.backupName = backupName;
|
|
7
8
|
exports.persistMigration = persistMigration;
|
|
8
9
|
exports.settingsReply = settingsReply;
|
|
9
10
|
exports.patchErrors = patchErrors;
|
|
@@ -18,17 +19,26 @@ const vessel_1 = require("./model/vessel");
|
|
|
18
19
|
const setup_checks_1 = require("./setup-checks");
|
|
19
20
|
/** Where the version 1 configuration is kept when it is first migrated. */
|
|
20
21
|
exports.V1_BACKUP = "config-v1.json";
|
|
22
|
+
/** Where a configuration of version `from` is kept when it is first migrated. */
|
|
23
|
+
function backupName(from) {
|
|
24
|
+
return `config-v${from}.json`;
|
|
25
|
+
}
|
|
21
26
|
function isObject(v) {
|
|
22
27
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
23
28
|
}
|
|
24
|
-
/** Saves a migrated configuration, after keeping
|
|
29
|
+
/** Saves a migrated configuration, after keeping the one it replaces beside the plugin data. */
|
|
25
30
|
function persistMigration(app, saved, m) {
|
|
26
31
|
if (!m.changed) {
|
|
27
32
|
return;
|
|
28
33
|
}
|
|
29
|
-
const
|
|
34
|
+
const dir = isObject(saved) ? saved.cellsDir : undefined;
|
|
35
|
+
if (typeof dir === "string" && dir.trim() !== "" && m.config.chartsProvider === undefined) {
|
|
36
|
+
app.error("the ENC cells directory setting is gone: ENC now comes only from a charts provider; " +
|
|
37
|
+
`choose it under Charts provider (the earlier settings are kept in ${backupName(m.from)})`);
|
|
38
|
+
}
|
|
39
|
+
const backup = path_1.default.join(app.getDataDirPath(), backupName(m.from));
|
|
30
40
|
try {
|
|
31
|
-
if (
|
|
41
|
+
if (!fs_1.default.existsSync(backup)) {
|
|
32
42
|
fs_1.default.writeFileSync(backup, JSON.stringify(saved ?? {}, null, 2));
|
|
33
43
|
}
|
|
34
44
|
}
|
package/lib/setup-checks.js
CHANGED
|
@@ -62,13 +62,18 @@ function suggestedSource(datums) {
|
|
|
62
62
|
}
|
|
63
63
|
function chartsCheck(s, facts) {
|
|
64
64
|
if (facts.cells > 0) {
|
|
65
|
-
|
|
65
|
+
// With no provider chosen no ENC is read, as a cells directory dropped by the version 3
|
|
66
|
+
// migration leaves it: said for as long as none is chosen.
|
|
67
|
+
const only = s.config?.chartsProviderEnc
|
|
68
|
+
? ""
|
|
69
|
+
: "; no charts provider chosen, so no ENC is read: choose the charts provider";
|
|
70
|
+
return { id: "charts", level: "ok", text: `Charts: ${facts.cells} cells indexed${only}` };
|
|
66
71
|
}
|
|
67
|
-
if (!s.config?.
|
|
72
|
+
if (!s.config?.chartsProviderEnc && !s.config?.wfs) {
|
|
68
73
|
return {
|
|
69
74
|
id: "charts",
|
|
70
75
|
level: "todo",
|
|
71
|
-
text: "Charts:
|
|
76
|
+
text: "Charts: choose the charts provider"
|
|
72
77
|
};
|
|
73
78
|
}
|
|
74
79
|
const reason = (0, public_text_1.publicReason)(s.load?.reason) ?? "no chart index loaded";
|
|
@@ -183,6 +188,6 @@ function schemaHints(app, s) {
|
|
|
183
188
|
checks: setupChecks(s, facts).map((c) => `${MARK[c.level]} ${c.text}`),
|
|
184
189
|
signalkDraft: facts.signalk.draft,
|
|
185
190
|
source: sourceHint(facts),
|
|
186
|
-
|
|
191
|
+
enc: (0, shared_enc_1.runningProviders)()
|
|
187
192
|
};
|
|
188
193
|
}
|
package/lib/shared-enc.js
CHANGED
|
@@ -3,15 +3,17 @@ 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.
|
|
7
|
-
exports.isCollectionChoice = isCollectionChoice;
|
|
6
|
+
exports.AREA_ERROR_RETRY_MS = exports.ALL_COLLECTIONS = exports.CHARTS_PROVIDER_ID = void 0;
|
|
8
7
|
exports.sharedEncApi = sharedEncApi;
|
|
9
8
|
exports.sharedEncRoot = sharedEncRoot;
|
|
10
9
|
exports.sharedCellsDir = sharedCellsDir;
|
|
11
10
|
exports.withSharedCells = withSharedCells;
|
|
11
|
+
exports.runningProviders = runningProviders;
|
|
12
|
+
exports.sharedCollections = sharedCollections;
|
|
12
13
|
exports.collectionsHint = collectionsHint;
|
|
13
14
|
exports.sharedCellsNote = sharedCellsNote;
|
|
14
15
|
exports.noteSharedCells = noteSharedCells;
|
|
16
|
+
exports.sharedCellsIn = sharedCellsIn;
|
|
15
17
|
exports.requestSharedArea = requestSharedArea;
|
|
16
18
|
exports.watchSharedCells = watchSharedCells;
|
|
17
19
|
const path_1 = __importDefault(require("path"));
|
|
@@ -20,19 +22,12 @@ const path_1 = __importDefault(require("path"));
|
|
|
20
22
|
* cells behind its ENC charts as delivered, one folder per collection (an
|
|
21
23
|
* ENC Charts set's cells, or a converted chart's):
|
|
22
24
|
* `<root>/<collection>/<CELL>/<CELL>.000` with the updates beside it. The
|
|
23
|
-
* plugin reads a collection, or the whole store, as its cells directory,
|
|
25
|
+
* plugin reads a collection, or the whole store, as its only cells directory,
|
|
24
26
|
* and is told when the cells change. See that plugin's docs/enc-store.md.
|
|
25
27
|
*/
|
|
26
28
|
exports.CHARTS_PROVIDER_ID = "signalk-charts-provider-simple";
|
|
27
29
|
/** Every collection the charts provider shares. */
|
|
28
30
|
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
31
|
/** The charts provider's ENC store API, while that plugin runs in this server. */
|
|
37
32
|
function sharedEncApi(scope = globalThis) {
|
|
38
33
|
const api = scope.__signalk_chartsProviderEnc;
|
|
@@ -59,24 +54,43 @@ function sharedCellsDir(dataDir, collection, api = sharedEncApi()) {
|
|
|
59
54
|
const root = sharedEncRoot(dataDir, api);
|
|
60
55
|
return collection === exports.ALL_COLLECTIONS ? root : path_1.default.join(root, collection);
|
|
61
56
|
}
|
|
62
|
-
/** `cfg` reading its cells from the charts provider's collection
|
|
57
|
+
/** `cfg` reading its cells from the charts provider's collection it names; none without one. */
|
|
63
58
|
function withSharedCells(cfg, dataDir, api = sharedEncApi()) {
|
|
59
|
+
const { cellsDir: _none, ...rest } = cfg;
|
|
64
60
|
return cfg.chartsProviderEnc === undefined
|
|
65
|
-
?
|
|
66
|
-
: { ...
|
|
61
|
+
? rest
|
|
62
|
+
: { ...rest, cellsDir: sharedCellsDir(dataDir, cfg.chartsProviderEnc, api) };
|
|
67
63
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The chart provider services running in this server, by id, and what Charts Provider shares
|
|
66
|
+
* now, for the admin form.
|
|
67
|
+
*/
|
|
68
|
+
function runningProviders(scope = globalThis) {
|
|
69
|
+
const api = sharedEncApi(scope);
|
|
70
|
+
const shared = collectionsHint(api);
|
|
71
|
+
return {
|
|
72
|
+
running: api ? [exports.CHARTS_PROVIDER_ID] : [],
|
|
73
|
+
...(shared === undefined ? {} : { shared })
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** The collections the charts provider shares now; undefined while it cannot tell. */
|
|
77
|
+
function sharedCollections(api = sharedEncApi()) {
|
|
70
78
|
if (!api) {
|
|
71
79
|
return undefined;
|
|
72
80
|
}
|
|
73
|
-
let collections;
|
|
74
81
|
try {
|
|
75
|
-
|
|
82
|
+
return api.collections();
|
|
76
83
|
}
|
|
77
84
|
catch {
|
|
78
85
|
return undefined;
|
|
79
86
|
}
|
|
87
|
+
}
|
|
88
|
+
/** One line naming the collections the charts provider shares. */
|
|
89
|
+
function collectionsHint(api = sharedEncApi()) {
|
|
90
|
+
const collections = sharedCollections(api);
|
|
91
|
+
if (collections === undefined) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
80
94
|
return collections.length === 0
|
|
81
95
|
? "Charts Provider shares no ENC cells yet: download an ENC Charts set there."
|
|
82
96
|
: `Shared now: ${collections
|
|
@@ -84,12 +98,13 @@ function collectionsHint(api = sharedEncApi()) {
|
|
|
84
98
|
.join(", ")}.`;
|
|
85
99
|
}
|
|
86
100
|
/**
|
|
87
|
-
* Why the charts provider's `collection` holds no cells to read, as it tells it
|
|
88
|
-
* when it is ready, or
|
|
101
|
+
* Why the charts provider's `collection` holds no cells to read, as it tells it, or that it is
|
|
102
|
+
* not running; undefined when it is ready, or cannot tell. A provider started later restarts
|
|
103
|
+
* the plugin (`watchShared`), so the note is read again then.
|
|
89
104
|
*/
|
|
90
105
|
function sharedCellsNote(collection, api = sharedEncApi()) {
|
|
91
106
|
if (!api) {
|
|
92
|
-
return
|
|
107
|
+
return "Charts Provider is not running";
|
|
93
108
|
}
|
|
94
109
|
if (collection === exports.ALL_COLLECTIONS) {
|
|
95
110
|
return collectionsHint(api)?.startsWith("Charts Provider shares no")
|
|
@@ -111,7 +126,7 @@ function sharedCellsNote(collection, api = sharedEncApi()) {
|
|
|
111
126
|
? `Charts Provider is downloading ${collection}; the index is built once its cells are shared`
|
|
112
127
|
: `${collection} is a Charts Provider chart set not downloaded yet: download it in its ENC Charts tab`;
|
|
113
128
|
case "absent":
|
|
114
|
-
return `Charts Provider shares no ${collection}: check
|
|
129
|
+
return `Charts Provider shares no ${collection}: check the charts it shares`;
|
|
115
130
|
default:
|
|
116
131
|
return undefined;
|
|
117
132
|
}
|
|
@@ -123,13 +138,36 @@ function noteSharedCells(load, cfg, api = sharedEncApi()) {
|
|
|
123
138
|
: undefined;
|
|
124
139
|
return note === undefined ? load : { ...load, reason: `${load.reason ?? ""}; ${note}` };
|
|
125
140
|
}
|
|
126
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* The stored cells of the charts provider reaching `bbox`; undefined when it cannot tell: not
|
|
143
|
+
* running, too old, or failing.
|
|
144
|
+
*/
|
|
145
|
+
function sharedCellsIn(bbox, api = sharedEncApi()) {
|
|
146
|
+
try {
|
|
147
|
+
return api?.cellsIn?.(bbox);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** When to ask again after the charts provider failed to answer, ms: it may recover. */
|
|
154
|
+
exports.AREA_ERROR_RETRY_MS = 5 * 60_000;
|
|
155
|
+
/**
|
|
156
|
+
* Asks the charts provider for the cells around `bbox`; undefined when it cannot be asked. A
|
|
157
|
+
* request that throws is a refusal asked again after AREA_ERROR_RETRY_MS, unlike one it
|
|
158
|
+
* refuses on purpose, which stays until the area changes.
|
|
159
|
+
*/
|
|
127
160
|
function requestSharedArea(bbox, api = sharedEncApi()) {
|
|
128
161
|
try {
|
|
129
162
|
return api?.requestArea?.(bbox, "signalk-underkeel");
|
|
130
163
|
}
|
|
131
164
|
catch (err) {
|
|
132
|
-
return {
|
|
165
|
+
return {
|
|
166
|
+
accepted: false,
|
|
167
|
+
started: false,
|
|
168
|
+
reason: err.message,
|
|
169
|
+
retryAfterMs: exports.AREA_ERROR_RETRY_MS
|
|
170
|
+
};
|
|
133
171
|
}
|
|
134
172
|
}
|
|
135
173
|
/**
|
package/lib/wfs-service.js
CHANGED
|
@@ -12,6 +12,7 @@ const box_sizing_1 = require("./box-sizing");
|
|
|
12
12
|
const box_status_1 = require("./box-status");
|
|
13
13
|
const cell_reader_1 = require("./ingest/cell-reader");
|
|
14
14
|
const wfs_1 = require("./ingest/wfs");
|
|
15
|
+
const shared_enc_1 = require("./shared-enc");
|
|
15
16
|
const build_box_1 = require("./spatial/build-box");
|
|
16
17
|
const chart_position_1 = require("./spatial/chart-position");
|
|
17
18
|
const chart_index_1 = require("./spatial/chart-index");
|
|
@@ -53,7 +54,7 @@ class WfsService {
|
|
|
53
54
|
indexed;
|
|
54
55
|
/** The box of the last index this service handed over in this run. */
|
|
55
56
|
handed;
|
|
56
|
-
/** The box
|
|
57
|
+
/** The box the charts provider is to be asked about again, and when. */
|
|
57
58
|
again;
|
|
58
59
|
/** The saved WFS cell was missing or unreadable at start: the box is downloaded again. */
|
|
59
60
|
cellLost = false;
|
|
@@ -136,12 +137,14 @@ class WfsService {
|
|
|
136
137
|
view() {
|
|
137
138
|
const bbox = this.handed ?? this.box?.bbox;
|
|
138
139
|
const lastMs = this.box?.durationMs;
|
|
140
|
+
const note = this.deps.area?.note();
|
|
139
141
|
return {
|
|
140
142
|
...(bbox ? { bbox } : {}),
|
|
141
143
|
rebuilding: this.busy,
|
|
142
144
|
failed: !this.busy && this.lastError !== undefined,
|
|
143
145
|
...(lastMs === undefined ? {} : { lastMs }),
|
|
144
|
-
summary: this.status().summary
|
|
146
|
+
summary: this.status().summary,
|
|
147
|
+
...(note === undefined ? {} : { providerNote: note })
|
|
145
148
|
};
|
|
146
149
|
}
|
|
147
150
|
/** The cap the box grows to, never below the configured radius. */
|
|
@@ -265,8 +268,19 @@ class WfsService {
|
|
|
265
268
|
this.ceilingKm = Math.max(min, Math.floor((radiusKm * 0.75) / box_sizing_1.STEP_KM) * box_sizing_1.STEP_KM);
|
|
266
269
|
}
|
|
267
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Tells the charts provider of the box; advisory, so a failure never stops the refresh. A
|
|
273
|
+
* failed ask is tried again later, as a provider that failed to answer may recover.
|
|
274
|
+
*/
|
|
268
275
|
announce(bbox, now) {
|
|
269
|
-
|
|
276
|
+
let ms;
|
|
277
|
+
try {
|
|
278
|
+
ms = this.deps.area?.ask(bbox);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
this.deps.error(`${this.label}: asking for the area's cells failed: ${err.message}`);
|
|
282
|
+
ms = shared_enc_1.AREA_ERROR_RETRY_MS;
|
|
283
|
+
}
|
|
270
284
|
this.again = ms === undefined ? undefined : { bbox, at: now + ms };
|
|
271
285
|
}
|
|
272
286
|
announceAgain(now) {
|
|
@@ -315,6 +329,8 @@ class WfsService {
|
|
|
315
329
|
}
|
|
316
330
|
: undefined,
|
|
317
331
|
cellsDir,
|
|
332
|
+
// WFS depths are built whatever ENC the charts provider has, but an index of
|
|
333
|
+
// charted hazards is kept while its cells are away, not replaced by depths alone.
|
|
318
334
|
cellsRequired: !("url" in config) || cells === true,
|
|
319
335
|
box: inBox ? bbox : undefined,
|
|
320
336
|
build: {
|
package/lib/wfs-wiring.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.AREA_ASK_MAX_MS = exports.AREA_ASK_MIN_MS = void 0;
|
|
7
7
|
exports.startBox = startBox;
|
|
8
8
|
exports.areaRequests = areaRequests;
|
|
9
|
+
exports.areaNote = areaNote;
|
|
9
10
|
exports.startWfs = startWfs;
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
@@ -84,47 +85,84 @@ function startBox(w) {
|
|
|
84
85
|
: undefined;
|
|
85
86
|
}
|
|
86
87
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
88
|
+
* The asking a configuration does: every new box asks the chosen charts provider for its
|
|
89
|
+
* cells, which it downloads into a chart set of its own; none without a provider.
|
|
89
90
|
*/
|
|
90
91
|
function areaRequests(config, app) {
|
|
91
|
-
return config.
|
|
92
|
-
? askForArea(app)
|
|
93
|
-
: undefined;
|
|
92
|
+
return config.chartsProviderEnc === shared_enc_1.ALL_COLLECTIONS ? askForArea(app) : undefined;
|
|
94
93
|
}
|
|
95
94
|
/** How soon, and how late, a box is asked about again: Charts Provider starting, or busy. */
|
|
96
95
|
exports.AREA_ASK_MIN_MS = 60_000;
|
|
97
96
|
exports.AREA_ASK_MAX_MS = 30 * 60_000;
|
|
98
97
|
const ASK_STARTING_MS = 5 * 60_000;
|
|
98
|
+
/**
|
|
99
|
+
* What the provider said of a box, as the status gives it when the vessel has no chart
|
|
100
|
+
* coverage: from its answer to the area request and the cells it has stored there (`cells`,
|
|
101
|
+
* undefined when it cannot tell). Undefined when it has cells there and is not downloading:
|
|
102
|
+
* a gap at the vessel is then the charts'.
|
|
103
|
+
*/
|
|
104
|
+
function areaNote(answer, cells, running = (0, shared_enc_1.sharedEncApi)() !== undefined) {
|
|
105
|
+
if (answer === undefined) {
|
|
106
|
+
return unaskedNote(running);
|
|
107
|
+
}
|
|
108
|
+
if (answer.started) {
|
|
109
|
+
return "Charts Provider is downloading the ENC around the vessel";
|
|
110
|
+
}
|
|
111
|
+
if (cells === undefined) {
|
|
112
|
+
return `Charts Provider: ${answer.reason}`;
|
|
113
|
+
}
|
|
114
|
+
if (cells.length > 0) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
// Accepted and not started: no free ENC covers the area, or its download waits for the
|
|
118
|
+
// internet connection; the provider's reason says which.
|
|
119
|
+
return answer.accepted
|
|
120
|
+
? `Charts Provider has no ENC for this area: ${answer.reason}`
|
|
121
|
+
: `Charts Provider has no ENC for this area and does not download it: ${answer.reason}`;
|
|
122
|
+
}
|
|
123
|
+
/** Why the charts provider could not be asked: not running, or a version without `requestArea`. */
|
|
124
|
+
function unaskedNote(running) {
|
|
125
|
+
return running
|
|
126
|
+
? "Charts Provider is too old to be asked for the ENC around the vessel: update it"
|
|
127
|
+
: "Charts Provider is not running";
|
|
128
|
+
}
|
|
99
129
|
/**
|
|
100
130
|
* Asks the charts provider for each box's cells, logging its answer when it changes, and says
|
|
101
131
|
* when to ask again: when the provider says so, or is not running yet.
|
|
102
132
|
*/
|
|
103
133
|
function askForArea(app) {
|
|
104
134
|
let last;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
135
|
+
let note;
|
|
136
|
+
return {
|
|
137
|
+
// A provider stopped since its answer leaves that answer stale: it is not running now.
|
|
138
|
+
note: () => ((0, shared_enc_1.sharedEncApi)() === undefined ? unaskedNote(false) : note),
|
|
139
|
+
ask: (bbox) => {
|
|
140
|
+
const api = (0, shared_enc_1.sharedEncApi)();
|
|
141
|
+
const answer = (0, shared_enc_1.requestSharedArea)(bbox, api);
|
|
142
|
+
note = areaNote(answer, answer === undefined ? undefined : (0, shared_enc_1.sharedCellsIn)(bbox, api), !!api);
|
|
143
|
+
const line = answer === undefined
|
|
144
|
+
? unaskedNote(!!api)
|
|
145
|
+
: `Charts Provider, asked for the ENC around the vessel: ${answer.reason}`;
|
|
146
|
+
if (line !== last) {
|
|
147
|
+
last = line;
|
|
148
|
+
if (answer?.accepted === true) {
|
|
149
|
+
app.debug(line);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
app.error(line);
|
|
153
|
+
}
|
|
117
154
|
}
|
|
155
|
+
const ms = answer === undefined ? ASK_STARTING_MS : answer.retryAfterMs;
|
|
156
|
+
return typeof ms === "number" && Number.isFinite(ms)
|
|
157
|
+
? Math.min(exports.AREA_ASK_MAX_MS, Math.max(exports.AREA_ASK_MIN_MS, ms))
|
|
158
|
+
: undefined;
|
|
118
159
|
}
|
|
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
160
|
};
|
|
124
161
|
}
|
|
125
162
|
function startWfs(w, config, dirName = "wfs") {
|
|
126
163
|
const dir = path_1.default.join(w.app.getDataDirPath(), dirName);
|
|
127
164
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
165
|
+
const area = areaRequests(w.config, w.app);
|
|
128
166
|
return new wfs_service_1.WfsService({
|
|
129
167
|
config,
|
|
130
168
|
dir,
|
|
@@ -132,7 +170,7 @@ function startWfs(w, config, dirName = "wfs") {
|
|
|
132
170
|
cacheDir: (0, index_loader_1.cellCacheDir)(w.app.getDataDirPath()),
|
|
133
171
|
leafZoom: w.config.leafZoom,
|
|
134
172
|
source: (0, index_source_1.indexSource)(w.config),
|
|
135
|
-
|
|
173
|
+
area,
|
|
136
174
|
cellsDir: w.config.cellsDir,
|
|
137
175
|
inBox: w.config.boxRadiusKm > 0,
|
|
138
176
|
index: () => w.state.load?.index,
|
|
@@ -147,11 +185,14 @@ function startWfs(w, config, dirName = "wfs") {
|
|
|
147
185
|
onIndex: (index, changed) => useIndex(w, index, changed),
|
|
148
186
|
error: (m) => {
|
|
149
187
|
w.app.error(m);
|
|
150
|
-
// With no index in use the failure is why the look-ahead is off: an error, not "waiting"
|
|
188
|
+
// With no index in use the failure is why the look-ahead is off: an error, not "waiting",
|
|
189
|
+
// with what the charts provider said of the box, as when it is downloading its cells.
|
|
151
190
|
const { load, monitor } = w.state;
|
|
152
191
|
if (load && !load.index) {
|
|
153
|
-
|
|
154
|
-
|
|
192
|
+
const note = area?.note();
|
|
193
|
+
const reason = note === undefined ? m : `${m}; ${note}`;
|
|
194
|
+
w.state.load = { ...load, reason };
|
|
195
|
+
monitor?.noIndex(reason);
|
|
155
196
|
}
|
|
156
197
|
}
|
|
157
198
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-underkeel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Under-keel clearance awareness for Signal K: look-ahead grounding risk, route checks and a living chart.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"keywords": [
|
|
20
20
|
"signalk-node-server-plugin",
|
|
21
21
|
"signalk-category-notifications",
|
|
22
|
+
"signalk-category-plotter-extensions",
|
|
22
23
|
"signalk-category-chart-plotters"
|
|
23
24
|
],
|
|
24
25
|
"signalk": {
|