signalk-underkeel 0.2.0 → 0.3.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 +89 -0
- package/README.md +27 -5
- package/lib/api/routes.js +35 -7
- package/lib/api-wiring.js +4 -3
- package/lib/box-sizing.js +94 -0
- package/lib/box-status.js +61 -0
- package/lib/build-process.js +174 -0
- package/lib/config-schema.js +20 -10
- package/lib/config.js +2 -0
- package/lib/engine/chart-box.js +83 -0
- package/lib/engine/fan.js +32 -22
- package/lib/engine/lookahead.js +3 -2
- package/lib/engine/schedule.js +32 -29
- package/lib/engine/ukc-reach.js +2 -2
- package/lib/engine/ukc-tile-cache.js +216 -0
- package/lib/engine/ukc-tile-geometry.js +70 -115
- package/lib/engine/verdict.js +5 -2
- package/lib/foundation/area-cover.js +212 -0
- package/lib/foundation/child-process.js +82 -0
- package/lib/foundation/geometry.js +61 -250
- package/lib/foundation/rect-distance.js +227 -0
- package/lib/index-loader.js +21 -4
- package/lib/index.js +1 -0
- package/lib/ingest/cell-cache.js +352 -0
- package/lib/ingest/cell-source.js +23 -0
- package/lib/ingest/enc-read-process.js +69 -0
- package/lib/ingest/enc-wasm.js +38 -6
- package/lib/livechart/csb-thin.js +39 -0
- package/lib/livechart-service.js +14 -2
- package/lib/monitor.js +17 -11
- package/lib/query-service.js +26 -12
- package/lib/route-service.js +3 -1
- package/lib/setup-checks.js +61 -3
- package/lib/spatial/build-box.js +6 -3
- package/lib/spatial/build-items.js +70 -7
- package/lib/spatial/build-progress.js +41 -0
- package/lib/spatial/build-reuse.js +58 -7
- package/lib/spatial/build-survey.js +112 -0
- package/lib/spatial/build-worker.js +152 -135
- package/lib/spatial/chart-index.js +35 -18
- package/lib/spatial/hazard-columns.js +213 -0
- package/lib/spatial/hazard-file.js +104 -0
- package/lib/spatial/hazards.js +170 -58
- package/lib/spatial/index-parts.js +25 -0
- package/lib/spatial/parsed-cells.js +101 -0
- package/lib/spatial/polygon-index.js +49 -0
- package/lib/spatial/pyramid-build.js +16 -46
- package/lib/spatial/pyramid-regions.js +2 -2
- package/lib/spatial/pyramid.js +48 -50
- package/lib/spatial/region-jobs.js +46 -0
- package/lib/spatial/survey-shadow.js +44 -12
- package/lib/spatial/tile-reuse.js +345 -0
- package/lib/status-report.js +13 -3
- package/lib/tick-publisher.js +60 -17
- package/lib/wfs-service.js +115 -152
- package/lib/wfs-wiring.js +5 -1
- package/lib/wfs-worker.js +11 -5
- package/package.json +1 -1
- package/public/panel.js +9 -0
- package/public/runtime.js +2 -3
- package/public/ukc-shared.js +37 -1
- package/public/widget.js +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to signalk-underkeel are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-10-28
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Index builds run in a child process (Node `fork`, `MALLOC_ARENA_MAX=2`) instead of a worker
|
|
10
|
+
thread, and S-57 cells are read with GDAL in a short-lived reader process of their own, so
|
|
11
|
+
the memory a build used is returned when it ends. After the whole 20-cell Waddenzee build the
|
|
12
|
+
server held 1.07–1.42 GB; it now holds about 100 MB (x86). A build that crashes or passes its
|
|
13
|
+
heap limit fails with the reason and keeps the index in use. Region threads default to 1 on
|
|
14
|
+
machines with up to 4 GB of RAM.
|
|
15
|
+
- Named hazards are saved in `hazards.bin` (CRC-checked columns, depths exact in centimetres)
|
|
16
|
+
instead of `hazards.json`: 11 MB instead of 89 MB for the Waddenzee set. Loading the saved
|
|
17
|
+
index takes 0.31–0.35 s instead of 1.5–1.7 s, blocks the event loop for 5–7 ms instead of
|
|
18
|
+
1.1–1.3 s, and leaves 97–101 MB resident instead of 282–292 MB. A damaged file rebuilds the
|
|
19
|
+
index; an index saved with `hazards.json` is rebuilt once.
|
|
20
|
+
- Index builds test soundings against survey zones through a zone grid, and coverage through
|
|
21
|
+
the edge grids, with results identical to before: the 10 km Waddenzee box built in 31.6 s
|
|
22
|
+
instead of 57.2 s, the whole set in 147 s instead of 216 s (x86, one thread).
|
|
23
|
+
- The look-ahead's chart walk measures footprint-to-tile distances with exact pre-checks and
|
|
24
|
+
allocates less: ticks on the Waddenzee index take 13–18 ms instead of 22–28 ms (median, x86),
|
|
25
|
+
with every result byte-identical.
|
|
26
|
+
- UKC overlay tiles are kept encoded (4 MiB) beside their geometry (24 MiB, about 41 % of its
|
|
27
|
+
former size, source edges stored as float32 rounded outward), and gzipped off the event
|
|
28
|
+
loop. A 40-tile viewport redrawn unchanged takes about 0 ms instead of 1.4–2.1 s (x86).
|
|
29
|
+
- While the chart box is rebuilding, or the vessel is outside it, the no-coverage reason says
|
|
30
|
+
so ("outside the chart box, the box is rebuilding around the vessel (last build took 5
|
|
31
|
+
min)"); the state stays `warn`. `GET /status` `chartBox` reports the rebuild's phase,
|
|
32
|
+
progress and estimate, and the panel shows it. The box grows with speed and the measured
|
|
33
|
+
build time up to `boxMaxRadiusKm` (by default from the build heap: 10 km on 1 GB, about 19 km on a
|
|
34
|
+
4 GB Raspberry Pi 4) and says when no size keeps up. Route legs left unverified past the box say why.
|
|
35
|
+
- `navigation.underKeel.state` and `.envelopeRev` are sent when they change and at least every
|
|
36
|
+
5 s, no longer every second: at the dock a history logger stored two points a second. Over
|
|
37
|
+
240 stationary ticks the plugin sent 626 values in 242 deltas, now 242 in 74. A display should
|
|
38
|
+
read a `state` not received for 15 s as unknown; the status widget does, and only a fresh
|
|
39
|
+
`state` keeps it current, and the chart runtime withdraws the envelope 15 s after the last
|
|
40
|
+
`envelopeRev` (it was 10 s). Notifications are unchanged.
|
|
41
|
+
- The plugin status no longer carries the seconds to shoal (`GET /status` still has them), and a
|
|
42
|
+
status that differs only in its numbers is set at most every 30 s: through the warn zone of a
|
|
43
|
+
240 s approach the status was set 67 times, now 5.
|
|
44
|
+
- Below 1 kn, or without a fresh SOG, the living chart's CSB export log (`soundings.ndjson`)
|
|
45
|
+
keeps one sounding per cell per minute. A 10 Hz sounder on a moored boat rewrote the
|
|
46
|
+
40 000-line file every 33 minutes. Every accepted sounding still updates its cell, so
|
|
47
|
+
discrepancies are found as before. `GET /status` counts the left-out soundings as
|
|
48
|
+
`livechart.csbThinned`.
|
|
49
|
+
|
|
50
|
+
- An index build keeps each S-57 cell's read layers and extent in `cell-cache` in the plugin
|
|
51
|
+
data directory (at most 512 MB) and reads with GDAL only the cells it holds no valid entry
|
|
52
|
+
for: changed cells, and cells whose entry is missing or was evicted. The
|
|
53
|
+
10 km Waddenzee chart box rebuilt in 12–15 s instead of 30–34 s, with no 300–370 MB reader
|
|
54
|
+
process; the whole 20-cell set in 78–90 s instead of 141 s on one thread (x86). A torn or
|
|
55
|
+
damaged entry is read from the cell again. The built index is byte for byte the same.
|
|
56
|
+
- A chart box refresh copies from the saved box index each tile down to z15 that lies inside
|
|
57
|
+
both boxes, or in both boxes' build-area band outside them, and is evaluated from the same
|
|
58
|
+
cell inputs in both builds; `tiles.json` beside the index holds
|
|
59
|
+
the digests. On the 10 km Waddenzee box (x86, warm cell cache) a 5 km move copied 66 % of
|
|
60
|
+
the nodes and built in 6.9–7.2 s instead of 11.6–11.7 s, an 8 km move 54 % in 6.4–6.9 s
|
|
61
|
+
instead of 10.4–11.6 s. The index is byte for byte the one a fresh build of the new box
|
|
62
|
+
makes; a changed cell's tiles, another leaf zoom or plugin version, or a missing or foreign
|
|
63
|
+
`tiles.json` are built afresh. `cells.json` is written in the field order it is read back.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- A hardware line in the setup check: CPUs, RAM and free RAM, with advice on 1 GB machines
|
|
68
|
+
(keep the index box, consider leaf zoom 19), on little free RAM, and on large ones (box 0
|
|
69
|
+
avoids rebuilds for a small chart set). It changes no setting.
|
|
70
|
+
- Descriptions on **Index leaf zoom**, **Rebuild index at start**, **Look-ahead distance** and
|
|
71
|
+
**Minimum horizon** saying what each costs in CPU or memory.
|
|
72
|
+
- README: what to check before installing on Victron Venus OS, whose Node.js version the
|
|
73
|
+
repository does not record.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- A larger least position buffer could lower the look-ahead's P: the buffer also set how far
|
|
78
|
+
apart the footprints were scored, so changing it moved the samples even where the footprints
|
|
79
|
+
kept the same buffer, and one set of samples could read a shoal up to 1e-4 higher (safety
|
|
80
|
+
invariant 2). Each footprint now sweeps the hull since the step before, at hull positions that
|
|
81
|
+
touch with no buffer, so no footprint time or shape depends on the buffer or the position's
|
|
82
|
+
uncertainty. The step stays 3 s at any speed; a fast boat scores fewer, longer footprints.
|
|
83
|
+
Beyond the warn window a footprint sweeps at most three hull lengths.
|
|
84
|
+
|
|
85
|
+
## [0.2.1] - 2026-10-27
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- The tests pass in the Signal K plugin registry, which runs them with a read-only home
|
|
90
|
+
directory. There npm could not write its cache, so the published-package test's
|
|
91
|
+
`npm pack --dry-run` failed and the plugin lost its "tested" mark. The test now gives npm a
|
|
92
|
+
cache under the temp directory. The plugin itself is unchanged.
|
|
93
|
+
|
|
5
94
|
## [0.2.0] - 2026-10-27
|
|
6
95
|
|
|
7
96
|
### Added
|
package/README.md
CHANGED
|
@@ -89,6 +89,16 @@ Restart the server, then enable **Under-Keel Awareness** under _Server → Plugi
|
|
|
89
89
|
Signal K admin UI. Its one runtime dependency is gdal3.js (about 40 MB), which reads the S-57
|
|
90
90
|
cells.
|
|
91
91
|
|
|
92
|
+
**Victron Venus OS (Cerbo GX).** `package.json` asks for Node.js 22 or newer (`engines`), and CI
|
|
93
|
+
tests Node 22 and 24 only. Venus OS runs Signal K on the Node.js it ships, and this repository
|
|
94
|
+
does not record which version that is: check with `node --version` over SSH on the device before
|
|
95
|
+
installing. On an older Node, npm prints an `EBADENGINE` warning and installs anyway unless
|
|
96
|
+
`engine-strict` is set, and the plugin runs untested.
|
|
97
|
+
|
|
98
|
+
On a 1 GB machine such as a Raspberry Pi 3 or a Cerbo GX, keep the index box (the default
|
|
99
|
+
10 km) rather than indexing every cell, and consider **Index leaf zoom** 19. The setup check
|
|
100
|
+
shows the RAM and CPUs it finds and says the same; it never changes a setting.
|
|
101
|
+
|
|
92
102
|
## Prepare the charts
|
|
93
103
|
|
|
94
104
|
The plugin reads unencrypted S-57 ENC cells as they are delivered: each `.000` base cell with
|
|
@@ -114,18 +124,27 @@ The index covers a box around the vessel, **Index box around the vessel (km)** (
|
|
|
114
124
|
each way), and is rebuilt off the event loop as the vessel moves, so a large exchange set such
|
|
115
125
|
as NOAA's 698 East and Gulf coast cells is never indexed whole: only the cells reaching the box
|
|
116
126
|
are read (21 of them around Miami, 22 s at leaf zoom 20 on x86). Outside the box the plugin
|
|
117
|
-
reports no coverage, never clear water
|
|
127
|
+
reports no coverage, never clear water, and the reason says the vessel is outside the chart box
|
|
128
|
+
and whether it is being rebuilt; the plugin status and the details panel show the rebuild's
|
|
129
|
+
phase, start and the last build's duration. At speed the box grows past its radius, up to
|
|
130
|
+
**Largest index box at speed** (0 sizes it from the RAM), so the vessel cannot reach the edge
|
|
131
|
+
before the next box is built (docs/HOW-IT-WORKS.md §12). A radius under 2 km is raised to 2.
|
|
132
|
+
Until the first position arrives the engine is `off`
|
|
118
133
|
with the reason `waiting for the vessel position to build the chart box`. Set the box to 0 to
|
|
119
134
|
index every cell.
|
|
120
135
|
|
|
121
|
-
At the first start the plugin builds a chart index from the cells in a
|
|
136
|
+
At the first start the plugin builds a chart index from the cells in a child process and saves
|
|
122
137
|
it under the plugin data directory, `~/.signalk/plugin-config-data/signalk-underkeel/index`
|
|
123
138
|
unless **Chart index directory** says otherwise. Until the index is ready the engine is `off`
|
|
124
139
|
with the reason `building chart index`. The 12 NOAA cells around Miami (6.8 MB of `.000` and
|
|
125
140
|
updates) build in 19 s at leaf zoom 18 on x86. Later starts load the saved index, 0.1 s for
|
|
126
141
|
those cells, and rebuild it when a cell was added or removed, or a cell's `.000` or update files
|
|
127
142
|
changed; `GET /status` names the cells that caused the rebuild. **Rebuild index at start**
|
|
128
|
-
forces a rebuild on every start. A cell folder may be a symbolic link.
|
|
143
|
+
forces a rebuild on every start. A cell folder may be a symbolic link. What the build read
|
|
144
|
+
from each cell is kept, compressed, in `cell-cache` in the plugin data directory (at most 512 MB), so a
|
|
145
|
+
chart box refresh reads again only the cells whose files changed: the 10 km Waddenzee box
|
|
146
|
+
rebuilds in 12–15 s instead of 30–34 s on x86. Deleting the folder is safe; the cells are then
|
|
147
|
+
read again.
|
|
129
148
|
|
|
130
149
|
Folders of GeoJSON layers exported by `scripts/enc-to-geojson.sh` for earlier versions are no
|
|
131
150
|
longer read: point **ENC cells directory** at the original `.000` cells instead. Such a folder
|
|
@@ -140,7 +159,8 @@ There are two places to set the plugin up, and both write the same configuration
|
|
|
140
159
|
level and warnings, in the same panel with a link back to the details. Drag the keel, the margin
|
|
141
160
|
line and the sounder on a side view of the hull, or type the numbers. The page shows the draft
|
|
142
161
|
Signal K already states, the vertical datum of your charts with the water-level source that fits
|
|
143
|
-
it, and a setup check
|
|
162
|
+
it, and a setup check, which also lists the machine's RAM and CPUs with advice on the index
|
|
163
|
+
settings. **Save and restart** needs a Signal K admin login.
|
|
144
164
|
- The plugin's page in the admin UI. It asks for the same essentials first; **Show expert
|
|
145
165
|
settings** opens the rest, and an optional feature's fields appear once it is enabled. Admin
|
|
146
166
|
UIs older than Signal K 2.32 show every field at once.
|
|
@@ -257,7 +277,9 @@ screenshots, including the plugin layers in the chart list and the setup steps i
|
|
|
257
277
|
| `notifications.grounding` | only from the optional grounding detector, when the boat has gone aground |
|
|
258
278
|
|
|
259
279
|
Any Signal K app can show these; the status line on the plugin's admin page reads, for
|
|
260
|
-
example, `Monitoring: unknown — position stale`.
|
|
280
|
+
example, `Monitoring: unknown — position stale`. Each value is sent when it changes; `state` and
|
|
281
|
+
`envelopeRev` at least every 5 s, the others every 10 s. A display that shows them should read a
|
|
282
|
+
`state` older than 15 s as unknown, as the plugin's own widget does.
|
|
261
283
|
|
|
262
284
|
## Optional features
|
|
263
285
|
|
package/lib/api/routes.js
CHANGED
|
@@ -144,13 +144,7 @@ function registerRoutes(router, api, log = console.error) {
|
|
|
144
144
|
res.setHeader("Cache-Control", "no-cache");
|
|
145
145
|
if (reply.body instanceof Uint8Array) {
|
|
146
146
|
res.setHeader("Vary", "Accept-Encoding");
|
|
147
|
-
|
|
148
|
-
const gzip = reply.body.length >= GZIP_MIN_BYTES && acceptsGzip(req.headers?.["accept-encoding"]);
|
|
149
|
-
if (gzip) {
|
|
150
|
-
res.setHeader("Content-Encoding", "gzip");
|
|
151
|
-
}
|
|
152
|
-
const body = gzip ? (0, zlib_1.gzipSync)(reply.body) : Buffer.from(reply.body);
|
|
153
|
-
res.status(reply.status).type(reply.contentType).send(body);
|
|
147
|
+
sendTile(res, reply, reply.body, acceptsGzip(req.headers?.["accept-encoding"]));
|
|
154
148
|
}
|
|
155
149
|
else {
|
|
156
150
|
res.status(reply.status).json(publicBody(reply.status, reply.body));
|
|
@@ -191,6 +185,40 @@ function registerSettings(router, api, log) {
|
|
|
191
185
|
.catch((err) => fail(res, err, log));
|
|
192
186
|
});
|
|
193
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Sends a tile body, gzipped when the client takes it and the body is worth it.
|
|
190
|
+
* Signal K's compression() skips the tile content type and leaves an encoded body alone.
|
|
191
|
+
*/
|
|
192
|
+
function sendTile(res, reply, raw, gzip) {
|
|
193
|
+
const sendBody = (body, encoded) => {
|
|
194
|
+
if (encoded) {
|
|
195
|
+
res.setHeader("Content-Encoding", "gzip");
|
|
196
|
+
}
|
|
197
|
+
res
|
|
198
|
+
.status(reply.status)
|
|
199
|
+
.type(reply.contentType)
|
|
200
|
+
.send(Buffer.from(body.buffer, body.byteOffset, body.byteLength));
|
|
201
|
+
};
|
|
202
|
+
if (raw.length < GZIP_MIN_BYTES || !gzip) {
|
|
203
|
+
sendBody(raw, false);
|
|
204
|
+
}
|
|
205
|
+
else if (reply.memo?.gzip) {
|
|
206
|
+
sendBody(reply.memo.gzip, true);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// Off the event loop: a tile of some kilobytes takes a millisecond or more on a Pi.
|
|
210
|
+
(0, zlib_1.gzip)(raw, (err, packed) => {
|
|
211
|
+
if (err) {
|
|
212
|
+
sendBody(raw, false);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (reply.memo) {
|
|
216
|
+
reply.memo.gzip = packed;
|
|
217
|
+
}
|
|
218
|
+
sendBody(packed, true);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
194
222
|
/** Smaller bodies gain less from gzip than its header costs; compression() uses the same. */
|
|
195
223
|
const GZIP_MIN_BYTES = 1024;
|
|
196
224
|
/** Whether an Accept-Encoding header accepts gzip, by name or `*`, with a q above 0. */
|
package/lib/api-wiring.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.apiFor = apiFor;
|
|
|
4
4
|
const ack_service_1 = require("./ack-service");
|
|
5
5
|
const manifest_1 = require("./api/manifest");
|
|
6
6
|
const ukc_tile_changes_1 = require("./api/ukc-tile-changes");
|
|
7
|
-
const
|
|
7
|
+
const ukc_tile_cache_1 = require("./engine/ukc-tile-cache");
|
|
8
8
|
const config_1 = require("./config");
|
|
9
9
|
const water_level_1 = require("./model/water-level");
|
|
10
10
|
const query_service_1 = require("./query-service");
|
|
@@ -19,7 +19,7 @@ function livechartReply(s, body) {
|
|
|
19
19
|
}
|
|
20
20
|
function apiFor(app, clock, s) {
|
|
21
21
|
const tileChanges = new ukc_tile_changes_1.UkcTileChanges();
|
|
22
|
-
const tileCache = new
|
|
22
|
+
const tileCache = new ukc_tile_cache_1.UkcTileCache();
|
|
23
23
|
const queryDeps = () => ({
|
|
24
24
|
index: s.load?.index,
|
|
25
25
|
indexReason: s.load?.reason ?? "plugin not started",
|
|
@@ -39,7 +39,8 @@ function apiFor(app, clock, s) {
|
|
|
39
39
|
waveHeight: () => s.monitor?.nav.waveHeight?.value,
|
|
40
40
|
getRoute: (id) => app.resourcesApi.getResource("routes", id),
|
|
41
41
|
now: () => clock.now(),
|
|
42
|
-
queue: s.requests
|
|
42
|
+
queue: s.requests,
|
|
43
|
+
chartBox: () => s.wfs?.view().bbox
|
|
43
44
|
});
|
|
44
45
|
const withAcks = (fn) => {
|
|
45
46
|
const m = s.monitor;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_BOX_KM = exports.SHRINK_BELOW = exports.SPEED_HEADROOM = exports.STEP_KM = void 0;
|
|
4
|
+
exports.autoCapKm = autoCapKm;
|
|
5
|
+
exports.buildMsAt = buildMsAt;
|
|
6
|
+
exports.fits = fits;
|
|
7
|
+
exports.nextSize = nextSize;
|
|
8
|
+
const build_items_1 = require("./spatial/build-items");
|
|
9
|
+
/**
|
|
10
|
+
* The radius of the chart box around the vessel (plan §4.2). A box must let the vessel run,
|
|
11
|
+
* from its centre, the distance it covers while the next box is built (with the refresh
|
|
12
|
+
* margin) plus the look-ahead's reach and the no-coverage band inside the edge; otherwise
|
|
13
|
+
* rebuilds run back to back and a slow machine at speed lets the vessel reach the edge. The
|
|
14
|
+
* box grows for that from the configured radius up to a cap, and shrinks back only once the
|
|
15
|
+
* speed needs much less, so the radius does not change with every gust.
|
|
16
|
+
*/
|
|
17
|
+
/** Radii are chosen in steps of this many km. */
|
|
18
|
+
exports.STEP_KM = 0.5;
|
|
19
|
+
/** A new box is sized for this much more than the current speed. */
|
|
20
|
+
exports.SPEED_HEADROOM = 1.25;
|
|
21
|
+
/** A box is kept at its size while the size the speed needs is above this share of it. */
|
|
22
|
+
exports.SHRINK_BELOW = 0.7;
|
|
23
|
+
/** The automatic cap: this radius on a 256 MB build heap, growing with the heap's root. */
|
|
24
|
+
const CAP_AT_256_MB_KM = 10;
|
|
25
|
+
/** The largest box the configuration allows, km. */
|
|
26
|
+
exports.MAX_BOX_KM = 50;
|
|
27
|
+
/**
|
|
28
|
+
* The automatic cap for a build heap of `heapMb`: 10 km at 256 MB (a 1 GB Pi 3 or Cerbo GX),
|
|
29
|
+
* 20 km at 1 GB, 28 km at 2 GB, rounded down to STEP_KM. The heap is a quarter of the RAM the
|
|
30
|
+
* system reports, which on a 4 GB Pi 4 is about 3.8 GiB: 19 km. A build's memory grows with the box's area,
|
|
31
|
+
* and the 10 km box with the ENC pair at leaf zoom 20 builds in under 96 MB.
|
|
32
|
+
*/
|
|
33
|
+
function autoCapKm(heapMb) {
|
|
34
|
+
const km = CAP_AT_256_MB_KM * Math.sqrt(heapMb / 256);
|
|
35
|
+
return Math.min(exports.MAX_BOX_KM, Math.floor(km / exports.STEP_KM) * exports.STEP_KM);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The build time of a box of `km`: the measured one, grown with the build area past the
|
|
39
|
+
* radius it was measured at. Tiles are built to BOX_REACH past the box, so the area grows
|
|
40
|
+
* with (radius + reach)². Never shorter than measured, so a smaller box is not assumed to
|
|
41
|
+
* build faster until it has.
|
|
42
|
+
*/
|
|
43
|
+
function buildMsAt(km, i) {
|
|
44
|
+
const reach = build_items_1.BOX_REACH / 1000;
|
|
45
|
+
return i.buildMs * Math.max(1, ((km + reach) / (i.builtKm + reach)) ** 2);
|
|
46
|
+
}
|
|
47
|
+
/** Whether a box of `km` holds a refresh's run at `sog` plus the reach between its centre and edge. */
|
|
48
|
+
function fits(km, i, sog = i.sog) {
|
|
49
|
+
return km * 1000 >= (i.margin * sog * buildMsAt(km, i)) / 1000 + i.reachM;
|
|
50
|
+
}
|
|
51
|
+
/** The smallest radius on the STEP_KM grid in [from, to] that fits at `sog`. */
|
|
52
|
+
function smallestFit(from, to, i, sog) {
|
|
53
|
+
for (let km = from; km <= to + 1e-9; km = nextStep(km)) {
|
|
54
|
+
if (fits(km, i, sog)) {
|
|
55
|
+
return km;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const nextStep = (km) => (Math.floor(km / exports.STEP_KM + 1e-9) + 1) * exports.STEP_KM;
|
|
61
|
+
/** The radius at which the box's spare reach is greatest, when none fits. */
|
|
62
|
+
function roomiest(i) {
|
|
63
|
+
let best = i.minKm;
|
|
64
|
+
const spare = (km) => km * 1000 - (i.margin * i.sog * buildMsAt(km, i)) / 1000;
|
|
65
|
+
for (let km = i.minKm; km <= i.capKm + 1e-9; km = nextStep(km)) {
|
|
66
|
+
if (spare(km) > spare(best)) {
|
|
67
|
+
best = km;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return best;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The radius for the next box, given the radius of the box in use. Stopped or without a
|
|
74
|
+
* speed the box in use keeps its size, within the configured radius and the cap.
|
|
75
|
+
*/
|
|
76
|
+
function nextSize(i, currentKm) {
|
|
77
|
+
const minKm = Math.min(i.minKm, i.capKm);
|
|
78
|
+
const within = currentKm !== undefined && currentKm >= minKm && currentKm <= i.capKm;
|
|
79
|
+
if (!(i.sog > 0)) {
|
|
80
|
+
return { radiusKm: within ? currentKm : minKm, enough: true };
|
|
81
|
+
}
|
|
82
|
+
const want = smallestFit(minKm, i.capKm, i, i.sog * exports.SPEED_HEADROOM);
|
|
83
|
+
if (want === undefined) {
|
|
84
|
+
const radiusKm = smallestFit(minKm, i.capKm, i, i.sog) ?? roomiest(i);
|
|
85
|
+
const enough = fits(radiusKm, i);
|
|
86
|
+
const neededKm = enough ? undefined : smallestFit(i.capKm, exports.MAX_BOX_KM, i, i.sog);
|
|
87
|
+
return { radiusKm, enough, ...(neededKm === undefined ? {} : { neededKm }) };
|
|
88
|
+
}
|
|
89
|
+
// Kept while it holds the speed and the speed does not need much less.
|
|
90
|
+
if (within && fits(currentKm, i) && want <= currentKm && want > exports.SHRINK_BELOW * currentKm) {
|
|
91
|
+
return { radiusKm: currentKm, enough: true };
|
|
92
|
+
}
|
|
93
|
+
return { radiusKm: want, enough: true };
|
|
94
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rebuildStatus = rebuildStatus;
|
|
4
|
+
exports.boxSummary = boxSummary;
|
|
5
|
+
const box_sizing_1 = require("./box-sizing");
|
|
6
|
+
const chart_box_1 = require("./engine/chart-box");
|
|
7
|
+
function rebuildStatus(r, now) {
|
|
8
|
+
return {
|
|
9
|
+
startedAt: new Date(r.startedAt).toISOString(),
|
|
10
|
+
elapsedMs: now - r.startedAt,
|
|
11
|
+
radiusKm: r.radiusKm,
|
|
12
|
+
...r.progress,
|
|
13
|
+
...(r.estimateMs === undefined ? {} : { estimateMs: r.estimateMs })
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function phaseText(r) {
|
|
17
|
+
const of = r.done !== undefined && r.total !== undefined ? ` ${r.done}/${r.total}` : "";
|
|
18
|
+
switch (r.phase) {
|
|
19
|
+
case "download":
|
|
20
|
+
return "downloading WFS depths";
|
|
21
|
+
case "cells":
|
|
22
|
+
return `reading cells${of}`;
|
|
23
|
+
case "roots":
|
|
24
|
+
return `building roots${of}`;
|
|
25
|
+
case "save":
|
|
26
|
+
return "saving the index";
|
|
27
|
+
default:
|
|
28
|
+
return "starting";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function rebuildText(r, lastMs) {
|
|
32
|
+
const expected = r.estimateMs === undefined
|
|
33
|
+
? "first build"
|
|
34
|
+
: r.estimateMs === lastMs
|
|
35
|
+
? `last build took ${(0, chart_box_1.spoken)(r.estimateMs)}`
|
|
36
|
+
: `about ${(0, chart_box_1.spoken)(r.estimateMs)} expected`;
|
|
37
|
+
const ago = (0, chart_box_1.spoken)(r.elapsedMs);
|
|
38
|
+
return `chart box rebuilding (${r.radiusKm} km): ${phaseText(r)}, started ${ago} ago, ${expected}`;
|
|
39
|
+
}
|
|
40
|
+
function tooSmallText(s, i) {
|
|
41
|
+
const kn = ((s.sog * 3600) / 1852).toFixed(1);
|
|
42
|
+
const builds = (0, chart_box_1.spoken)((0, box_sizing_1.buildMsAt)(s.radiusKm, i));
|
|
43
|
+
const need = s.neededKm === undefined
|
|
44
|
+
? "no box size keeps up"
|
|
45
|
+
: `needs ${s.neededKm} km, limit ${s.capKm} km`;
|
|
46
|
+
return `chart box ${s.radiusKm} km is too small for ${kn} kn with ${builds} builds: ${need}`;
|
|
47
|
+
}
|
|
48
|
+
/** One line on the rebuild in progress and the box size; undefined when neither needs saying. */
|
|
49
|
+
function boxSummary(s) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
if (s.rebuild) {
|
|
52
|
+
parts.push(rebuildText(s.rebuild, s.lastMs));
|
|
53
|
+
}
|
|
54
|
+
if (!s.sizing.enough) {
|
|
55
|
+
parts.push(tooSmallText(s.sizing, s.input));
|
|
56
|
+
}
|
|
57
|
+
if (s.ceilingKm !== undefined) {
|
|
58
|
+
parts.push(`chart box limited to ${s.ceilingKm} km: a larger one ran out of memory`);
|
|
59
|
+
}
|
|
60
|
+
return parts.length > 0 ? parts.join("; ") : undefined;
|
|
61
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
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.workerRefresher = exports.workerBuilder = void 0;
|
|
7
|
+
exports.workerHeapLimitMb = workerHeapLimitMb;
|
|
8
|
+
exports.runBuildProcess = runBuildProcess;
|
|
9
|
+
exports.inThreadRefresher = inThreadRefresher;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const child_process_1 = require("./foundation/child-process");
|
|
13
|
+
const memory_1 = require("./foundation/memory");
|
|
14
|
+
const wfs_1 = require("./ingest/wfs");
|
|
15
|
+
const wfs_worker_1 = require("./wfs-worker");
|
|
16
|
+
const build_progress_1 = require("./spatial/build-progress");
|
|
17
|
+
const build_worker_1 = require("./spatial/build-worker");
|
|
18
|
+
const block_reader_1 = require("./spatial/block-reader");
|
|
19
|
+
const chart_index_1 = require("./spatial/chart-index");
|
|
20
|
+
/** Settles once every build process aborted so far has exited. */
|
|
21
|
+
let exits = Promise.resolve();
|
|
22
|
+
/**
|
|
23
|
+
* Old-generation heap for a build process: a quarter of the RAM, 256 MB to 2 GB. The ENC pair
|
|
24
|
+
* plus a 10 km WFS box at leaf zoom 20 builds in under 96 MB; without a limit an oversized
|
|
25
|
+
* build grows until the kernel ends the whole Signal K server on a 1 GB Pi 3 or Cerbo GX.
|
|
26
|
+
*/
|
|
27
|
+
function workerHeapLimitMb(totalBytes = (0, memory_1.usableMemory)()) {
|
|
28
|
+
return Math.min(2048, Math.max(256, Math.floor(totalBytes / 4 / 2 ** 20)));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Runs the compiled `script` in a child process (`forkLimited`) with `data` and resolves with
|
|
32
|
+
* its staged reply. The child only stages; the caller moves the result into place on the main
|
|
33
|
+
* thread, and never for an aborted run, so a build still exiting cannot replace an index
|
|
34
|
+
* saved after it was aborted. An abort kills the child and its S-57 reader (`killGroup`); its
|
|
35
|
+
* run rejects once the child has exited and its staging is removed, and the next run starts
|
|
36
|
+
* only then. stop() itself returns at once. A child that ends without a reply fails the run.
|
|
37
|
+
* Progress messages the child sends before its reply go to `progress`.
|
|
38
|
+
*/
|
|
39
|
+
async function runBuildProcess(script, data, signal, indexDir, heapMb = workerHeapLimitMb(), progress) {
|
|
40
|
+
await exits;
|
|
41
|
+
if (signal.aborted) {
|
|
42
|
+
throw new Error("aborted");
|
|
43
|
+
}
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const { child, stderr, outOfMemory } = (0, child_process_1.forkLimited)(script, heapMb, true);
|
|
46
|
+
const { pid } = child;
|
|
47
|
+
let replied = false;
|
|
48
|
+
// Staging the child left half-written: once it has exited nothing else writes there.
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
if (indexDir !== undefined && pid !== undefined) {
|
|
51
|
+
(0, chart_index_1.removeStaging)(indexDir, (p) => p === pid);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const closed = new Promise((r) => child.once("close", () => r()));
|
|
55
|
+
const abort = () => {
|
|
56
|
+
(0, child_process_1.killGroup)(child);
|
|
57
|
+
const exit = closed.then(cleanup).catch(() => undefined);
|
|
58
|
+
exits = Promise.all([exits, exit]);
|
|
59
|
+
void exit.then(() => reject(new Error("aborted")));
|
|
60
|
+
};
|
|
61
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
62
|
+
child.on("message", (m) => {
|
|
63
|
+
if ((0, build_progress_1.isProgress)(m)) {
|
|
64
|
+
if (!replied && !signal.aborted) {
|
|
65
|
+
progress?.(m.progress);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
replied = true;
|
|
70
|
+
// It exits once the reply is sent; killed, it cannot linger holding its memory.
|
|
71
|
+
child.kill("SIGKILL");
|
|
72
|
+
if (signal.aborted) {
|
|
73
|
+
if (m.staging) {
|
|
74
|
+
fs_1.default.rmSync(m.staging, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
signal.removeEventListener("abort", abort);
|
|
79
|
+
if (m.ok && m.staging) {
|
|
80
|
+
resolve(m);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
reject(new Error(m.error ?? "index build failed"));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
// A child that could not start, or ended without a reply: the index in use stays.
|
|
87
|
+
child.once("error", (err) => {
|
|
88
|
+
if (!signal.aborted && !replied) {
|
|
89
|
+
signal.removeEventListener("abort", abort);
|
|
90
|
+
(0, child_process_1.killGroup)(child);
|
|
91
|
+
reject(err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
child.once("close", (code, end) => {
|
|
95
|
+
if (signal.aborted || replied) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
signal.removeEventListener("abort", abort);
|
|
99
|
+
try {
|
|
100
|
+
cleanup();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Left for the next start's recovery, which removes staging of exited processes.
|
|
104
|
+
}
|
|
105
|
+
const how = code === null ? `signal ${end ?? "?"}` : `code ${code}`;
|
|
106
|
+
const tail = stderr() === "" ? "" : `: ${stderr()}`;
|
|
107
|
+
reject(new Error(outOfMemory()
|
|
108
|
+
? `index build stopped at the ${heapMb} MB heap limit${tail}`
|
|
109
|
+
: `index build process exited with ${how} without a result${tail}`));
|
|
110
|
+
});
|
|
111
|
+
// A child that cannot take the job exits, and "close" reports it.
|
|
112
|
+
child.send(data, () => undefined);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Moves a staged index into `indexDir` and adopts the one the build posted, unless the run
|
|
117
|
+
* was aborted meanwhile. Reading the saved index back blocked the event loop for 52–61 ms
|
|
118
|
+
* with the Nauvo pair and a 10 km WFS box at leaf zoom 20, 258–292 ms with 32 cells (x86).
|
|
119
|
+
*/
|
|
120
|
+
async function commit(indexDir, m, signal) {
|
|
121
|
+
await (0, block_reader_1.readsSettled)(indexDir);
|
|
122
|
+
if (signal.aborted) {
|
|
123
|
+
fs_1.default.rmSync(m.staging, { recursive: true, force: true });
|
|
124
|
+
throw new Error("aborted");
|
|
125
|
+
}
|
|
126
|
+
(0, chart_index_1.commitChartIndex)(indexDir, m.staging);
|
|
127
|
+
if (m.index) {
|
|
128
|
+
return (0, build_worker_1.adoptIndex)(m.index);
|
|
129
|
+
}
|
|
130
|
+
// A pyramid above HELD_WHOLE_NODES is not posted: the committed file is read block by block,
|
|
131
|
+
// its blocks verified off the event loop.
|
|
132
|
+
const index = await (0, chart_index_1.loadChartIndexAsync)(indexDir);
|
|
133
|
+
if (signal.aborted) {
|
|
134
|
+
throw new Error("aborted");
|
|
135
|
+
}
|
|
136
|
+
return index;
|
|
137
|
+
}
|
|
138
|
+
/** Builds in a child process from the compiled `lib/spatial/build-worker.js`. */
|
|
139
|
+
const workerBuilder = async (job, signal, progress) => {
|
|
140
|
+
const script = path_1.default.join(__dirname, "spatial", "build-worker.js");
|
|
141
|
+
const heapMb = workerHeapLimitMb();
|
|
142
|
+
const data = { ...job, heapMb };
|
|
143
|
+
const m = await runBuildProcess(script, data, signal, job.indexDir, heapMb, progress);
|
|
144
|
+
return await commit(job.indexDir, m, signal);
|
|
145
|
+
};
|
|
146
|
+
exports.workerBuilder = workerBuilder;
|
|
147
|
+
/**
|
|
148
|
+
* The whole refresh in a child process from the compiled `lib/wfs-worker.js`, which fetches
|
|
149
|
+
* with its own global fetch.
|
|
150
|
+
*/
|
|
151
|
+
const workerRefresher = async (job, signal, progress) => {
|
|
152
|
+
const script = path_1.default.join(__dirname, "wfs-worker.js");
|
|
153
|
+
const heapMb = workerHeapLimitMb();
|
|
154
|
+
const data = { ...job, build: { ...job.build, heapMb } };
|
|
155
|
+
const { indexDir } = job.build;
|
|
156
|
+
const m = await runBuildProcess(script, data, signal, indexDir, heapMb, progress);
|
|
157
|
+
const features = m.features ?? wfs_worker_1.NO_FEATURES;
|
|
158
|
+
return { index: await commit(job.build.indexDir, m, signal), features };
|
|
159
|
+
};
|
|
160
|
+
exports.workerRefresher = workerRefresher;
|
|
161
|
+
/** The download on this thread with `fetch`, then `build`: for an injected fetch or builder. */
|
|
162
|
+
function inThreadRefresher(fetch, build) {
|
|
163
|
+
return async (job, signal, progress) => {
|
|
164
|
+
const cells = (0, wfs_worker_1.refreshCells)(job);
|
|
165
|
+
if (job.download) {
|
|
166
|
+
progress?.({ phase: "download" });
|
|
167
|
+
}
|
|
168
|
+
const features = job.download
|
|
169
|
+
? await (0, wfs_1.downloadWfsCell)({ ...job.download, now: new Date(job.download.now), fetch, signal })
|
|
170
|
+
: wfs_worker_1.NO_FEATURES;
|
|
171
|
+
await (0, block_reader_1.readsSettled)(job.build.indexDir);
|
|
172
|
+
return { index: await build((0, wfs_worker_1.refreshBuild)(job, cells), signal, progress), features };
|
|
173
|
+
};
|
|
174
|
+
}
|
package/lib/config-schema.js
CHANGED
|
@@ -202,13 +202,14 @@ const LOOKAHEAD = {
|
|
|
202
202
|
distance: {
|
|
203
203
|
type: "number",
|
|
204
204
|
title: "Look-ahead distance (m)",
|
|
205
|
-
description: FOLLOWS_STYLE,
|
|
205
|
+
description: `${FOLLOWS_STYLE} Each second the look-ahead scores footprints out to this distance, at most 400; a longer one costs more CPU per tick, which counts on a Raspberry Pi 3 or Cerbo GX.`,
|
|
206
206
|
minimum: 0,
|
|
207
207
|
maximum: lookahead_1.LOOKAHEAD_LIMITS.distance
|
|
208
208
|
},
|
|
209
209
|
minHorizon: {
|
|
210
210
|
type: "number",
|
|
211
211
|
title: "Minimum horizon (s)",
|
|
212
|
+
description: "The look-ahead reaches at least this far in time at speed. A longer horizon scores more footprints each second, at most 400, so more CPU per tick.",
|
|
212
213
|
default: 60,
|
|
213
214
|
minimum: 0,
|
|
214
215
|
maximum: lookahead_1.LOOKAHEAD_LIMITS.minHorizon
|
|
@@ -239,22 +240,31 @@ const LOOKAHEAD = {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
};
|
|
243
|
+
/** A chart box half-width field, km. */
|
|
244
|
+
const BOX_KM = { type: "number", minimum: 0, maximum: 50 };
|
|
245
|
+
const boxKm = (title, def, description) => Object.assign({ title, description, default: def }, BOX_KM);
|
|
242
246
|
const CHART_INDEX = {
|
|
243
247
|
indexDir: {
|
|
244
248
|
type: "string",
|
|
245
249
|
title: "Chart index directory (optional)",
|
|
246
250
|
description: "Where the depth index built from the cells is kept between restarts. Default: <plugin data dir>/index"
|
|
247
251
|
},
|
|
248
|
-
rebuildIndex: {
|
|
249
|
-
|
|
250
|
-
|
|
252
|
+
rebuildIndex: {
|
|
253
|
+
type: "boolean",
|
|
254
|
+
title: "Rebuild index at start",
|
|
255
|
+
description: "Builds the index again at every start, with the CPU, memory and flash writes of a first build. The index is rebuilt anyway when a cell changes; leave this off.",
|
|
256
|
+
default: false
|
|
257
|
+
},
|
|
258
|
+
leafZoom: {
|
|
251
259
|
type: "number",
|
|
252
|
-
title: "Index
|
|
253
|
-
description: "
|
|
254
|
-
default:
|
|
255
|
-
minimum:
|
|
256
|
-
maximum:
|
|
257
|
-
}
|
|
260
|
+
title: "Index leaf zoom",
|
|
261
|
+
description: "Size of the smallest index tile: about 19 m across at 20 and 38 m at 19 at 60° N. Each step down needs fewer tiles, so less memory, disk and build time. A tile keeps the shallowest depth inside it, so a coarser index warns more, never less. Consider 19 on a 1 GB Raspberry Pi 3 or Cerbo GX. Changing it rebuilds the index.",
|
|
262
|
+
default: 20,
|
|
263
|
+
minimum: 14,
|
|
264
|
+
maximum: 22
|
|
265
|
+
},
|
|
266
|
+
boxRadiusKm: boxKm("Index box around the vessel (km)", 10, "Half-width of the area the index is built in around the vessel; it is rebuilt as the vessel moves. Outside it the plugin reports no coverage, and the reason says the vessel is outside the chart box. At speed the box grows past this radius, up to the largest index box, so the vessel cannot reach its edge, less the look-ahead, before the next box is built: a box must hold 1.5 times the distance run during the last build (grown with the build area) plus the look-ahead and a 2.4 km band. It grows at once when too small, and shrinks only at the next rebuild once the speed needs under 70% of it. 0 indexes every cell (slow for large chart sets); a radius under 2 km is raised to 2. With the Traficom WFS on, its radius is used, and the box grows to 25 km at most."),
|
|
267
|
+
boxMaxRadiusKm: boxKm("Largest index box at speed (km)", 0, "The box around the vessel grows up to this half-width at speed. 0 sizes it from the RAM the system reports: 10 km on a 1 GB Raspberry Pi or Cerbo GX, about 19 km on a 4 GB Raspberry Pi 4, about 27 km with 8 GB. The status says when even this box is too small for the speed and build time.")
|
|
258
268
|
};
|
|
259
269
|
const EXPERT = {
|
|
260
270
|
...CHART_INDEX,
|
package/lib/config.js
CHANGED
|
@@ -9,6 +9,7 @@ const vessel_1 = require("./model/vessel");
|
|
|
9
9
|
const route_request_1 = require("./api/route-request");
|
|
10
10
|
const wfs_1 = require("./ingest/wfs");
|
|
11
11
|
const config_presets_1 = require("./config-presets");
|
|
12
|
+
const box_sizing_1 = require("./box-sizing");
|
|
12
13
|
var config_schema_1 = require("./config-schema");
|
|
13
14
|
Object.defineProperty(exports, "SCHEMA", { enumerable: true, get: function () { return config_schema_1.SCHEMA; } });
|
|
14
15
|
exports.DEFAULT_ROUTE_CHECK = { speed: 2.5, xteLimit: 30, probability: 0.01 };
|
|
@@ -161,6 +162,7 @@ function parseConfig(raw) {
|
|
|
161
162
|
rebuildIndex: r.rebuildIndex === true,
|
|
162
163
|
leafZoom: Math.round(num(r.leafZoom, 20, 14, 22)),
|
|
163
164
|
boxRadiusKm: boxRadius(r.boxRadiusKm),
|
|
165
|
+
boxMaxRadiusKm: num(r.boxMaxRadiusKm, 0, 0, box_sizing_1.MAX_BOX_KM),
|
|
164
166
|
warningStyle: style,
|
|
165
167
|
vessel: {
|
|
166
168
|
draft: optionalPositive(v.draft),
|