signalk-underkeel 0.5.1 → 0.6.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 +19 -0
- package/README.md +9 -4
- package/lib/box-status.js +19 -0
- package/lib/cell-warm.js +110 -0
- package/lib/index-loader.js +5 -2
- package/lib/index.js +60 -5
- package/lib/ingest/cell-cache.js +54 -12
- package/lib/ingest/enc-read-process.js +5 -3
- package/lib/monitor.js +4 -4
- package/lib/shared-enc.js +27 -8
- package/lib/spatial/build-survey.js +8 -3
- package/lib/status-report.js +5 -3
- package/lib/wfs-service.js +38 -28
- package/lib/wfs-wiring.js +41 -8
- package/lib/wfs-worker.js +30 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to signalk-underkeel are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-11-10
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Charts Provider sharing its store after this plugin started, as it does at every server
|
|
10
|
+
start once its charts are loaded, no longer restarts the plugin when the store holds the
|
|
11
|
+
cells read at start; only the note that it was not running goes.
|
|
12
|
+
- The first chart box is built as soon as the first position arrives, not at the next 10 s
|
|
13
|
+
check. Until it is in use the reason says the box is being built and how far it has come,
|
|
14
|
+
for example `building the chart box around the vessel: reading cells 3/7`, and the cells
|
|
15
|
+
phase counts each cell as it is read instead of jumping from 0 to all.
|
|
16
|
+
- A charts provider still starting, or one that does not download an area, goes to the debug
|
|
17
|
+
log instead of the error log; one too old to be asked, or whose answer failed, stays an
|
|
18
|
+
error.
|
|
19
|
+
- Once a chart box is in use, the cells the next box can reach are read into the cell cache by
|
|
20
|
+
a child process at the lowest CPU priority, so moving the box over them reads none during the
|
|
21
|
+
rebuild: after a 6 km move the Waddenzee box rebuilt in 8.6 s against 34 s for its first
|
|
22
|
+
build (x86).
|
|
23
|
+
|
|
5
24
|
## [0.5.1] - 2026-11-10
|
|
6
25
|
|
|
7
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -144,8 +144,10 @@ phase, start and the last build's duration. At speed the box grows past its radi
|
|
|
144
144
|
**Largest index box at speed** (0 sizes it from the RAM), so the vessel cannot reach the edge
|
|
145
145
|
before the next box is built (docs/HOW-IT-WORKS.md §12). A radius under 2 km is raised to 2.
|
|
146
146
|
Until the first position arrives the engine is `off`
|
|
147
|
-
with the reason `waiting for the vessel position to build the chart box
|
|
148
|
-
|
|
147
|
+
with the reason `waiting for the vessel position to build the chart box`; the box is built
|
|
148
|
+
as soon as one does, and meanwhile the reason says how far it has come, for example
|
|
149
|
+
`building the chart box around the vessel: reading cells 3/7`. Set the box to 0 to index every
|
|
150
|
+
cell.
|
|
149
151
|
|
|
150
152
|
At the first start the plugin builds a chart index from the cells in a child process and saves
|
|
151
153
|
it under the plugin data directory, `~/.signalk/plugin-config-data/signalk-underkeel/index`
|
|
@@ -157,8 +159,11 @@ changed; `GET /status` names the cells that caused the rebuild. **Rebuild index
|
|
|
157
159
|
forces a rebuild on every start. A cell folder may be a symbolic link. What the build read
|
|
158
160
|
from each cell is kept, compressed, in `cell-cache` in the plugin data directory (at most 512 MB), so a
|
|
159
161
|
chart box refresh reads again only the cells whose files changed: the 10 km Waddenzee box
|
|
160
|
-
rebuilds in 12–15 s instead of 30–34 s on x86.
|
|
161
|
-
|
|
162
|
+
rebuilds in 12–15 s instead of 30–34 s on x86. Once a box is in use, a child process at the
|
|
163
|
+
lowest CPU priority reads into it the cells the next box can reach, so a box moved over cells it
|
|
164
|
+
had not met reads none during the rebuild: after a 6 km move the Waddenzee box was rebuilt in
|
|
165
|
+
8.6 s against 34 s for its first build (x86). A rebuild ends that process first. Deleting the
|
|
166
|
+
folder is safe; the cells are then read again.
|
|
162
167
|
|
|
163
168
|
Folders of GeoJSON layers exported by `scripts/enc-to-geojson.sh` for earlier versions are no
|
|
164
169
|
longer read: convert the original `.000` cells in the charts provider instead. Such a folder
|
package/lib/box-status.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPending = void 0;
|
|
3
4
|
exports.rebuildStatus = rebuildStatus;
|
|
5
|
+
exports.noIndexReason = noIndexReason;
|
|
6
|
+
exports.phaseText = phaseText;
|
|
4
7
|
exports.boxSummary = boxSummary;
|
|
5
8
|
const box_sizing_1 = require("./box-sizing");
|
|
6
9
|
const chart_box_1 = require("./engine/chart-box");
|
|
10
|
+
const index_loader_1 = require("./index-loader");
|
|
7
11
|
function rebuildStatus(r, now) {
|
|
8
12
|
return {
|
|
9
13
|
startedAt: new Date(r.startedAt).toISOString(),
|
|
@@ -13,6 +17,21 @@ function rebuildStatus(r, now) {
|
|
|
13
17
|
...(r.estimateMs === undefined ? {} : { estimateMs: r.estimateMs })
|
|
14
18
|
};
|
|
15
19
|
}
|
|
20
|
+
/** Whether `reason` is one of PENDING_REASONS, maybe with a note (`; …`) or detail (`: …`). */
|
|
21
|
+
const isPending = (reason) => [...index_loader_1.PENDING_REASONS].some((p) => reason.startsWith(p) && /^(?:$|[;:] )/.test(reason.slice(p.length)));
|
|
22
|
+
exports.isPending = isPending;
|
|
23
|
+
/**
|
|
24
|
+
* Why there is no index: `reason`, or what the first chart box build is doing while it runs,
|
|
25
|
+
* as the vessel position it waited for has come.
|
|
26
|
+
*/
|
|
27
|
+
function noIndexReason(reason, box) {
|
|
28
|
+
const why = reason ?? "no chart index loaded";
|
|
29
|
+
if (box?.rebuilding && why.startsWith(index_loader_1.BOX_WAITING_REASON)) {
|
|
30
|
+
return box.phase ? `${index_loader_1.BOX_BUILDING_REASON}: ${box.phase}` : index_loader_1.BOX_BUILDING_REASON;
|
|
31
|
+
}
|
|
32
|
+
return why;
|
|
33
|
+
}
|
|
34
|
+
/** What a box build in progress is doing, with its count where it gave one. */
|
|
16
35
|
function phaseText(r) {
|
|
17
36
|
const of = r.done !== undefined && r.total !== undefined ? ` ${r.done}/${r.total}` : "";
|
|
18
37
|
switch (r.phase) {
|
package/lib/cell-warm.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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.warmCells = warmCells;
|
|
7
|
+
exports.startWarming = startWarming;
|
|
8
|
+
exports.boxWarmer = boxWarmer;
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const worker_threads_1 = require("worker_threads");
|
|
12
|
+
const child_process_1 = require("./foundation/child-process");
|
|
13
|
+
const index_loader_1 = require("./index-loader");
|
|
14
|
+
const cell_cache_1 = require("./ingest/cell-cache");
|
|
15
|
+
const cell_source_1 = require("./ingest/cell-source");
|
|
16
|
+
const enc_read_process_1 = require("./ingest/enc-read-process");
|
|
17
|
+
const build_box_1 = require("./spatial/build-box");
|
|
18
|
+
/** Reads the cells of `job.around` the cache lacks into it; resolves with how many were read. */
|
|
19
|
+
async function warmCells(job, reader = enc_read_process_1.readInProcess) {
|
|
20
|
+
const cells = (0, index_loader_1.cellSources)(job.cellsDir).filter(cell_source_1.isEncFile);
|
|
21
|
+
const cache = cell_cache_1.LayerCache.open(job.cacheDir, cells);
|
|
22
|
+
if (!cache) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
cache.hold((0, build_box_1.boxCells)(cells, job.hold, job.cacheDir));
|
|
26
|
+
const want = (0, build_box_1.boxCells)(cells, job.around, job.cacheDir).filter((f) => !cache.has(f));
|
|
27
|
+
let read = 0;
|
|
28
|
+
for (const file of want) {
|
|
29
|
+
const layers = (await reader([file])).get(file);
|
|
30
|
+
if (layers) {
|
|
31
|
+
cache.put(file, layers);
|
|
32
|
+
read++;
|
|
33
|
+
}
|
|
34
|
+
// One cell's layers held at a time.
|
|
35
|
+
await cache.flush();
|
|
36
|
+
}
|
|
37
|
+
return read;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Runs `job` in a child process from the compiled `script`, its heap capped at `heapMb`;
|
|
41
|
+
* `done` gets its reply, or why it ended without one, unless it was stopped.
|
|
42
|
+
*/
|
|
43
|
+
function startWarming(job, heapMb, done, script = path_1.default.join(__dirname, "cell-warm.js")) {
|
|
44
|
+
const { child, stderr } = (0, child_process_1.forkLimited)(script, heapMb, true);
|
|
45
|
+
let stopped = false;
|
|
46
|
+
let reply;
|
|
47
|
+
const closed = new Promise((resolve) => {
|
|
48
|
+
child.once("close", (code) => {
|
|
49
|
+
if (!stopped) {
|
|
50
|
+
done(reply ?? { error: `cell cache warming exited with ${code ?? "a signal"}: ${stderr()}` });
|
|
51
|
+
}
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
child.once("error", () => undefined);
|
|
56
|
+
child.on("message", (m) => (reply = m));
|
|
57
|
+
child.send(job, () => undefined);
|
|
58
|
+
return {
|
|
59
|
+
stop: () => {
|
|
60
|
+
stopped = true;
|
|
61
|
+
(0, child_process_1.killGroup)(child);
|
|
62
|
+
return closed;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** One run at a time through `start`; a run that cannot start is reported to `error`. */
|
|
67
|
+
function boxWarmer(start, error) {
|
|
68
|
+
let running;
|
|
69
|
+
let stopped = false;
|
|
70
|
+
const end = () => {
|
|
71
|
+
const r = running;
|
|
72
|
+
running = undefined;
|
|
73
|
+
return r ? r.stop() : Promise.resolve();
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
boxInUse: (bbox, reachKm) => {
|
|
77
|
+
if (stopped) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
void end();
|
|
81
|
+
try {
|
|
82
|
+
running = start((0, build_box_1.growBbox)(bbox, reachKm), bbox);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
error(`filling the cell cache failed: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
beforeBuild: end,
|
|
89
|
+
stop: () => {
|
|
90
|
+
stopped = true;
|
|
91
|
+
void end();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (worker_threads_1.isMainThread && require.main === module && process.send) {
|
|
96
|
+
(0, child_process_1.exitWithParent)();
|
|
97
|
+
try {
|
|
98
|
+
// Behind the server and the look-ahead, whatever the cores.
|
|
99
|
+
os_1.default.setPriority(19);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Not permitted: it runs at the server's priority.
|
|
103
|
+
}
|
|
104
|
+
process.once("message", (job) => {
|
|
105
|
+
warmCells(job)
|
|
106
|
+
.then((read) => ({ read }), (err) => ({ error: err.message }))
|
|
107
|
+
.then(child_process_1.sendToParent)
|
|
108
|
+
.finally(() => process.exit(0));
|
|
109
|
+
});
|
|
110
|
+
}
|
package/lib/index-loader.js
CHANGED
|
@@ -3,7 +3,7 @@ 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.failedLoad = exports.cellCacheDir = exports.SYNC_LOAD_BYTES = exports.PENDING_REASONS = exports.BOX_WAITING_REASON = exports.WFS_WAITING_REASON = exports.LOADING_REASON = exports.BUILDING_REASON = void 0;
|
|
6
|
+
exports.failedLoad = exports.cellCacheDir = exports.SYNC_LOAD_BYTES = exports.PENDING_REASONS = exports.BOX_BUILDING_REASON = exports.BOX_WAITING_REASON = exports.WFS_WAITING_REASON = exports.LOADING_REASON = exports.BUILDING_REASON = void 0;
|
|
7
7
|
exports.cellSources = cellSources;
|
|
8
8
|
exports.cellsMissing = cellsMissing;
|
|
9
9
|
exports.savedCatalogue = savedCatalogue;
|
|
@@ -27,12 +27,15 @@ exports.BUILDING_REASON = "building chart index";
|
|
|
27
27
|
exports.LOADING_REASON = "loading chart index";
|
|
28
28
|
exports.WFS_WAITING_REASON = "waiting for the first Traficom WFS download";
|
|
29
29
|
exports.BOX_WAITING_REASON = "waiting for the vessel position to build the chart box";
|
|
30
|
+
/** Why there is no index while the first chart box around the vessel is built. */
|
|
31
|
+
exports.BOX_BUILDING_REASON = "building the chart box around the vessel";
|
|
30
32
|
/** Reasons that mean the index is on its way, not that loading it failed. */
|
|
31
33
|
exports.PENDING_REASONS = new Set([
|
|
32
34
|
exports.BUILDING_REASON,
|
|
33
35
|
exports.LOADING_REASON,
|
|
34
36
|
exports.WFS_WAITING_REASON,
|
|
35
|
-
exports.BOX_WAITING_REASON
|
|
37
|
+
exports.BOX_WAITING_REASON,
|
|
38
|
+
exports.BOX_BUILDING_REASON
|
|
36
39
|
]);
|
|
37
40
|
/**
|
|
38
41
|
* A saved pyramid.bin larger than this is read and inflated off the event
|
package/lib/index.js
CHANGED
|
@@ -66,7 +66,7 @@ function stopPlugin(app, clock, state) {
|
|
|
66
66
|
* Restarts the plugin once the shared ENC cells it reads changed: a start compares them with the
|
|
67
67
|
* saved index and rebuilds it when they did. A server without a restart callback logs instead.
|
|
68
68
|
*/
|
|
69
|
-
function watchShared(app, clock, collection, restart) {
|
|
69
|
+
function watchShared(app, clock, collection, restart, sameCells) {
|
|
70
70
|
return (0, shared_enc_1.watchSharedCells)(collection, clock, () => {
|
|
71
71
|
if (restart) {
|
|
72
72
|
restart();
|
|
@@ -74,7 +74,64 @@ function watchShared(app, clock, collection, restart) {
|
|
|
74
74
|
else {
|
|
75
75
|
app.error("the shared ENC cells changed; restart the plugin to index them");
|
|
76
76
|
}
|
|
77
|
-
});
|
|
77
|
+
}, sameCells ? { sameCells } : {});
|
|
78
|
+
}
|
|
79
|
+
/** The stamps of the S-57 cells under `dir`, as one string; undefined when it cannot be read. */
|
|
80
|
+
function cellsStamp(dir) {
|
|
81
|
+
try {
|
|
82
|
+
return (0, index_loader_1.sourceKeys)((0, index_loader_1.cellSources)(dir)).join("\n");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Watches the charts provider's cells in use (`watchShared`). A provider publishing later the
|
|
90
|
+
* cells read at start restarts nothing: the reason shown loses its note that it had not started.
|
|
91
|
+
*/
|
|
92
|
+
function watchAtStart(app, clock, state, plan, restart) {
|
|
93
|
+
const { config, load } = state;
|
|
94
|
+
const collection = config?.chartsProviderEnc;
|
|
95
|
+
if (!config || collection === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const same = sameCellsAsStart(config.cellsDir, collection);
|
|
99
|
+
const renote = () => {
|
|
100
|
+
if ("load" in plan && state.load === load && load?.reason !== undefined) {
|
|
101
|
+
const fresh = (0, shared_enc_1.noteSharedCells)(plan.load, config);
|
|
102
|
+
state.load = fresh;
|
|
103
|
+
if (!fresh.index && fresh.reason !== undefined) {
|
|
104
|
+
state.monitor?.noIndex(fresh.reason);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const quiet = same &&
|
|
109
|
+
((root) => {
|
|
110
|
+
const unchanged = same(root);
|
|
111
|
+
if (unchanged) {
|
|
112
|
+
renote();
|
|
113
|
+
}
|
|
114
|
+
return unchanged;
|
|
115
|
+
});
|
|
116
|
+
return watchShared(app, clock, collection, restart, quiet);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Whether a charts provider that publishes its store later than this plugin starts, as it does
|
|
120
|
+
* once its charts are loaded, publishes the cells read at start: the same directory, the same
|
|
121
|
+
* cells. Undefined when the provider was there at start, or nothing was read.
|
|
122
|
+
*/
|
|
123
|
+
function sameCellsAsStart(cellsDir, collection) {
|
|
124
|
+
if ((0, shared_enc_1.sharedEncApi)() !== undefined || cellsDir === undefined) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const stamp = cellsStamp(cellsDir);
|
|
128
|
+
if (stamp === undefined) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return (root) => {
|
|
132
|
+
const dir = collection === shared_enc_1.ALL_COLLECTIONS ? root : path_1.default.join(root, collection);
|
|
133
|
+
return path_1.default.resolve(dir) === path_1.default.resolve(cellsDir) && cellsStamp(dir) === stamp;
|
|
134
|
+
};
|
|
78
135
|
}
|
|
79
136
|
/**
|
|
80
137
|
* The admin form for this server: the essentials with expert settings
|
|
@@ -145,9 +202,7 @@ module.exports = function underkeel(app, clock = monitor_1.SYSTEM_CLOCK, fetch =
|
|
|
145
202
|
state.zoneErrors = errors;
|
|
146
203
|
errors.forEach((e) => app.error(e));
|
|
147
204
|
});
|
|
148
|
-
shared = config
|
|
149
|
-
? watchShared(app, clock, config.chartsProviderEnc, restart && (() => restart(migration.config)))
|
|
150
|
-
: undefined;
|
|
205
|
+
shared = watchAtStart(app, clock, state, plan, restart && (() => restart(migration.config)));
|
|
151
206
|
(0, assets_1.mountAssets)(app);
|
|
152
207
|
app.registerResourceProvider((0, manifest_1.manifestProvider)(VERSION));
|
|
153
208
|
},
|
package/lib/ingest/cell-cache.js
CHANGED
|
@@ -68,8 +68,8 @@ function writeQuietly(file, data) {
|
|
|
68
68
|
fs_1.default.rmSync(tmp, { force: true });
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
/** The
|
|
72
|
-
function
|
|
71
|
+
/** The entry's header and where its body lies; "corrupt" for a damaged entry. */
|
|
72
|
+
function entryHead(bytes) {
|
|
73
73
|
const end = bytes.length - SUM_BYTES;
|
|
74
74
|
const headAt = MAGIC.length + 4;
|
|
75
75
|
if (end < headAt || !bytes.subarray(0, MAGIC.length).equals(MAGIC)) {
|
|
@@ -79,16 +79,27 @@ function decodeEntry(bytes, key) {
|
|
|
79
79
|
return "corrupt";
|
|
80
80
|
}
|
|
81
81
|
const bodyAt = headAt + bytes.readUInt32LE(MAGIC.length);
|
|
82
|
-
|
|
82
|
+
try {
|
|
83
|
+
return bodyAt > end
|
|
84
|
+
? "corrupt"
|
|
85
|
+
: { head: JSON.parse(bytes.toString("utf8", headAt, bodyAt)), bodyAt, end };
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return "corrupt";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The layers in `bytes` filed under `key`; "stale" for another key, "corrupt" for a damaged entry. */
|
|
92
|
+
function decodeEntry(bytes, key) {
|
|
93
|
+
const entry = entryHead(bytes);
|
|
94
|
+
if (entry === "corrupt") {
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
const { head, bodyAt, end } = entry;
|
|
98
|
+
if (head.key !== key) {
|
|
99
|
+
return "stale";
|
|
100
|
+
}
|
|
83
101
|
let raw;
|
|
84
102
|
try {
|
|
85
|
-
if (bodyAt > end) {
|
|
86
|
-
return "corrupt";
|
|
87
|
-
}
|
|
88
|
-
head = JSON.parse(bytes.toString("utf8", headAt, bodyAt));
|
|
89
|
-
if (head.key !== key) {
|
|
90
|
-
return "stale";
|
|
91
|
-
}
|
|
92
103
|
raw = zlib_1.default.brotliDecompressSync(bytes.subarray(bodyAt, end));
|
|
93
104
|
}
|
|
94
105
|
catch {
|
|
@@ -188,6 +199,36 @@ class LayerCache {
|
|
|
188
199
|
}
|
|
189
200
|
return typeof layers === "string" ? undefined : layers;
|
|
190
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether an entry for `file` as its files are now is kept, checked whole but not
|
|
204
|
+
* decompressed; a damaged one is removed.
|
|
205
|
+
*/
|
|
206
|
+
has(file) {
|
|
207
|
+
try {
|
|
208
|
+
const { name, key } = this.identify(file, (0, enc_wasm_1.encStamp)(file));
|
|
209
|
+
const entry = path_1.default.join(this.dir, name);
|
|
210
|
+
const found = entryHead(fs_1.default.readFileSync(entry));
|
|
211
|
+
if (found === "corrupt") {
|
|
212
|
+
fs_1.default.rmSync(entry, { force: true });
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return found.head.key === key;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Keeps the entries of `files` from being dropped to make room, as if this build used them. */
|
|
222
|
+
hold(files) {
|
|
223
|
+
for (const file of files) {
|
|
224
|
+
try {
|
|
225
|
+
this.used.add(entryName(fs_1.default.realpathSync(file)));
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Gone: nothing to keep.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
191
232
|
/**
|
|
192
233
|
* Files the layers just read from `file`, compressed off the event loop; `flush` waits for
|
|
193
234
|
* the write. Layers of a cell whose files changed while it was read are not kept: they are
|
|
@@ -276,19 +317,20 @@ exports.LayerCache = LayerCache;
|
|
|
276
317
|
* from, and filing those it reads. With every cell kept, gdal3.js is not started.
|
|
277
318
|
*/
|
|
278
319
|
function cachedReader(reader, cache) {
|
|
279
|
-
return async (files) => {
|
|
320
|
+
return async (files, onRead) => {
|
|
280
321
|
const out = new Map();
|
|
281
322
|
const read = [];
|
|
282
323
|
for (const file of new Set(files)) {
|
|
283
324
|
const layers = cache.get(file);
|
|
284
325
|
if (layers) {
|
|
285
326
|
out.set(file, layers);
|
|
327
|
+
onRead?.(file);
|
|
286
328
|
}
|
|
287
329
|
else {
|
|
288
330
|
read.push(file);
|
|
289
331
|
}
|
|
290
332
|
}
|
|
291
|
-
for (const [file, layers] of await reader(read)) {
|
|
333
|
+
for (const [file, layers] of await reader(read, onRead)) {
|
|
292
334
|
out.set(file, layers);
|
|
293
335
|
cache.put(file, layers);
|
|
294
336
|
}
|
|
@@ -6,10 +6,11 @@ const worker_threads_1 = require("worker_threads");
|
|
|
6
6
|
const child_process_1 = require("../foundation/child-process");
|
|
7
7
|
const enc_wasm_1 = require("./enc-wasm");
|
|
8
8
|
/** Reads on the calling thread; the WebAssembly memory stays with it. */
|
|
9
|
-
const readInProcess = async (files) => {
|
|
9
|
+
const readInProcess = async (files, onRead) => {
|
|
10
10
|
const out = new Map();
|
|
11
|
-
for (const file of files) {
|
|
11
|
+
for (const file of new Set(files)) {
|
|
12
12
|
out.set(file, await (0, enc_wasm_1.readEncLayers)(file));
|
|
13
|
+
onRead?.(file);
|
|
13
14
|
}
|
|
14
15
|
return out;
|
|
15
16
|
};
|
|
@@ -20,7 +21,7 @@ exports.readInProcess = readInProcess;
|
|
|
20
21
|
* that exits before every cell has come back fails the read.
|
|
21
22
|
*/
|
|
22
23
|
function readInChild(heapMb, script = __filename) {
|
|
23
|
-
return (files) => {
|
|
24
|
+
return (files, onRead) => {
|
|
24
25
|
const want = [...new Set(files)];
|
|
25
26
|
if (want.length === 0) {
|
|
26
27
|
return Promise.resolve(new Map());
|
|
@@ -35,6 +36,7 @@ function readInChild(heapMb, script = __filename) {
|
|
|
35
36
|
}
|
|
36
37
|
else if (m.file !== undefined && m.layers) {
|
|
37
38
|
out.set(m.file, (0, enc_wasm_1.sharedLayers)(new Map(Object.entries(m.layers))));
|
|
39
|
+
onRead?.(m.file);
|
|
38
40
|
}
|
|
39
41
|
});
|
|
40
42
|
child.once("error", (err) => (failure ??= err));
|
package/lib/monitor.js
CHANGED
|
@@ -11,7 +11,7 @@ const notify_1 = require("./engine/notify");
|
|
|
11
11
|
const state_1 = require("./foundation/state");
|
|
12
12
|
const integrity_1 = require("./model/integrity");
|
|
13
13
|
const vessel_1 = require("./model/vessel");
|
|
14
|
-
const
|
|
14
|
+
const box_status_1 = require("./box-status");
|
|
15
15
|
const wfs_1 = require("./ingest/wfs");
|
|
16
16
|
const water_level_service_1 = require("./water-level-service");
|
|
17
17
|
const notifications_1 = require("./notifications");
|
|
@@ -262,7 +262,7 @@ class Monitor {
|
|
|
262
262
|
if (!this.index) {
|
|
263
263
|
return {
|
|
264
264
|
state: "off",
|
|
265
|
-
reason: this.indexReason
|
|
265
|
+
reason: (0, box_status_1.noIndexReason)(this.indexReason, this.chartBox?.()),
|
|
266
266
|
clearance: null,
|
|
267
267
|
clearanceSigma: null,
|
|
268
268
|
risk: null,
|
|
@@ -331,8 +331,8 @@ class Monitor {
|
|
|
331
331
|
const now = this.clock.now();
|
|
332
332
|
if (!this.index) {
|
|
333
333
|
// The engine is off either way; a pending reason is a status, any other an error.
|
|
334
|
-
const reason = this.indexReason
|
|
335
|
-
if (
|
|
334
|
+
const reason = (0, box_status_1.noIndexReason)(this.indexReason, this.chartBox?.());
|
|
335
|
+
if ((0, box_status_1.isPending)(reason)) {
|
|
336
336
|
this.status.status(boxNote ? `${reason} · ${boxNote}` : reason, now);
|
|
337
337
|
}
|
|
338
338
|
else {
|
package/lib/shared-enc.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.AREA_ERROR_RETRY_MS = exports.ALL_COLLECTIONS = exports.CHARTS_PROVIDER_ID = void 0;
|
|
6
|
+
exports.AREA_ERROR_RETRY_MS = exports.PROVIDER_ABSENT = exports.ALL_COLLECTIONS = exports.CHARTS_PROVIDER_ID = void 0;
|
|
7
7
|
exports.sharedEncApi = sharedEncApi;
|
|
8
8
|
exports.sharedEncRoot = sharedEncRoot;
|
|
9
9
|
exports.sharedCellsDir = sharedCellsDir;
|
|
@@ -97,14 +97,20 @@ function collectionsHint(api = sharedEncApi()) {
|
|
|
97
97
|
.map((c) => `${c.id} (${c.kind === "chart-set" ? "chart set" : "converted chart"}, ${c.cellCount} cells)`)
|
|
98
98
|
.join(", ")}.`;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* What is said of a charts provider whose store is not published: at a server start it is
|
|
102
|
+
* published only once the provider's charts are loaded, after this plugin has started.
|
|
103
|
+
*/
|
|
104
|
+
exports.PROVIDER_ABSENT = "Charts Provider is not running or still starting";
|
|
100
105
|
/**
|
|
101
106
|
* Why the charts provider's `collection` holds no cells to read, as it tells it, or that it is
|
|
102
107
|
* not running; undefined when it is ready, or cannot tell. A provider started later restarts
|
|
103
|
-
* the plugin (`watchShared`)
|
|
108
|
+
* the plugin (`watchShared`) unless it shares the cells read at start; then only this note is
|
|
109
|
+
* read again (`watchAtStart` in index.ts).
|
|
104
110
|
*/
|
|
105
111
|
function sharedCellsNote(collection, api = sharedEncApi()) {
|
|
106
112
|
if (!api) {
|
|
107
|
-
return
|
|
113
|
+
return exports.PROVIDER_ABSENT;
|
|
108
114
|
}
|
|
109
115
|
if (collection === exports.ALL_COLLECTIONS) {
|
|
110
116
|
return collectionsHint(api)?.startsWith("Charts Provider shares no")
|
|
@@ -166,18 +172,28 @@ function requestSharedArea(bbox, api = sharedEncApi()) {
|
|
|
166
172
|
accepted: false,
|
|
167
173
|
started: false,
|
|
168
174
|
reason: err.message,
|
|
169
|
-
retryAfterMs: exports.AREA_ERROR_RETRY_MS
|
|
175
|
+
retryAfterMs: exports.AREA_ERROR_RETRY_MS,
|
|
176
|
+
failed: true
|
|
170
177
|
};
|
|
171
178
|
}
|
|
172
179
|
}
|
|
180
|
+
/** A comparison that throws counts as cells changed, so the plugin restarts rather than keep them. */
|
|
181
|
+
function safely(f) {
|
|
182
|
+
try {
|
|
183
|
+
return f();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
173
189
|
/**
|
|
174
190
|
* Calls `onChange` once the cells of `collection` (any, for ALL_COLLECTIONS) changed and
|
|
175
191
|
* settled. While the charts provider has not published its API, looks for it every `pollMs`,
|
|
176
192
|
* and after `waitMs` every `slowPollMs` for as long as the watch runs, since it may start any
|
|
177
193
|
* time; meanwhile the cells are read as found at start. An API that appears later counts as
|
|
178
|
-
* a change
|
|
179
|
-
*
|
|
180
|
-
*
|
|
194
|
+
* a change, unless `sameCells` finds its store holds the cells read at start: the provider
|
|
195
|
+
* may have written cells meanwhile, or keep its store elsewhere than the default path the
|
|
196
|
+
* cells were read from. It is asked once subscribed, so a write after it is heard.
|
|
181
197
|
*/
|
|
182
198
|
function watchSharedCells(collection, clock, onChange, opts = {}) {
|
|
183
199
|
const settleMs = opts.settleMs ?? 5_000;
|
|
@@ -185,6 +201,7 @@ function watchSharedCells(collection, clock, onChange, opts = {}) {
|
|
|
185
201
|
const pollMs = opts.pollMs ?? 5_000;
|
|
186
202
|
const slowPollMs = opts.slowPollMs ?? 60_000;
|
|
187
203
|
const started = clock.now();
|
|
204
|
+
const sameAsStart = (root) => safely(() => opts.sameCells?.(root) ?? false);
|
|
188
205
|
let unsubscribe;
|
|
189
206
|
let changedAt;
|
|
190
207
|
let timer;
|
|
@@ -212,7 +229,9 @@ function watchSharedCells(collection, clock, onChange, opts = {}) {
|
|
|
212
229
|
});
|
|
213
230
|
if (late) {
|
|
214
231
|
stopTimer();
|
|
215
|
-
|
|
232
|
+
if (!sameAsStart(api.root)) {
|
|
233
|
+
changed();
|
|
234
|
+
}
|
|
216
235
|
}
|
|
217
236
|
return true;
|
|
218
237
|
};
|
|
@@ -28,9 +28,14 @@ class EncReads {
|
|
|
28
28
|
if (files.length === 0) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const total = this.layers.size + new Set(files).size;
|
|
32
|
+
let done = this.layers.size;
|
|
33
|
+
(0, build_progress_1.reportProgress)({ phase: "cells", done, total });
|
|
34
|
+
const read = await this.reader(files, () => {
|
|
35
|
+
done = Math.min(total, done + 1);
|
|
36
|
+
(0, build_progress_1.reportProgress)({ phase: "cells", done, total });
|
|
37
|
+
});
|
|
38
|
+
for (const [file, layers] of read) {
|
|
34
39
|
this.layers.set(file, layers);
|
|
35
40
|
}
|
|
36
41
|
(0, build_progress_1.reportProgress)({ phase: "cells", done: this.layers.size, total: this.layers.size });
|
package/lib/status-report.js
CHANGED
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.statusReport = statusReport;
|
|
4
4
|
const public_text_1 = require("./foundation/public-text");
|
|
5
5
|
/** Body of `GET /status`: engine state, index provenance and look-ahead summary; `?summary` without the per-cell lists. */
|
|
6
|
-
function indexSection(load, summary) {
|
|
6
|
+
function indexSection(load, monitor, summary) {
|
|
7
7
|
const index = load?.index;
|
|
8
|
+
// Without an index the engine's reason is why, and follows a first box build as it runs.
|
|
9
|
+
const reason = !index && monitor?.last.state === "off" ? monitor.last.reason : load?.reason;
|
|
8
10
|
return {
|
|
9
11
|
loaded: index !== undefined,
|
|
10
12
|
built: load?.built ?? false,
|
|
11
|
-
reason: (0, public_text_1.publicReason)(
|
|
13
|
+
reason: (0, public_text_1.publicReason)(reason),
|
|
12
14
|
nodes: index?.pyramid.count ?? 0,
|
|
13
15
|
hazards: index?.hazards.size ?? 0,
|
|
14
16
|
...(summary ? {} : { cells: index?.catalogue.cells ?? [] })
|
|
@@ -38,7 +40,7 @@ function statusReport(s) {
|
|
|
38
40
|
const wfs = s.wfs?.status();
|
|
39
41
|
return {
|
|
40
42
|
...(s.monitor ? monitorSection(s.monitor, summary) : { engine: s.stopped, vessel: [] }),
|
|
41
|
-
index: indexSection(s.load, summary),
|
|
43
|
+
index: indexSection(s.load, s.monitor, summary),
|
|
42
44
|
livechart: s.livechart?.status() ?? null,
|
|
43
45
|
wfs: wfs && s.wfs?.downloads ? { ...wfs, lastError: (0, public_text_1.publicReason)(wfs.lastError) } : null,
|
|
44
46
|
chartBox: s.wfs ? boxSection(s.wfs) : null,
|
package/lib/wfs-service.js
CHANGED
|
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const fan_1 = require("./engine/fan");
|
|
10
10
|
const geo_1 = require("./foundation/geo");
|
|
11
11
|
const box_sizing_1 = require("./box-sizing");
|
|
12
|
+
const wfs_worker_1 = require("./wfs-worker");
|
|
12
13
|
const box_status_1 = require("./box-status");
|
|
13
14
|
const cell_reader_1 = require("./ingest/cell-reader");
|
|
14
15
|
const wfs_1 = require("./ingest/wfs");
|
|
@@ -24,6 +25,8 @@ Object.defineProperty(exports, "workerHeapLimitMb", { enumerable: true, get: fun
|
|
|
24
25
|
Object.defineProperty(exports, "workerRefresher", { enumerable: true, get: function () { return build_process_1.workerRefresher; } });
|
|
25
26
|
/** Position checks; a download is due only when the vessel has left the inner half of the box. */
|
|
26
27
|
const CHECK_EVERY_MS = 10_000;
|
|
28
|
+
/** How often a service started before the first position looks for it, ms. */
|
|
29
|
+
const FIRST_FIX_EVERY_MS = 1000;
|
|
27
30
|
/** No two downloads start closer together than this, and after a failure the wait is longer. */
|
|
28
31
|
const MIN_INTERVAL_MS = 60_000;
|
|
29
32
|
const RETRY_AFTER_ERROR_MS = 300_000;
|
|
@@ -62,6 +65,8 @@ class WfsService {
|
|
|
62
65
|
lastAttempt = -Infinity;
|
|
63
66
|
lastError;
|
|
64
67
|
timer;
|
|
68
|
+
/** Looks for the first position while none has come. */
|
|
69
|
+
firstFix;
|
|
65
70
|
controller;
|
|
66
71
|
rebuild;
|
|
67
72
|
/** Lowered for this run when a box ran its build out of memory. */
|
|
@@ -108,14 +113,41 @@ class WfsService {
|
|
|
108
113
|
start() {
|
|
109
114
|
const check = () => void this.check().catch((err) => this.deps.error(`${this.label}: ${err.message}`));
|
|
110
115
|
this.timer = this.deps.clock.setInterval(check, CHECK_EVERY_MS);
|
|
116
|
+
// A server starts this plugin before the position arrives: the first box is checked once
|
|
117
|
+
// one does, not up to CHECK_EVERY_MS later.
|
|
118
|
+
if (!this.hasPosition()) {
|
|
119
|
+
this.firstFix = this.deps.clock.setInterval(() => {
|
|
120
|
+
if (this.hasPosition()) {
|
|
121
|
+
this.stopFirstFix();
|
|
122
|
+
check();
|
|
123
|
+
}
|
|
124
|
+
}, FIRST_FIX_EVERY_MS);
|
|
125
|
+
}
|
|
111
126
|
check();
|
|
112
127
|
}
|
|
128
|
+
/** Whether a position is known; one that cannot be read is reported by the check. */
|
|
129
|
+
hasPosition() {
|
|
130
|
+
try {
|
|
131
|
+
return this.deps.position() !== undefined;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
stopFirstFix() {
|
|
138
|
+
if (this.firstFix !== undefined) {
|
|
139
|
+
this.deps.clock.clearInterval(this.firstFix);
|
|
140
|
+
this.firstFix = undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
113
143
|
stop() {
|
|
114
144
|
if (this.timer !== undefined) {
|
|
115
145
|
this.deps.clock.clearInterval(this.timer);
|
|
116
146
|
this.timer = undefined;
|
|
117
147
|
}
|
|
148
|
+
this.stopFirstFix();
|
|
118
149
|
this.controller?.abort();
|
|
150
|
+
this.deps.warm?.stop();
|
|
119
151
|
}
|
|
120
152
|
status() {
|
|
121
153
|
const rebuild = this.rebuild && (0, box_status_1.rebuildStatus)(this.rebuild, this.deps.clock.now());
|
|
@@ -138,9 +170,11 @@ class WfsService {
|
|
|
138
170
|
const bbox = this.handed ?? this.box?.bbox;
|
|
139
171
|
const lastMs = this.box?.durationMs;
|
|
140
172
|
const note = this.deps.area?.note();
|
|
173
|
+
const run = this.rebuild;
|
|
141
174
|
return {
|
|
142
175
|
...(bbox ? { bbox } : {}),
|
|
143
176
|
rebuilding: this.busy,
|
|
177
|
+
...(this.busy && run ? { phase: (0, box_status_1.phaseText)((0, box_status_1.rebuildStatus)(run, this.deps.clock.now())) } : {}),
|
|
144
178
|
failed: !this.busy && this.lastError !== undefined,
|
|
145
179
|
...(lastMs === undefined ? {} : { lastMs }),
|
|
146
180
|
summary: this.status().summary,
|
|
@@ -311,37 +345,11 @@ class WfsService {
|
|
|
311
345
|
const run = this.begin(now, controller);
|
|
312
346
|
const { radiusKm } = run;
|
|
313
347
|
try {
|
|
348
|
+
await this.deps.warm?.beforeBuild();
|
|
314
349
|
const centre = this.centreAhead(pos, radiusKm, run.estimateMs ?? this.refreshMs());
|
|
315
350
|
const bbox = (0, wfs_1.boxAround)(centre, radiusKm);
|
|
316
351
|
this.announce(bbox, now);
|
|
317
|
-
const
|
|
318
|
-
const { inBox, cellsDir } = this.deps;
|
|
319
|
-
// With WFS depths alone in use an empty cells directory loses nothing.
|
|
320
|
-
const cells = this.deps.index()?.catalogue.cells.some((c) => c.source !== wfs_1.WFS_SOURCE);
|
|
321
|
-
const { index, features } = await this.deps.refresh({
|
|
322
|
-
download: "url" in config
|
|
323
|
-
? {
|
|
324
|
-
url: config.url,
|
|
325
|
-
bbox,
|
|
326
|
-
dir: this.cellDir,
|
|
327
|
-
now: new Date(now).toISOString(),
|
|
328
|
-
catzoc: config.catzoc
|
|
329
|
-
}
|
|
330
|
-
: undefined,
|
|
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.
|
|
334
|
-
cellsRequired: !("url" in config) || cells === true,
|
|
335
|
-
box: inBox ? bbox : undefined,
|
|
336
|
-
build: {
|
|
337
|
-
dirs: "url" in config ? [this.cellDir] : [],
|
|
338
|
-
leafZoom: this.deps.leafZoom,
|
|
339
|
-
indexDir: this.deps.indexDir,
|
|
340
|
-
...(this.deps.cacheDir === undefined ? {} : { cacheDir: this.deps.cacheDir }),
|
|
341
|
-
source: this.deps.source,
|
|
342
|
-
...(inBox ? {} : { changed: [this.cellDir] })
|
|
343
|
-
}
|
|
344
|
-
}, controller.signal, (progress) => {
|
|
352
|
+
const { index, features } = await this.deps.refresh((0, wfs_worker_1.refreshJob)(this.deps, this.cellDir, bbox, now), controller.signal, (progress) => {
|
|
345
353
|
if (this.rebuild === run) {
|
|
346
354
|
run.progress = progress;
|
|
347
355
|
}
|
|
@@ -359,6 +367,8 @@ class WfsService {
|
|
|
359
367
|
this.box = box;
|
|
360
368
|
this.lastError = undefined;
|
|
361
369
|
await this.handOver(index, bbox);
|
|
370
|
+
// The next box is centred inside this one, at most MAX_LEAD of its radius ahead.
|
|
371
|
+
this.deps.warm?.boxInUse(bbox, radiusKm * (1 + MAX_LEAD));
|
|
362
372
|
}
|
|
363
373
|
catch (err) {
|
|
364
374
|
if (controller.signal.aborted) {
|
package/lib/wfs-wiring.js
CHANGED
|
@@ -18,6 +18,7 @@ const wfs_1 = require("./ingest/wfs");
|
|
|
18
18
|
const livechart_service_1 = require("./livechart-service");
|
|
19
19
|
const rebind_1 = require("./livechart/rebind");
|
|
20
20
|
const box_sizing_1 = require("./box-sizing");
|
|
21
|
+
const cell_warm_1 = require("./cell-warm");
|
|
21
22
|
const wfs_service_1 = require("./wfs-service");
|
|
22
23
|
const pause = () => new Promise((resolve) => setImmediate(resolve));
|
|
23
24
|
/** The catalogues list the same cells in the same order, the WFS cell aside. */
|
|
@@ -94,7 +95,9 @@ function areaRequests(config, app) {
|
|
|
94
95
|
/** How soon, and how late, a box is asked about again: Charts Provider starting, or busy. */
|
|
95
96
|
exports.AREA_ASK_MIN_MS = 60_000;
|
|
96
97
|
exports.AREA_ASK_MAX_MS = 30 * 60_000;
|
|
97
|
-
|
|
98
|
+
/** A charts provider not running yet is most likely starting: at a server start it publishes
|
|
99
|
+
* its store once its charts are loaded, after this plugin started. */
|
|
100
|
+
const ASK_STARTING_MS = exports.AREA_ASK_MIN_MS;
|
|
98
101
|
/**
|
|
99
102
|
* What the provider said of a box, as the status gives it when the vessel has no chart
|
|
100
103
|
* coverage: from its answer to the area request and the cells it has stored there (`cells`,
|
|
@@ -124,7 +127,7 @@ function areaNote(answer, cells, running = (0, shared_enc_1.sharedEncApi)() !==
|
|
|
124
127
|
function unaskedNote(running) {
|
|
125
128
|
return running
|
|
126
129
|
? "Charts Provider is too old to be asked for the ENC around the vessel: update it"
|
|
127
|
-
:
|
|
130
|
+
: shared_enc_1.PROVIDER_ABSENT;
|
|
128
131
|
}
|
|
129
132
|
/**
|
|
130
133
|
* Asks the charts provider for each box's cells, logging its answer when it changes, and says
|
|
@@ -133,11 +136,18 @@ function unaskedNote(running) {
|
|
|
133
136
|
function askForArea(app) {
|
|
134
137
|
let last;
|
|
135
138
|
let note;
|
|
139
|
+
/** Whether the last ask found no charts provider running. */
|
|
140
|
+
let askedAbsent = false;
|
|
136
141
|
return {
|
|
137
|
-
// A provider stopped since its answer leaves that answer stale: it is not running now.
|
|
138
|
-
|
|
142
|
+
// A provider stopped since its answer leaves that answer stale: it is not running now. One
|
|
143
|
+
// started since it was found absent has not been asked yet: it is asked again shortly.
|
|
144
|
+
note: () => {
|
|
145
|
+
const running = (0, shared_enc_1.sharedEncApi)() !== undefined;
|
|
146
|
+
return !running ? unaskedNote(false) : askedAbsent ? undefined : note;
|
|
147
|
+
},
|
|
139
148
|
ask: (bbox) => {
|
|
140
149
|
const api = (0, shared_enc_1.sharedEncApi)();
|
|
150
|
+
askedAbsent = api === undefined;
|
|
141
151
|
const answer = (0, shared_enc_1.requestSharedArea)(bbox, api);
|
|
142
152
|
note = areaNote(answer, answer === undefined ? undefined : (0, shared_enc_1.sharedCellsIn)(bbox, api), !!api);
|
|
143
153
|
const line = answer === undefined
|
|
@@ -145,11 +155,15 @@ function askForArea(app) {
|
|
|
145
155
|
: `Charts Provider, asked for the ENC around the vessel: ${answer.reason}`;
|
|
146
156
|
if (line !== last) {
|
|
147
157
|
last = line;
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
// A refusal follows the provider's own setting, and a provider not running yet is most
|
|
159
|
+
// likely starting; the status says both where the vessel lacks coverage. A provider too
|
|
160
|
+
// old to be asked, or one whose answer threw, needs the user.
|
|
161
|
+
const fault = answer === undefined ? api !== undefined : answer.failed === true;
|
|
162
|
+
if (fault) {
|
|
163
|
+
app.error(line);
|
|
150
164
|
}
|
|
151
165
|
else {
|
|
152
|
-
app.
|
|
166
|
+
app.debug(line);
|
|
153
167
|
}
|
|
154
168
|
}
|
|
155
169
|
const ms = answer === undefined ? ASK_STARTING_MS : answer.retryAfterMs;
|
|
@@ -159,15 +173,28 @@ function askForArea(app) {
|
|
|
159
173
|
}
|
|
160
174
|
};
|
|
161
175
|
}
|
|
176
|
+
/** Fills the cell cache around the box, saying in the debug log how many cells it read. */
|
|
177
|
+
function warmAround(w, job) {
|
|
178
|
+
return (0, cell_warm_1.startWarming)(job, (0, wfs_service_1.workerHeapLimitMb)(), (reply) => {
|
|
179
|
+
if (reply.error !== undefined) {
|
|
180
|
+
w.app.debug(`cell cache: filling it around the chart box failed: ${reply.error}`);
|
|
181
|
+
}
|
|
182
|
+
else if (reply.read) {
|
|
183
|
+
w.app.debug(`cell cache: read ${reply.read} cells around the chart box`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
162
187
|
function startWfs(w, config, dirName = "wfs") {
|
|
163
188
|
const dir = path_1.default.join(w.app.getDataDirPath(), dirName);
|
|
164
189
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
165
190
|
const area = areaRequests(w.config, w.app);
|
|
191
|
+
const cacheDir = (0, index_loader_1.cellCacheDir)(w.app.getDataDirPath());
|
|
192
|
+
const { cellsDir } = w.config;
|
|
166
193
|
return new wfs_service_1.WfsService({
|
|
167
194
|
config,
|
|
168
195
|
dir,
|
|
169
196
|
indexDir: w.config.indexDir ?? path_1.default.join(w.app.getDataDirPath(), "index"),
|
|
170
|
-
cacheDir
|
|
197
|
+
cacheDir,
|
|
171
198
|
leafZoom: w.config.leafZoom,
|
|
172
199
|
source: (0, index_source_1.indexSource)(w.config),
|
|
173
200
|
area,
|
|
@@ -183,6 +210,12 @@ function startWfs(w, config, dirName = "wfs") {
|
|
|
183
210
|
lookahead: w.config.lookahead,
|
|
184
211
|
maxRadiusKm: w.config.boxMaxRadiusKm || (0, box_sizing_1.autoCapKm)((0, wfs_service_1.workerHeapLimitMb)()),
|
|
185
212
|
onIndex: (index, changed) => useIndex(w, index, changed),
|
|
213
|
+
// In processes of their own, as the builds are; an injected builder, as in the tests, gets none.
|
|
214
|
+
...(w.build === wfs_service_1.workerBuilder && cellsDir !== undefined && w.config.boxRadiusKm > 0
|
|
215
|
+
? {
|
|
216
|
+
warm: (0, cell_warm_1.boxWarmer)((around, hold) => warmAround(w, { cellsDir, cacheDir, around, hold }), (m) => w.app.error(m))
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
186
219
|
error: (m) => {
|
|
187
220
|
w.app.error(m);
|
|
188
221
|
// With no index in use the failure is why the look-ahead is off: an error, not "waiting",
|
package/lib/wfs-worker.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.NO_FEATURES = void 0;
|
|
4
|
+
exports.refreshJob = refreshJob;
|
|
4
5
|
exports.refreshCells = refreshCells;
|
|
5
6
|
exports.refreshBuild = refreshBuild;
|
|
6
7
|
exports.runWfsJob = runWfsJob;
|
|
@@ -10,6 +11,35 @@ const wfs_1 = require("./ingest/wfs");
|
|
|
10
11
|
const build_box_1 = require("./spatial/build-box");
|
|
11
12
|
const build_progress_1 = require("./spatial/build-progress");
|
|
12
13
|
const build_worker_1 = require("./spatial/build-worker");
|
|
14
|
+
/** The refresh of the box `bbox`, started at `now`, with its WFS cell kept in `cellDir`. */
|
|
15
|
+
function refreshJob(deps, cellDir, bbox, now) {
|
|
16
|
+
const { config, inBox, cellsDir } = deps;
|
|
17
|
+
const wfs = "url" in config ? config : undefined;
|
|
18
|
+
// With WFS depths alone in use an empty cells directory loses nothing.
|
|
19
|
+
const cells = deps.index()?.catalogue.cells.some((c) => c.source !== wfs_1.WFS_SOURCE);
|
|
20
|
+
return {
|
|
21
|
+
download: wfs && {
|
|
22
|
+
url: wfs.url,
|
|
23
|
+
bbox,
|
|
24
|
+
dir: cellDir,
|
|
25
|
+
now: new Date(now).toISOString(),
|
|
26
|
+
catzoc: wfs.catzoc
|
|
27
|
+
},
|
|
28
|
+
cellsDir,
|
|
29
|
+
// WFS depths are built whatever ENC the charts provider has, but an index of charted
|
|
30
|
+
// hazards is kept while its cells are away, not replaced by depths alone.
|
|
31
|
+
cellsRequired: !wfs || cells === true,
|
|
32
|
+
box: inBox ? bbox : undefined,
|
|
33
|
+
build: {
|
|
34
|
+
dirs: wfs ? [cellDir] : [],
|
|
35
|
+
leafZoom: deps.leafZoom,
|
|
36
|
+
indexDir: deps.indexDir,
|
|
37
|
+
...(deps.cacheDir === undefined ? {} : { cacheDir: deps.cacheDir }),
|
|
38
|
+
source: deps.source,
|
|
39
|
+
...(inBox ? {} : { changed: [cellDir] })
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
13
43
|
exports.NO_FEATURES = { depthAreas: 0, dredgedAreas: 0, soundings: 0 };
|
|
14
44
|
/**
|
|
15
45
|
* The S-57 cells under `cellsDir`, found before the download. Scanning and stamping the 698
|
package/package.json
CHANGED