signalk-chiplog 2.1.0 → 2.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +73 -34
  3. package/index.js +53 -17
  4. package/lib/api.js +15 -0
  5. package/lib/{forecast-schedule.js → background-schedule.js} +11 -11
  6. package/lib/database.js +44 -0
  7. package/lib/detection.js +9 -4
  8. package/lib/entries.js +7 -2
  9. package/lib/event-watcher.js +1 -3
  10. package/lib/events.js +38 -3
  11. package/lib/export.js +8 -0
  12. package/lib/influx-history.js +9 -4
  13. package/lib/landmark-finder.js +209 -0
  14. package/lib/landmarks.js +210 -0
  15. package/lib/logbook-pdf.js +45 -7
  16. package/lib/replay-job.js +1 -0
  17. package/package.json +3 -2
  18. package/public/app.css +121 -0
  19. package/public/entry/sw.js +1 -0
  20. package/public/js/animation/camera.mjs +180 -0
  21. package/public/js/animation/formats.mjs +45 -0
  22. package/public/js/animation/mercator.mjs +50 -0
  23. package/public/js/animation/mp4.mjs +77 -0
  24. package/public/js/animation/player.mjs +120 -0
  25. package/public/js/animation/renderer.mjs +326 -0
  26. package/public/js/animation/schedule.mjs +49 -0
  27. package/public/js/animation/storyboard.mjs +195 -0
  28. package/public/js/animation/tiles.mjs +219 -0
  29. package/public/js/animation/timeline.mjs +186 -0
  30. package/public/js/components/AnimationExport.mjs +173 -0
  31. package/public/js/components/AnimationView.mjs +439 -0
  32. package/public/js/components/ExportView.mjs +3 -8
  33. package/public/js/components/PassageView.mjs +29 -8
  34. package/public/js/components/Timeline.mjs +30 -7
  35. package/public/js/context.mjs +7 -0
  36. package/public/js/days.mjs +10 -0
  37. package/public/js/format.mjs +11 -0
  38. package/public/js/i18n.mjs +129 -0
  39. package/public/js/landmarks.mjs +128 -0
  40. package/public/js/main.mjs +8 -5
  41. package/public/vendor/mediabunny-LICENSE +373 -0
  42. package/public/vendor/mediabunny.min.mjs +50 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,50 @@ All notable changes to Chiplog are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.3.0] - 2026-09-18
10
+
11
+ ### Added
12
+
13
+ - **InfluxDB query timeout** setting (`influxQueryTimeoutSeconds`, 30 s by default): how long a retrospective replay
14
+ waits for the InfluxDB server to answer before giving up on it as unreachable or overloaded, now configurable instead
15
+ of a fixed 30 seconds — useful against a Raspberry Pi that is simply slow to answer a six-hour chunk.
16
+
17
+ ### Fixed
18
+
19
+ - **Retrospective replay backfilling a gap before passages already logged live** no longer dated every reconstructed
20
+ passage to the most recent existing passage's end time. Detection's guard against an out-of-order departure looked at
21
+ the latest `end_time` in the whole logbook rather than only at passages that actually preceded the new one, so filling
22
+ in an earlier gap — installing Chiplog after the fact, or after a stop — pinned every reconstructed departure to that
23
+ unrelated, later date, and the replay summary reported no passage found for the requested period even though entries
24
+ had been created.
25
+
26
+ ## [2.2.0] - 2026-09-18
27
+
28
+ ### Added
29
+
30
+ - **Landmark bearings (amers)** — every position in the log is now also given the way a paper logbook gives one: a
31
+ distance and a bearing from the nearest landmark, "2,3 M ENE (065°) — Phare de Chauveau", under the coordinates in a
32
+ lighter grey, on the passage page and in the PDF logbook. The coordinates themselves are unchanged. Landmarks —
33
+ lighthouses, capes, named towers, harbours — are fetched from OpenStreetMap area by area and kept, so past passages
34
+ fill in as soon as their area is known and the same waters are never asked for twice; the bearing is computed when the
35
+ page is drawn, never stored. The landmark quoted is the one closest relative to its own range, so a lighthouse three
36
+ miles off wins over a marina alongside, and offshore the coordinates stay alone. A new setting, **Read each journal
37
+ line against the nearest landmark**, turns it off.
38
+ - **Animation** — a new page that replays passages on the map. Pick a start and an end date and every passage between
39
+ them plays in sequence, the time spent in port skipped, so a week's cruise takes seconds. The map follows the boat at
40
+ a scale chosen for each passage — a short hop stays readable instead of being magnified, a long crossing gets a wider
41
+ view without leaving the boat crawling across empty water — and a bubble on the chart shows the speed, the distance
42
+ covered since the animation began and the date and hour. Play, pause, four speeds (×0,5, ×1, ×2, ×4 — an hour of
43
+ sailing per second at ×1) and a slider over the animation's own time. A passage page links straight to it with its own
44
+ dates filled in.
45
+ - **Save an animation as an MP4** — Mobile (9:16), Portrait (3:4), Square (1:1), Landscape (4:3) or Widescreen (16:9),
46
+ encoded in the browser: nothing is uploaded, and the Signal K server renders nothing. Each frame's map is downloaded
47
+ just before that frame is drawn, so the film is the same whatever the connection was doing and no range is ever too
48
+ long to export. Needs a browser with WebCodecs (Chrome, Edge, Safari 17, Firefox 130 and later); without one the
49
+ animation still plays on screen.
50
+ - **Delete an alarm** — a logged alarm can now be removed from a passage's log, like a crew-entered line already could.
51
+ Deleting either the alarm or the line that later cleared it removes both, so no orphaned half is left behind.
52
+
9
53
  ## [2.1.0] - 2026-09-17
10
54
 
11
55
  ### Added
@@ -222,7 +266,9 @@ First release.
222
266
  - REST API under `/plugins/signalk-chiplog/api`, documented in [docs/API.md](docs/API.md).
223
267
  - Single SQLite database through Node's built-in `node:sqlite`: no native module to build.
224
268
 
225
- [Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.1.0...HEAD
269
+ [Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.3.0...HEAD
270
+ [2.3.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.2.0...v2.3.0
271
+ [2.2.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.1.0...v2.2.0
226
272
  [2.1.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.0.0...v2.1.0
227
273
  [2.0.0]: https://github.com/ricard33/signalk-chiplog/compare/v1.2.0...v2.0.0
228
274
  [1.2.0]: https://github.com/ricard33/signalk-chiplog/compare/v1.1.0...v1.2.0
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Chiplog
2
2
 
3
- An automated logbook for [Signal K](https://signalk.org). Chiplog writes the logbook from the data already on your
4
- boat's Signal K server — passages, track, engine and sail, instrument readings, alarms — and lets the crew add what
3
+ An automated nautical logbook for [Signal K](https://signalk.org). Chiplog writes the logbook from the data already on
4
+ your boat's Signal K server — passages, track, engine and sail, instrument readings, alarms — and lets the crew add what
5
5
  sensors cannot know from a tablet at the helm: manoeuvres, notes and handwriting.
6
6
 
7
7
  - **One entry per passage**, opened when the boat leaves and closed as soon as it arrives, carrying on after short stops
@@ -11,6 +11,8 @@ sensors cannot know from a tablet at the helm: manoeuvres, notes and handwriting
11
11
  - **Automatic events**: alarms, autopilot changes, strong wind, falling barometer.
12
12
  - **Departure and arrival names**, looked up online and corrected once for good.
13
13
  - **Consultation webapp**: logbook by day, map, timeline, corrections, export.
14
+ - **Replay a range of passages** on the map — an hour of sailing per second — and save it as an MP4 for a phone, a
15
+ square post or a widescreen, rendered entirely in your browser.
14
16
  - **Tablet entry app**: big buttons for gloves and wet fingers, stylus handwriting, night mode, works through Wi-Fi
15
17
  dropouts.
16
18
  - **Abandon-ship copy**: JSON, CSV and GPX, downloadable or written to a USB drive.
@@ -169,6 +171,26 @@ Departures and arrivals are named automatically:
169
171
  3. **Corrections are remembered.** Renaming a departure or arrival in the webapp also renames that place for every later
170
172
  passage starting or ending nearby. Past passages keep the name they recorded.
171
173
 
174
+ ### Landmarks (amers)
175
+
176
+ Every position in the log is also given the way a paper logbook gives one — as a distance and a bearing **from a
177
+ landmark**, under the coordinates, in a lighter grey:
178
+
179
+ ```text
180
+ 46°08.88′N 001°12.90′W
181
+ 2,3 M ENE (065°) — Phare de Chauveau
182
+ ```
183
+
184
+ - **The landmarks come from OpenStreetMap**, fetched area by area through Overpass and kept: lighthouses and major
185
+ lights, capes, named towers and other seamark landmarks, minor lights, isolated-danger and safe-water beacons,
186
+ harbours and marinas. The numbered marks of a channel are left out — "6 c" says nothing in a logbook.
187
+ - **The nearest useful one wins**, not simply the nearest: each kind carries a range (15 nm for a lighthouse, 3 for a
188
+ harbour…), narrowed by the light's own range when known, and the landmark closest relative to its range is the one
189
+ quoted. Offshore, beyond them all, the coordinates stay alone.
190
+ - **Past passages fill in by themselves** once their area has been fetched — the bearing is worked out when the page or
191
+ the PDF is drawn, never stored.
192
+ - Turn **Read each journal line against the nearest landmark** off to keep the boat off Overpass entirely.
193
+
172
194
  ## The logbook webapp 💻
173
195
 
174
196
  Open **Chiplog** from the Signal K webapps, or `/signalk-chiplog/`. Reading needs no more than read-only access.
@@ -189,13 +211,20 @@ Open **Chiplog** from the Signal K webapps, or `/signalk-chiplog/`. Reading need
189
211
  current noted at departure), and the log: every reading and event in order, including handwritten notes. A passage in
190
212
  progress refreshes every minute. Each line's comment can be edited (read/write access); a manoeuvre or note the crew
191
213
  logged themselves can also be deleted — automatic lines (alarms, autopilot, weather, corrections) can only be
192
- annotated.
214
+ annotated. Under each position, in grey, its bearing and distance from the nearest landmark.
193
215
  - **Corrections** (read/write access):
194
216
  - rename the departure, or the arrival once the passage is closed — a passage in progress has none yet to rename;
195
217
  - switch an engine period to sail or back;
196
218
  - close a passage in progress, e.g. to confirm an arrival;
197
219
  - merge with the previous or next passage;
198
220
  - delete a passage (admin).
221
+ - **Animation** — pick two dates and every passage between them replays on the map, one after another, the port time
222
+ skipped. The map follows the boat at a scale chosen for each passage — a short hop kept readable rather than
223
+ magnified, a long crossing allowed a wider view but never so wide the boat crawls across empty water — while a bubble
224
+ shows the speed, the distance covered since the start and the date. Play, pause and a slider over the animation's own
225
+ time, at ×0,5, ×1, ×2 or ×4. **Export MP4** saves it as a video in one of five shapes (Mobile 9:16, Portrait 3:4,
226
+ Square 1:1, Landscape 4:3, Widescreen 16:9). Everything happens in the browser: nothing is rendered or encoded on the
227
+ Signal K server, and the map tiles are the only thing downloaded.
199
228
  - **Export** — download the whole logbook or a date range as a PDF logbook to print, JSON, CSV or GPX, and write the
200
229
  abandon-ship copy to the USB drive now (admin). The PDF is written in the webapp's language and the device's time
201
230
  zone.
@@ -291,33 +320,36 @@ a regular Signal K user account.
291
320
 
292
321
  In the Signal K admin, **Apps & Plugins → Configuration → Chiplog**.
293
322
 
294
- | Setting | Default | What it does |
295
- | ---------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
296
- | Stop duration within which a new departure continues the passage | 30 min | A passage closes when the boat stops; leaving again sooner reopens it, the stop kept as a stopover. |
297
- | Under-way speed without navigation.state | 1 kn | Used only without signalk-autostate: under way above it, stopped below half of it. |
298
- | Propulsion assumed without engine data | sail | When nothing says whether the engine is running. Set to engine on a motorboat. |
299
- | Instrument snapshot interval | 60 min | Readings on this clock boundary during a passage. |
300
- | Track point interval | 15 s | A track point at least this often while moving. |
301
- | Place matching radius | 200 m | A departure or arrival this close to a known place takes its name. |
302
- | Name departures and arrivals with online geocoding | on | Turn off to never send positions online; places are then named after their coordinates until corrected. |
303
- | Geocoding service | `https://nominatim.openstreetmap.org` | Any Nominatim-compatible service, e.g. a self-hosted one. |
304
- | Fetch the tide forecast at departure | on | Turn off to never send the departure position online; the passage page then shows no tide. |
305
- | Marine service | `https://marine-api.open-meteo.com/v1/marine` | Any Open-Meteo Marine-compatible service, e.g. a self-hosted one. Serves the tide, and the sea state and current of the weather forecast. |
306
- | Fetch the marine weather forecast at departure | on | Turn off to never send the departure position to the weather services; the passage page and the PDF then show no forecast. |
307
- | Weather service | `https://api.open-meteo.com/v1/forecast` | Any Open-Meteo-compatible forecast service, e.g. a self-hosted one. |
308
- | USB export directory | | Where the abandon-ship copy is written, e.g. `/media/usb`. Empty turns the USB copy off. |
309
- | Automatic USB copy interval | 15 min | How often the USB copy is brought up to date. 0 turns the periodic copy off. |
310
- | Copy to the USB drive at each arrival | on | Brings the USB copy up to date as soon as a passage ends. |
311
- | Logbook language (PDF) | en | Language of the PDF logbooks on the USB drive (English or French). |
312
- | Ship's time zone (PDF) | the server's | Time zone of the PDF logbooks on the USB drive, e.g. `Europe/Paris`. |
313
- | Wind speed thresholds | 20, 30 kn | Logged when the 2-minute average true wind crosses them. |
314
- | Barometric drop warning | 4 hPa / 3 h | 0 turns it off. |
315
- | InfluxDB host (retrospective analysis) | — | Local or remote host of the InfluxDB 1.x database signalk-to-influxdb writes to. Empty turns the retrospective analysis page off. |
316
- | InfluxDB port | 8086 | |
317
- | InfluxDB database | — | |
318
- | InfluxDB username / password | | Leave empty if the database needs none. |
319
- | InfluxDB protocol | http | `http` or `https`. |
320
- | InfluxDB vessel context | this server's own | Only needed running the replay from a different Signal K server than the one that wrote the history, e.g. development pointed at a production database. |
323
+ | Setting | Default | What it does |
324
+ | ---------------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
325
+ | Stop duration within which a new departure continues the passage | 30 min | A passage closes when the boat stops; leaving again sooner reopens it, the stop kept as a stopover. |
326
+ | Under-way speed without navigation.state | 1 kn | Used only without signalk-autostate: under way above it, stopped below half of it. |
327
+ | Propulsion assumed without engine data | sail | When nothing says whether the engine is running. Set to engine on a motorboat. |
328
+ | Instrument snapshot interval | 60 min | Readings on this clock boundary during a passage. |
329
+ | Track point interval | 15 s | A track point at least this often while moving. |
330
+ | Place matching radius | 200 m | A departure or arrival this close to a known place takes its name. |
331
+ | Name departures and arrivals with online geocoding | on | Turn off to never send positions online; places are then named after their coordinates until corrected. |
332
+ | Geocoding service | `https://nominatim.openstreetmap.org` | Any Nominatim-compatible service, e.g. a self-hosted one. |
333
+ | Read each journal line against the nearest landmark | on | Fetches the landmarks of the areas sailed through from OpenStreetMap, so each position is also given as a bearing and distance from one. Turn off to keep the coordinates alone. |
334
+ | Landmark service (Overpass API) | `https://overpass-api.de/api/interpreter` | Any Overpass-compatible service, e.g. a self-hosted one. |
335
+ | Fetch the tide forecast at departure | on | Turn off to never send the departure position online; the passage page then shows no tide. |
336
+ | Marine service | `https://marine-api.open-meteo.com/v1/marine` | Any Open-Meteo Marine-compatible service, e.g. a self-hosted one. Serves the tide, and the sea state and current of the weather forecast. |
337
+ | Fetch the marine weather forecast at departure | on | Turn off to never send the departure position to the weather services; the passage page and the PDF then show no forecast. |
338
+ | Weather service | `https://api.open-meteo.com/v1/forecast` | Any Open-Meteo-compatible forecast service, e.g. a self-hosted one. |
339
+ | USB export directory | | Where the abandon-ship copy is written, e.g. `/media/usb`. Empty turns the USB copy off. |
340
+ | Automatic USB copy interval | 15 min | How often the USB copy is brought up to date. 0 turns the periodic copy off. |
341
+ | Copy to the USB drive at each arrival | on | Brings the USB copy up to date as soon as a passage ends. |
342
+ | Logbook language (PDF) | en | Language of the PDF logbooks on the USB drive (English or French). |
343
+ | Ship's time zone (PDF) | the server's | Time zone of the PDF logbooks on the USB drive, e.g. `Europe/Paris`. |
344
+ | Wind speed thresholds | 20, 30 kn | Logged when the 2-minute average true wind crosses them. |
345
+ | Barometric drop warning | 4 hPa / 3 h | 0 turns it off. |
346
+ | InfluxDB host (retrospective analysis) | — | Local or remote host of the InfluxDB 1.x database signalk-to-influxdb writes to. Empty turns the retrospective analysis page off. |
347
+ | InfluxDB port | 8086 | |
348
+ | InfluxDB database | | |
349
+ | InfluxDB username / password | | Leave empty if the database needs none. |
350
+ | InfluxDB protocol | http | `http` or `https`. |
351
+ | InfluxDB query timeout | 30 s | Each retrospective query gives up and reports an error past this, instead of hanging against an unreachable or overloaded database. |
352
+ | InfluxDB vessel context | this server's own | Only needed running the replay from a different Signal K server than the one that wrote the history, e.g. development pointed at a production database. |
321
353
 
322
354
  ## Signal K data used 🔌
323
355
 
@@ -378,6 +410,8 @@ at the time.
378
410
  - **Place names.** With geocoding on, the position of each departure and arrival that matches no known place is sent to
379
411
  the geocoding service — OpenStreetMap's public Nominatim by default. Nothing else is sent, and nothing at all when it
380
412
  is off.
413
+ - **Landmarks.** With them on, the area a passage sailed through — a half-degree box, not its track — is sent to the
414
+ Overpass service, OpenStreetMap's public instance by default, once per area ever. Nothing at all when it is off.
381
415
  - **Tide forecast.** With it on, the departure position of each passage is sent to the tide service — the public
382
416
  Open-Meteo by default — once, at departure. Nothing at all when it is off.
383
417
  - **Weather forecast.** With it on, the departure position of each passage is sent to the weather service and to the
@@ -464,9 +498,10 @@ context** in the plugin configuration.
464
498
  ### A retrospective analysis takes minutes then fails with no clear reason
465
499
 
466
500
  The InfluxDB server did not answer — unreachable, overloaded, a firewall or a VPN not connected. Each query now gives up
467
- after 30 seconds with the connection problem it ran into, rather than hanging until some far longer, less informative
468
- failure; check that the server named in the plugin configuration is reachable from wherever Signal K runs, and that it
469
- is not overloaded.
501
+ after **InfluxDB query timeout** (30 seconds by default) with the connection problem it ran into, rather than hanging
502
+ until some far longer, less informative failure; check that the server named in the plugin configuration is reachable
503
+ from wherever Signal K runs, and that it is not overloaded — or raise the timeout if it is simply slow to answer a
504
+ six-hour chunk.
470
505
 
471
506
  ### A retrospective analysis over several days makes the InfluxDB server unresponsive
472
507
 
@@ -486,7 +521,11 @@ between attempts; nothing is lost, it just takes longer to resolve.
486
521
  ## Limitations 🚧
487
522
 
488
523
  - **Not yet:** a places page, and editing manoeuvre shortcuts from the webapps.
489
- - **Offline charts** are not provided.
524
+ - **Offline charts** are not provided; the animation draws the tracks on a blank sea when there is no connection.
525
+ - **The MP4 export needs a browser with WebCodecs** — Chrome, Edge, Safari 17 or Firefox 130 and later. Without it the
526
+ animation still plays on screen, and the export button is simply not offered.
527
+ - **A long animation takes a while to export**: every frame waits for its map before it is drawn, so the video is the
528
+ same whatever the connection was doing, but a long range means a lot of tiles. Turning the seamarks off halves them.
490
529
  - **One vessel per Signal K server**, and no per-crew-member authorship.
491
530
 
492
531
  ## Development 🧑‍💻
package/index.js CHANGED
@@ -2,9 +2,10 @@ const { registerRoutes } = require('./lib/api');
2
2
  const { openDatabase } = require('./lib/database');
3
3
  const { createPassageDetector, DETECTION_DEFAULTS, TICK_INTERVAL_MS } = require('./lib/detection');
4
4
  const { ApiError } = require('./lib/errors');
5
- const { createForecastSchedule } = require('./lib/forecast-schedule');
5
+ const { createBackgroundSchedule } = require('./lib/background-schedule');
6
6
  const { createEventWatcher, CHECK_INTERVAL_MS, EVENT_DEFAULTS } = require('./lib/event-watcher');
7
7
  const { INFLUX_DEFAULTS } = require('./lib/influx-history');
8
+ const { createLandmarkFinder, LANDMARK_DEFAULTS } = require('./lib/landmark-finder');
8
9
  const { OBSERVATION_DEFAULTS } = require('./lib/observation-recorder');
9
10
  const { createPlaceNamer, GEOCODING_DEFAULTS } = require('./lib/place-names');
10
11
  const { PROPULSION_DEFAULTS } = require('./lib/propulsion-detector');
@@ -26,6 +27,9 @@ const DEFAULT_PLACE_MATCH_RADIUS = 200;
26
27
  const FIRST_NAMING_DELAY_MS = 5 * 1000;
27
28
  const NAMING_ERROR_RETRY_MS = 5 * 60 * 1000;
28
29
  const FIRST_FORECAST_DELAY_MS = 5 * 1000;
30
+ // Landmarks are not needed for the passage under way, only for reading it back,
31
+ // so the first lookup waits for the busier start-up work to be done.
32
+ const FIRST_LANDMARK_DELAY_MS = 30 * 1000;
29
33
 
30
34
  const MOTION_LABELS = { underway: 'Under way', stopped: 'Stopped', unknown: 'Waiting for data' };
31
35
  const MODE_LABELS = { autostate: 'navigation.state', fallback: 'speed fallback' };
@@ -56,7 +60,7 @@ module.exports = function (app) {
56
60
  let settings = null;
57
61
  let detector = null;
58
62
  let namer = null;
59
- let forecasts = [];
63
+ let schedules = [];
60
64
  // The open passage detection last reported, to notice a new one.
61
65
  let lastActiveEntryId = null;
62
66
  let usbExport = null;
@@ -132,6 +136,19 @@ module.exports = function (app) {
132
136
  description: 'The public OpenStreetMap instance by default, or a self-hosted Nominatim',
133
137
  default: GEOCODING_DEFAULTS.geocodingUrl
134
138
  },
139
+ landmarksEnabled: {
140
+ type: 'boolean',
141
+ title: 'Read each journal line against the nearest landmark',
142
+ description:
143
+ 'Fetches the lighthouses, capes, towers and harbours of the areas sailed through from OpenStreetMap, so every position in the logbook is also given as a bearing and distance from the nearest one (© OpenStreetMap contributors, ODbL). Without it, only the coordinates are shown',
144
+ default: LANDMARK_DEFAULTS.landmarksEnabled
145
+ },
146
+ overpassUrl: {
147
+ type: 'string',
148
+ title: 'Landmark service (Overpass API)',
149
+ description: 'The public Overpass instance by default, or a self-hosted one',
150
+ default: LANDMARK_DEFAULTS.overpassUrl
151
+ },
135
152
  tidesEnabled: {
136
153
  type: 'boolean',
137
154
  title: 'Fetch the tide forecast at departure',
@@ -239,6 +256,14 @@ module.exports = function (app) {
239
256
  enum: ['http', 'https'],
240
257
  default: INFLUX_DEFAULTS.influxProtocol
241
258
  },
259
+ influxQueryTimeoutSeconds: {
260
+ type: 'number',
261
+ title: 'InfluxDB query timeout (seconds)',
262
+ description:
263
+ 'Each retrospective query is given up on and reported as an error past this, rather than hanging indefinitely against an unreachable or overloaded database',
264
+ default: INFLUX_DEFAULTS.influxQueryTimeoutSeconds,
265
+ minimum: 1
266
+ },
242
267
  influxSelfContext: {
243
268
  type: 'string',
244
269
  title: 'InfluxDB vessel context',
@@ -252,10 +277,11 @@ module.exports = function (app) {
252
277
  try {
253
278
  const outcome = detector.tick();
254
279
  usbExport.afterDetection(outcome);
255
- // A passage just opened: fetch its forecasts now, not at the next idle
256
- // poll or at the end of a retry delay left from an earlier failure.
280
+ // A passage just opened: fetch its forecasts and the landmarks of where
281
+ // it is starting from now, not at the next idle poll or at the end of a
282
+ // retry delay left from an earlier failure.
257
283
  if (outcome.activeEntryId !== null && outcome.activeEntryId !== lastActiveEntryId) {
258
- forecasts.forEach((schedule) => schedule.nudge());
284
+ schedules.forEach((schedule) => schedule.nudge());
259
285
  }
260
286
  lastActiveEntryId = outcome.activeEntryId;
261
287
  const usbError = usbExport.status().lastError;
@@ -344,6 +370,8 @@ module.exports = function (app) {
344
370
  placeMatchRadius: config.placeMatchRadius ?? DEFAULT_PLACE_MATCH_RADIUS,
345
371
  geocodingEnabled: config.geocodingEnabled ?? GEOCODING_DEFAULTS.geocodingEnabled,
346
372
  geocodingUrl: config.geocodingUrl || GEOCODING_DEFAULTS.geocodingUrl,
373
+ landmarksEnabled: config.landmarksEnabled ?? LANDMARK_DEFAULTS.landmarksEnabled,
374
+ overpassUrl: config.overpassUrl || LANDMARK_DEFAULTS.overpassUrl,
347
375
  tidesEnabled: config.tidesEnabled ?? TIDE_DEFAULTS.tidesEnabled,
348
376
  tideUrl: config.tideUrl || TIDE_DEFAULTS.tideUrl,
349
377
  weatherEnabled: config.weatherEnabled ?? WEATHER_DEFAULTS.weatherEnabled,
@@ -364,6 +392,8 @@ module.exports = function (app) {
364
392
  influxUsername: config.influxUsername || null,
365
393
  influxPassword: config.influxPassword || null,
366
394
  influxProtocol: config.influxProtocol || INFLUX_DEFAULTS.influxProtocol,
395
+ influxQueryTimeoutSeconds:
396
+ config.influxQueryTimeoutSeconds ?? INFLUX_DEFAULTS.influxQueryTimeoutSeconds,
367
397
  influxSelfContext: config.influxSelfContext || null
368
398
  };
369
399
 
@@ -411,23 +441,29 @@ module.exports = function (app) {
411
441
  });
412
442
  namingTimer = setTimeout(runNaming, FIRST_NAMING_DELAY_MS);
413
443
  // Same idea as geocoding: network calls fetching the tide and weather
414
- // forecasts near a recent departure, each on its own chain rather than
415
- // inside detection.
416
- const forecastOptions = { db: database, settings, userAgent: `signalk-chiplog/${version}` };
444
+ // forecasts near a recent departure, and the landmarks of the areas
445
+ // sailed through, each on its own chain rather than inside detection.
446
+ const networkOptions = { db: database, settings, userAgent: `signalk-chiplog/${version}` };
417
447
  const log = (level, message) => (level === 'error' ? app.error(message) : app.debug(message));
418
448
  lastActiveEntryId = null;
419
- forecasts = [
420
- createForecastSchedule({
421
- label: 'Tide',
422
- forecaster: createTideForecaster(forecastOptions),
449
+ schedules = [
450
+ createBackgroundSchedule({
451
+ label: 'Tide forecast',
452
+ resolver: createTideForecaster(networkOptions),
423
453
  log,
424
454
  firstDelayMs: FIRST_FORECAST_DELAY_MS
425
455
  }),
426
- createForecastSchedule({
427
- label: 'Weather',
428
- forecaster: createWeatherForecaster(forecastOptions),
456
+ createBackgroundSchedule({
457
+ label: 'Weather forecast',
458
+ resolver: createWeatherForecaster(networkOptions),
429
459
  log,
430
460
  firstDelayMs: FIRST_FORECAST_DELAY_MS
461
+ }),
462
+ createBackgroundSchedule({
463
+ label: 'Landmark lookup',
464
+ resolver: createLandmarkFinder(networkOptions),
465
+ log,
466
+ firstDelayMs: FIRST_LANDMARK_DELAY_MS
431
467
  })
432
468
  ];
433
469
  lastStatus = null;
@@ -455,8 +491,8 @@ module.exports = function (app) {
455
491
  clearTimeout(namingTimer);
456
492
  namer?.stop();
457
493
  namer = null;
458
- forecasts.forEach((schedule) => schedule.stop());
459
- forecasts = [];
494
+ schedules.forEach((schedule) => schedule.stop());
495
+ schedules = [];
460
496
  usbExport?.stop();
461
497
  usbExport = null;
462
498
  replayJob?.cancel();
package/lib/api.js CHANGED
@@ -4,6 +4,7 @@ const { ApiError, badRequest, conflict, notFound } = require('./errors');
4
4
  const entries = require('./entries');
5
5
  const events = require('./events');
6
6
  const { renderExport } = require('./export');
7
+ const { listEntryLandmarks } = require('./landmarks');
7
8
  const { isTimeZone, PDF_LANGUAGES } = require('./logbook-pdf');
8
9
  const { toGeoJson, toGpx } = require('./formats');
9
10
  const manoeuvreTypes = require('./manoeuvre-types');
@@ -229,6 +230,20 @@ function registerRoutes(router, { getContext, logError }) {
229
230
  })
230
231
  );
231
232
 
233
+ // The amers a line of this passage's journal can be read against (SPEC
234
+ // §4.13); which one each line takes, and the bearing, is worked out where
235
+ // the line is shown. Empty until the area's landmarks have been fetched.
236
+ readonly.get(
237
+ '/api/entries/:id/landmarks',
238
+ handle(({ db }, req) => {
239
+ const id = entryId(req);
240
+ entries.requireEntryRow(db, id);
241
+ const { limit, offset } = v.parsePagination(req.query);
242
+ const items = listEntryLandmarks(db, id);
243
+ return { total: items.length, limit, offset, items: items.slice(offset, offset + limit) };
244
+ })
245
+ );
246
+
232
247
  readonly.get(
233
248
  '/api/entries/:id/tide',
234
249
  handle(({ db }, req) => {
@@ -1,12 +1,12 @@
1
- // Runs a departure forecaster (tide, weather) as its own chain of timeouts:
2
- // each `resolveNext()` says when the next is due. `nudge()` runs it now --
3
- // when a passage opens, rather than leaving it to the idle poll or a retry
4
- // delay. A nudge during a fetch runs it again as soon as that one ends,
5
- // never a second chain alongside.
1
+ // Runs a background resolver -- a departure forecast, a landmark lookup -- as
2
+ // its own chain of timeouts: each `resolveNext()` says when the next is due.
3
+ // `nudge()` runs it now -- when a passage opens, rather than leaving it to the
4
+ // idle poll or a retry delay. A nudge during a fetch runs it again as soon as
5
+ // that one ends, never a second chain alongside.
6
6
 
7
7
  const ERROR_RETRY_MS = 5 * 60 * 1000;
8
8
 
9
- function createForecastSchedule({ label, forecaster, log, firstDelayMs }) {
9
+ function createBackgroundSchedule({ label, resolver, log, firstDelayMs }) {
10
10
  let timer = null;
11
11
  let running = false;
12
12
  let nudged = false;
@@ -23,9 +23,9 @@ function createForecastSchedule({ label, forecaster, log, firstDelayMs }) {
23
23
  nudged = false;
24
24
  let result;
25
25
  try {
26
- result = await forecaster.resolveNext();
26
+ result = await resolver.resolveNext();
27
27
  } catch (err) {
28
- log('error', `${label} forecast failed: ${err.stack ?? err}`);
28
+ log('error', `${label} failed: ${err.stack ?? err}`);
29
29
  result = { retryInMs: ERROR_RETRY_MS };
30
30
  }
31
31
  running = false;
@@ -36,7 +36,7 @@ function createForecastSchedule({ label, forecaster, log, firstDelayMs }) {
36
36
  // Expected whenever the boat is out of reach of a network.
37
37
  log(
38
38
  'debug',
39
- `${label} forecast unavailable, retrying in ${Math.round(result.retryInMs / 60000)} min: ${result.error.message}`
39
+ `${label} unavailable, retrying in ${Math.round(result.retryInMs / 60000)} min: ${result.error.message}`
40
40
  );
41
41
  }
42
42
  schedule(nudged ? 0 : result.retryInMs);
@@ -59,9 +59,9 @@ function createForecastSchedule({ label, forecaster, log, firstDelayMs }) {
59
59
  stop() {
60
60
  stopped = true;
61
61
  clearTimeout(timer);
62
- forecaster.stop();
62
+ resolver.stop();
63
63
  }
64
64
  };
65
65
  }
66
66
 
67
- module.exports = { createForecastSchedule };
67
+ module.exports = { createBackgroundSchedule };
package/lib/database.js CHANGED
@@ -314,6 +314,50 @@ const MIGRATIONS = [
314
314
  CREATE UNIQUE INDEX idx_log_entry_crew_unique_member
315
315
  ON log_entry_crew (entry_id, crew_member_id)
316
316
  WHERE crew_member_id IS NOT NULL;
317
+ `,
318
+ `
319
+ -- Landmarks -- amers (SPEC §4.13): the named features a journal line's
320
+ -- position is read against, "1.2 nm NE (053°) of Phare du Cap-Ferret".
321
+ -- Fetched from OpenStreetMap area by area and kept: the bearing itself is
322
+ -- worked out at read time, so a past passage fills in as soon as its area
323
+ -- is known, and nothing here is part of the logbook's record.
324
+ CREATE TABLE landmarks (
325
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
326
+ osm_type TEXT NOT NULL CHECK (osm_type IN ('node', 'way', 'relation')),
327
+ osm_id INTEGER NOT NULL,
328
+ name TEXT NOT NULL,
329
+ kind TEXT NOT NULL CHECK (
330
+ kind IN ('lighthouse', 'light', 'cape', 'landmark', 'beacon', 'harbour')
331
+ ),
332
+ lat REAL NOT NULL,
333
+ lon REAL NOT NULL,
334
+ -- The nominal range of its light in metres, when OpenStreetMap gives one:
335
+ -- how far out this amer is still worth naming a position from.
336
+ light_range REAL,
337
+ created_at TEXT NOT NULL,
338
+ updated_at TEXT NOT NULL
339
+ );
340
+
341
+ CREATE UNIQUE INDEX idx_landmarks_osm ON landmarks (osm_type, osm_id);
342
+
343
+ -- Supports the bounding-box prefilter, as for places.
344
+ CREATE INDEX idx_landmarks_position ON landmarks (lat, lon);
345
+
346
+ -- The half-degree cells landmarks have already been fetched for. A fetch
347
+ -- covers its cell plus a margin, so every position inside the cell has all
348
+ -- the amers within range in the table -- one request per cell, ever.
349
+ CREATE TABLE landmark_areas (
350
+ cell_lat INTEGER NOT NULL,
351
+ cell_lon INTEGER NOT NULL,
352
+ fetched_at TEXT NOT NULL,
353
+ PRIMARY KEY (cell_lat, cell_lon)
354
+ );
355
+
356
+ -- Cleared once every cell a closed passage sailed through has been fetched.
357
+ -- An open passage stays pending: it keeps moving into new cells. Existing
358
+ -- passages default to pending, so their areas are fetched too.
359
+ ALTER TABLE log_entries ADD COLUMN landmarks_pending INTEGER NOT NULL DEFAULT 1
360
+ CHECK (landmarks_pending IN (0, 1));
317
361
  `
318
362
  ];
319
363
 
package/lib/detection.js CHANGED
@@ -314,11 +314,16 @@ function createPassageDetector({ db, readSelfPath, settings, clock = Date.now })
314
314
  }
315
315
 
316
316
  function openPassage(now, position, departure) {
317
+ const startIso = iso(departure.time);
318
+ // Only a passage that actually precedes this departure can push it later
319
+ // -- not just whichever entry happens to hold the latest end_time. A
320
+ // retrospective replay filling a gap before passages logged live (SPEC
321
+ // §4.10) would otherwise have every reconstructed departure clamped to
322
+ // the most recent (unrelated, later) passage's end.
317
323
  const previousEnd = db
318
- .prepare('SELECT MAX(end_time) AS endTime FROM log_entries')
319
- .get().endTime;
320
- const startTime =
321
- previousEnd && iso(departure.time) < previousEnd ? previousEnd : iso(departure.time);
324
+ .prepare('SELECT MAX(end_time) AS endTime FROM log_entries WHERE start_time < ?')
325
+ .get(startIso).endTime;
326
+ const startTime = previousEnd && startIso < previousEnd ? previousEnd : startIso;
322
327
  const start = departure.position ?? position;
323
328
  const place = initialPlaceName(db, start, settings.placeMatchRadius);
324
329
 
package/lib/entries.js CHANGED
@@ -216,13 +216,16 @@ function updateEntry(db, id, patch, { placeMatchRadius, now }) {
216
216
  next.distance = patch.distance;
217
217
  }
218
218
 
219
+ // landmarks_pending: an edited departure or arrival position may sit in an
220
+ // area no landmarks have been fetched for (SPEC §4.13); the lookup checks
221
+ // and clears it again.
219
222
  db.prepare(
220
223
  `UPDATE log_entries SET
221
224
  start_time = ?, end_time = ?,
222
225
  start_lat = ?, start_lon = ?, end_lat = ?, end_lon = ?,
223
226
  start_place_id = ?, end_place_id = ?, start_place_name = ?, end_place_name = ?,
224
227
  start_place_pending = ?, end_place_pending = ?,
225
- distance = ?, updated_at = ?
228
+ distance = ?, landmarks_pending = 1, updated_at = ?
226
229
  WHERE id = ?`
227
230
  ).run(
228
231
  next.start_time,
@@ -361,11 +364,13 @@ function mergeEntries(db, id, otherId, now) {
361
364
  // index would otherwise reject the update below.
362
365
  db.prepare('DELETE FROM log_entries WHERE id = ?').run(later.id);
363
366
 
367
+ // landmarks_pending: the merged passage now covers the later one's track
368
+ // as well, which may reach areas no landmarks have been fetched for.
364
369
  db.prepare(
365
370
  `UPDATE log_entries SET
366
371
  state = ?, end_time = ?, stopped_since = ?, closed_by = ?,
367
372
  end_lat = ?, end_lon = ?, end_place_id = ?, end_place_name = ?, end_place_pending = ?,
368
- distance = ?, updated_at = ?
373
+ distance = ?, landmarks_pending = 1, updated_at = ?
369
374
  WHERE id = ?`
370
375
  ).run(
371
376
  later.state,
@@ -1,14 +1,12 @@
1
1
  const { createFreshnessTracker } = require('./detection');
2
2
  const { activeEntryId, entryNearArrival } = require('./entries');
3
+ const { CRITICAL_STATES } = require('./events');
3
4
 
4
5
  const METRES_PER_SECOND_PER_KNOT = 1852 / 3600;
5
6
 
6
7
  const EVENT_DEFAULTS = { windSpeedThresholds: [20, 30], pressureDropThreshold: 4 };
7
8
 
8
9
  const CHECK_INTERVAL_MS = 1000;
9
-
10
- // Any notification reaching these states is critical, whatever its path.
11
- const CRITICAL_STATES = new Set(['alarm', 'emergency']);
12
10
  const NOTIFICATION_METADATA_KEYS = new Set([
13
11
  'meta',
14
12
  'values',