signalk-underkeel 0.3.0 → 0.4.0

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