@signalk/freeboard-sk 3.4.0-beta.0 → 3.4.0-beta.2

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 (96) hide show
  1. package/package.json +21 -13
  2. package/plugin/alarms/alarms.js +46 -112
  3. package/plugin/alarms/area-schemas.js +57 -0
  4. package/plugin/index.js +20 -38
  5. package/plugin/lib/fetch.js +5 -11
  6. package/plugin/vitest.config.mjs +0 -1
  7. package/public/3rdpartylicenses.txt +256 -251
  8. package/public/assets/features/imported-tracks-1.jpg +0 -0
  9. package/public/assets/features/imported-tracks-2.jpg +0 -0
  10. package/public/assets/features/own-vessel-display-2.jpg +0 -0
  11. package/public/assets/features/own-vessel-display-3.jpg +0 -0
  12. package/public/assets/features/own-vessel-display-4.jpg +0 -0
  13. package/public/assets/features/split-view-1.jpg +0 -0
  14. package/public/assets/features/split-view-2.jpg +0 -0
  15. package/public/assets/features/toolbar-overflow-1.jpg +0 -0
  16. package/public/assets/features/toolbar-overflow-2.jpg +0 -0
  17. package/public/assets/features-corpus.json +1 -1
  18. package/public/assets/help/index.html +72 -62
  19. package/public/assets/img/vessels/ATTRIBUTION.md +9 -0
  20. package/public/assets/img/vessels/self_catamaran.svg +2 -0
  21. package/public/assets/img/vessels/self_monohull.svg +2 -0
  22. package/public/assets/img/vessels/self_powerboat.svg +11 -0
  23. package/public/chunk-26ZBWNUQ.js +1 -0
  24. package/public/{chunk-L4ONBCRR.js → chunk-2DNFYKJI.js} +1 -1
  25. package/public/{chunk-3HO5JDYF.js → chunk-2YSXXOPC.js} +1 -1
  26. package/public/chunk-3D457EZY.js +1 -0
  27. package/public/chunk-3E3U7Z7O.js +1 -0
  28. package/public/chunk-3PBISOLT.js +4 -0
  29. package/public/{chunk-LNU63734.js → chunk-6FLQJLM2.js} +1 -1
  30. package/public/chunk-6W3TVG4T.js +1 -0
  31. package/public/chunk-6XBJQDQZ.js +1 -0
  32. package/public/chunk-7FANVIZX.js +1 -0
  33. package/public/{chunk-AQKT2J2C.js → chunk-7PF3OZ2P.js} +2 -2
  34. package/public/{chunk-MYBGWSLC.js → chunk-A7RX2LBX.js} +7 -7
  35. package/public/chunk-B5DI22LT.js +1 -0
  36. package/public/chunk-BZGM3F3K.js +1 -0
  37. package/public/chunk-CI2BDMX2.js +1 -0
  38. package/public/chunk-CKZDFNLJ.js +1 -0
  39. package/public/{chunk-J6MIIPAV.js → chunk-CSVULD7M.js} +1 -1
  40. package/public/chunk-CZWRDBMY.js +1 -0
  41. package/public/{chunk-QWUBBFQO.js → chunk-EKGFBNKZ.js} +1 -1
  42. package/public/{chunk-X6D4D6EN.js → chunk-EX2EX65O.js} +3 -3
  43. package/public/chunk-FFPCEPCR.js +7 -0
  44. package/public/chunk-FX2SMI5S.js +1 -0
  45. package/public/chunk-GJFI75CP.js +1706 -0
  46. package/public/{chunk-3UKG3D3M.js → chunk-GKG3KTBW.js} +1 -1
  47. package/public/{chunk-45ZTFAA5.js → chunk-I3RHGD62.js} +1 -1
  48. package/public/{chunk-MX5HWKAH.js → chunk-I6KNDLKZ.js} +1 -1
  49. package/public/chunk-IZEHULJO.js +2 -0
  50. package/public/chunk-L766X4HH.js +1 -0
  51. package/public/{chunk-3BUYRM6E.js → chunk-M4EXFQJE.js} +5 -5
  52. package/public/chunk-N6FC7TMF.js +1 -0
  53. package/public/chunk-NUFT6SHL.js +1 -0
  54. package/public/{chunk-ISXARX6U.js → chunk-O2XN7DIT.js} +1 -1
  55. package/public/chunk-ON6WKWFV.js +106 -0
  56. package/public/chunk-P2M2PBKT.js +1 -0
  57. package/public/chunk-PKC2MOZE.js +1 -0
  58. package/public/{chunk-LFAWLMM4.js → chunk-PU65TI55.js} +1 -1
  59. package/public/chunk-RYULE37H.js +1 -0
  60. package/public/{chunk-DD47QU4L.js → chunk-TKZSDI3W.js} +1 -1
  61. package/public/{chunk-62WHV6CJ.js → chunk-UBRYY245.js} +7 -7
  62. package/public/chunk-UIEE3YOF.js +1 -0
  63. package/public/chunk-UXZI7A6H.js +2 -0
  64. package/public/{chunk-T7ZWMXKZ.js → chunk-V44P57GA.js} +1 -1
  65. package/public/chunk-VAE47BFB.js +1 -0
  66. package/public/chunk-VCZ35SJT.js +1 -0
  67. package/public/chunk-VG2HTHDN.js +4402 -0
  68. package/public/chunk-XPTNBYFH.js +1 -0
  69. package/public/{chunk-YZN4NRWW.js → chunk-Z2OUIJPL.js} +1 -1
  70. package/public/{chunk-GVRPUNRX.js → chunk-ZFWDWQNT.js} +1 -1
  71. package/public/chunk-ZHXEZBYI.js +1 -0
  72. package/public/index.html +2 -2
  73. package/public/main-YMAIP3FL.js +358 -0
  74. package/public/{styles-BRU77GKR.css → styles-ADI62PD5.css} +1 -1
  75. package/public/{worker-UJD2XPGR.js → worker-2QCQHBWQ.js} +1 -1
  76. package/public/{worker-W3UMLYIU.js → worker-6BUXF663.js} +6 -6
  77. package/public/worker-CDFJSLG6.js +3 -0
  78. package/public/{worker-E75PSRY4.js → worker-OJDHVISM.js} +1 -1
  79. package/public/assets/img/vessels/self_blur.png +0 -0
  80. package/public/chunk-4YPS6TRI.js +0 -1
  81. package/public/chunk-5GKIE35D.js +0 -1
  82. package/public/chunk-6E2BDKCH.js +0 -106
  83. package/public/chunk-6HBY4BXL.js +0 -1
  84. package/public/chunk-7NFD4L3V.js +0 -1
  85. package/public/chunk-BXGNCKBA.js +0 -1
  86. package/public/chunk-I33LU6L4.js +0 -1
  87. package/public/chunk-KD22K3HF.js +0 -4289
  88. package/public/chunk-NCEQPEJE.js +0 -1
  89. package/public/chunk-NFXFAYGP.js +0 -1
  90. package/public/chunk-QVENIT7C.js +0 -1
  91. package/public/chunk-RG7WQ7O2.js +0 -1709
  92. package/public/chunk-ROVQT3EG.js +0 -1
  93. package/public/chunk-VFDN43NP.js +0 -2
  94. package/public/chunk-YSHHABBQ.js +0 -1
  95. package/public/main-D2JKS7F4.js +0 -468
  96. package/public/worker-PCWM7FLK.js +0 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/freeboard-sk",
3
- "version": "3.4.0-beta.0",
3
+ "version": "3.4.0-beta.2",
4
4
  "description": "Openlayers chart plotter implementation for Signal K",
5
5
  "keywords": [
6
6
  "signalk-webapp",
@@ -42,11 +42,12 @@
42
42
  "node": ">=18"
43
43
  },
44
44
  "repository": "https://github.com/SignalK/freeboard-sk",
45
+ "type": "module",
45
46
  "main": "plugin/index.js",
46
47
  "signalk-plugin-enabled-by-default": true,
47
48
  "scripts": {
48
49
  "ng": "ng",
49
- "prestart": "node scripts/build-features.mjs",
50
+ "prestart": "node scripts/build-features.mts",
50
51
  "start": "ng serve",
51
52
  "build": "ng build",
52
53
  "test": "ng test",
@@ -56,17 +57,20 @@
56
57
  "format:helper": "prettier --ignore-path .gitignore --write \"helper/**/*.+(ts|html)\"",
57
58
  "format:all": "npm run format && npm run format:helper",
58
59
  "build:helper": "tsc -p tsconfig-helper.json",
59
- "build:features": "node scripts/build-features.mjs",
60
- "prebuild:web": "node scripts/build-features.mjs",
61
- "build:web": "node scripts/build-web.mjs",
60
+ "typecheck:tools": "tsc -p tsconfig.tools.json",
61
+ "typecheck:workers": "tsc -p tsconfig.worker.json --noEmit",
62
+ "typecheck:helper": "tsc -p tsconfig-helper.spec.json",
63
+ "build:features": "node scripts/build-features.mts",
64
+ "prebuild:web": "node scripts/build-features.mts",
65
+ "build:web": "node scripts/build-web.mts",
62
66
  "build:sit": "ng build -c development --aot",
63
- "build:all": "npm run build:helper && npm run build:web",
67
+ "build:all": "npm run typecheck:tools && npm run typecheck:workers && npm run typecheck:helper && npm run build:helper && npm run build:web",
64
68
  "build:prod": "npm run build:all",
65
69
  "prepack": "npm run build:all",
66
- "test:ci": "node scripts/test-ci.mjs",
67
- "test:tools": "vitest run --config dev-tools/changelog/vitest.config.mjs",
70
+ "test:ci": "node scripts/test-ci.mts",
71
+ "test:tools": "tsc -p dev-tools/changelog && vitest run --config dev-tools/changelog/vitest.config.mts",
68
72
  "test:helper": "vitest run --config helper/vitest.config.mts",
69
- "version": "node dev-tools/changelog/index.mjs stamp && git add features/changelog.json"
73
+ "version": "node dev-tools/changelog/index.mts stamp && git add features/changelog.json"
70
74
  },
71
75
  "author": "AdrianP",
72
76
  "contributors": [
@@ -80,12 +84,13 @@
80
84
  "license": "Apache-v2",
81
85
  "dependencies": {
82
86
  "@signalk/server-api": "^2.24.0",
83
- "geolib": "^3.3.3",
87
+ "geolib": "^4.0.0",
84
88
  "google-protobuf": "^4.0.1",
85
89
  "remark-gfm": "^4.0.1",
86
90
  "remark-parse": "^11.0.0",
87
- "signalk-plotterext-bus": "^0.21.0",
91
+ "signalk-plotterext-bus": "^0.24.0",
88
92
  "tslib": "^2.0.0",
93
+ "typebox": "^1.3.36",
89
94
  "unified": "^11.0.5",
90
95
  "uuid": "^14.0.1"
91
96
  },
@@ -109,16 +114,18 @@
109
114
  "@types/arcgis-rest-api": "^10.4.5",
110
115
  "@types/express": "^4.17.17",
111
116
  "@types/geojson": "^7946.0.10",
117
+ "@types/google-protobuf": "^3.15.12",
118
+ "@types/ngeohash": "^0.6.8",
112
119
  "@types/node": "^24.10.1",
120
+ "@types/semver": "^7.8.0",
113
121
  "@types/topojson-specification": "^1.0.2",
114
122
  "@types/xml2js": "^0.4.14",
115
- "@typescript-eslint/eslint-plugin": "^8.46.3",
116
- "@typescript-eslint/parser": "^8.46.3",
117
123
  "@vitest/web-worker": "^4.0.18",
118
124
  "baseline-browser-mapping": "^2.9.18",
119
125
  "buffer": "^6.0.3",
120
126
  "eslint": "^10.5.0",
121
127
  "events": "^3.3.0",
128
+ "jiti": "^2.7.0",
122
129
  "jsdom": "^29.1.1",
123
130
  "ng-packagr": "^21.0.0",
124
131
  "ngeohash": "^0.6.3",
@@ -139,6 +146,7 @@
139
146
  "timers": "^0.1.1",
140
147
  "ts-node": "~10.9.2",
141
148
  "typescript": "~5.9.3",
149
+ "typescript-eslint": "^8.71.2",
142
150
  "vitest": "^4.0.18",
143
151
  "xml2js": "^0.6.2",
144
152
  "zone.js": "~0.16.2"
@@ -1,12 +1,8 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.shutdownAlarms = exports.initAlarms = void 0;
4
- const tslib_1 = require("tslib");
5
- const server_api_1 = require("@signalk/server-api");
6
- const uuid = tslib_1.__importStar(require("uuid"));
7
- const geolib_1 = require("geolib");
8
- const AREA_TRIGGERS = ['entry', 'exit'];
9
- const AREA_GEOMETRIES = ['polygon', 'circle', 'region'];
1
+ import { ALARM_METHOD, ALARM_STATE, hasValues, SKVersion } from '@signalk/server-api';
2
+ import * as uuid from 'uuid';
3
+ import { isPointWithinRadius, isPointInPolygon } from 'geolib';
4
+ import { Value } from 'typebox/value';
5
+ import { parseAreaBody, PositionSchema } from './area-schemas.js';
10
6
  const ALARM_API_PATH = '/signalk/v2/api/alarms';
11
7
  class AreaAlarmManager {
12
8
  alarms;
@@ -32,17 +28,20 @@ class AreaAlarmManager {
32
28
  * @returns void
33
29
  */
34
30
  update(id, condition) {
35
- if (!alarmAreas.has(id)) {
31
+ const area = alarmAreas.get(id);
32
+ if (!area) {
36
33
  return;
37
34
  }
38
- if (!this.alarms.has(id)) {
39
- this.alarms.set(id, {
35
+ let alarm = this.alarms.get(id);
36
+ if (!alarm) {
37
+ alarm = {
40
38
  alarmId: id,
41
39
  active: false,
42
40
  lastUpdate: Date.now() - 1000
43
- });
41
+ };
42
+ this.alarms.set(id, alarm);
44
43
  }
45
- this.assessStatus(id, condition);
44
+ this.assessStatus(id, area, alarm, condition);
46
45
  }
47
46
  /**
48
47
  * Silence alarm with the supplied identifier
@@ -50,26 +49,26 @@ class AreaAlarmManager {
50
49
  */
51
50
  silence(id) {
52
51
  const n = getSelfPathValue(`notifications.area.${id}`);
53
- if (n?.value && Array.isArray(n.value.method)) {
52
+ if (!n?.value) {
53
+ return;
54
+ }
55
+ if (Array.isArray(n.value.method)) {
54
56
  const m = n.value.method.filter((i) => i !== 'sound');
55
57
  n.value.method = m;
56
58
  }
57
59
  emitNotification({
58
60
  path: `notifications.area.${id}`,
59
- value: n?.value
61
+ value: n.value
60
62
  });
61
63
  }
62
64
  /**
63
65
  * Assess and emit alarm based on supplied condition
64
66
  * @param id alarm id
67
+ * @param area monitored area
68
+ * @param alarm alarm status of the area
65
69
  * @param condition current condition
66
70
  */
67
- assessStatus(id, condition) {
68
- if (!alarmAreas.has(id)) {
69
- return;
70
- }
71
- const area = alarmAreas.get(id);
72
- const alarm = this.alarms.get(id);
71
+ assessStatus(id, area, alarm, condition) {
73
72
  let notify = false;
74
73
  if (area.trigger === 'entry') {
75
74
  if (condition === 'inside' && !alarm.active) {
@@ -108,12 +107,12 @@ class AreaAlarmManager {
108
107
  : alarm.active
109
108
  ? `Vessel has left the monitored area ${area.name ?? ''}`
110
109
  : '';
111
- const state = alarm.active ? server_api_1.ALARM_STATE.alarm : server_api_1.ALARM_STATE.normal;
110
+ const state = alarm.active ? ALARM_STATE.alarm : ALARM_STATE.normal;
112
111
  emitNotification({
113
112
  path: `notifications.area.${id}`,
114
113
  value: {
115
114
  message: msg,
116
- method: [server_api_1.ALARM_METHOD.sound, server_api_1.ALARM_METHOD.visual],
115
+ method: [ALARM_METHOD.sound, ALARM_METHOD.visual],
117
116
  state: state
118
117
  }
119
118
  });
@@ -138,7 +137,7 @@ let regionLoadActive = false;
138
137
  const getSelfPathValue = (path) => {
139
138
  return server.getSelfPath(path);
140
139
  };
141
- const initAlarms = (app, id) => {
140
+ export const initAlarms = (app, id) => {
142
141
  server = app;
143
142
  pluginId = id;
144
143
  server.debug(`** initAlarms() **`);
@@ -163,8 +162,7 @@ const initAlarms = (app, id) => {
163
162
  console.log(`error: ${err}`);
164
163
  }, handleDeltaMessage);
165
164
  };
166
- exports.initAlarms = initAlarms;
167
- const shutdownAlarms = () => {
165
+ export const shutdownAlarms = () => {
168
166
  regionLoadActive = false;
169
167
  if (regionLoadTimer) {
170
168
  clearTimeout(regionLoadTimer);
@@ -173,13 +171,12 @@ const shutdownAlarms = () => {
173
171
  unsubscribes.forEach((s) => s());
174
172
  unsubscribes = [];
175
173
  };
176
- exports.shutdownAlarms = shutdownAlarms;
177
174
  const handleDeltaMessage = (delta) => {
178
175
  if (!delta.updates) {
179
176
  return;
180
177
  }
181
178
  delta.updates.forEach((u) => {
182
- if (!(0, server_api_1.hasValues)(u)) {
179
+ if (!hasValues(u)) {
183
180
  return;
184
181
  }
185
182
  u.values.forEach((v) => {
@@ -207,8 +204,9 @@ const initAlarmEndpoints = () => {
207
204
  });
208
205
  // new area alarm
209
206
  server.post(`${ALARM_API_PATH}/area`, async (req, res) => {
207
+ let body;
210
208
  try {
211
- validateAreaBody(req.body);
209
+ body = parseAreaBody(req.body);
212
210
  }
213
211
  catch (err) {
214
212
  res.status(400).json({
@@ -218,7 +216,7 @@ const initAlarmEndpoints = () => {
218
216
  });
219
217
  return;
220
218
  }
221
- if (req.body.geometry === 'region') {
219
+ if (body.geometry === 'region') {
222
220
  res.status(400).json({
223
221
  state: 'FAILED',
224
222
  statusCode: 400,
@@ -227,7 +225,7 @@ const initAlarmEndpoints = () => {
227
225
  return;
228
226
  }
229
227
  const id = uuid.v4();
230
- alarmAreas.set(id, req.body);
228
+ alarmAreas.set(id, body);
231
229
  res.status(200).json({
232
230
  state: 'COMPLETE',
233
231
  statusCode: 200,
@@ -236,8 +234,9 @@ const initAlarmEndpoints = () => {
236
234
  });
237
235
  server.put(`${ALARM_API_PATH}/area/:id`, async (req, res) => {
238
236
  server.debug(`** ${req.method} ${req.path}`);
237
+ let body;
239
238
  try {
240
- validateAreaBody(req.body);
239
+ body = parseAreaBody(req.body);
241
240
  }
242
241
  catch (err) {
243
242
  res.status(400).json({
@@ -247,15 +246,15 @@ const initAlarmEndpoints = () => {
247
246
  });
248
247
  return;
249
248
  }
250
- if (req.body.geometry === 'region') {
249
+ if (body.geometry === 'region') {
251
250
  // use region resource as alarm area
252
251
  try {
253
252
  const reg = await fetchRegion(req.params.id);
254
253
  const coords = parseRegionCoords(reg);
255
254
  if (Array.isArray(coords)) {
256
255
  alarmAreas.set(req.params.id, {
257
- geometry: req.body.geometry,
258
- trigger: req.body.trigger,
256
+ geometry: body.geometry,
257
+ trigger: body.trigger,
259
258
  coords: coords,
260
259
  name: reg.name
261
260
  });
@@ -287,7 +286,7 @@ const initAlarmEndpoints = () => {
287
286
  const msg = alarmAreas.has(req.params.id)
288
287
  ? `Alarm Area updated: ${req.params.id}`
289
288
  : `Alarm Area created: ${req.params.id}`;
290
- alarmAreas.set(req.params.id, req.body);
289
+ alarmAreas.set(req.params.id, body);
291
290
  res.status(200).json({
292
291
  state: 'COMPLETE',
293
292
  statusCode: 200,
@@ -355,7 +354,7 @@ const emitNotification = (msg) => {
355
354
  const delta = {
356
355
  updates: [{ values: [msg] }]
357
356
  };
358
- server.handleMessage(pluginId, delta, server_api_1.SKVersion.v2);
357
+ server.handleMessage(pluginId, delta, SKVersion.v2);
359
358
  };
360
359
  // ********** Area Alarm methods ***************
361
360
  /**
@@ -366,70 +365,6 @@ const deleteArea = (id) => {
366
365
  alarmAreas.delete(id);
367
366
  alarmManager.delete(id);
368
367
  };
369
- /**
370
- * Validate Area Alarm request parameters
371
- * @param body request body (untrusted; defaults are applied and stray
372
- * fields removed in place)
373
- */
374
- const validateAreaBody = (body) => {
375
- if (!body.trigger) {
376
- body.trigger = 'entry';
377
- }
378
- else if (!AREA_TRIGGERS.includes(body.trigger)) {
379
- throw new Error(`Area alarm trigger is invalid!`);
380
- }
381
- if (!body.geometry) {
382
- body.geometry = 'polygon';
383
- }
384
- else if (!AREA_GEOMETRIES.includes(body.geometry)) {
385
- throw new Error(`Area alarm geometry is invalid!`);
386
- }
387
- if (body.geometry === 'polygon') {
388
- if (!Array.isArray(body.coords)) {
389
- throw new Error(`Area coordinates not provided or are invalid!`);
390
- }
391
- if (body.coords.length === 0) {
392
- throw new Error(`Area coordinates not provided!`);
393
- }
394
- else if (!isValidPosition(body.coords[0])) {
395
- throw new Error(`Area coordinates are invalid!`);
396
- }
397
- delete body.center;
398
- delete body.radius;
399
- }
400
- if (body.geometry === 'circle') {
401
- if (!body.center || !isValidPosition(body.center)) {
402
- throw new Error(`Center coordinate not provided or is invalid!`);
403
- }
404
- if (typeof body.radius !== 'number') {
405
- throw new Error(`Radius not provided or invalid!`);
406
- }
407
- delete body.coords;
408
- }
409
- if (body.geometry === 'region') {
410
- delete body.coords;
411
- delete body.center;
412
- delete body.radius;
413
- }
414
- };
415
- /**
416
- * Determines if supplied position is valid
417
- * @param position
418
- * @returns true if valid
419
- */
420
- const isValidPosition = (position) => {
421
- return position &&
422
- 'latitude' in position &&
423
- 'longitude' in position &&
424
- typeof position.latitude === 'number' &&
425
- position.latitude >= -90 &&
426
- position.latitude <= 90 &&
427
- typeof position.longitude === 'number' &&
428
- position.longitude >= -180 &&
429
- position.longitude <= 180
430
- ? true
431
- : false;
432
- };
433
368
  /**
434
369
  * Fetch region resource details
435
370
  * @param id Region identifier
@@ -466,7 +401,7 @@ const parseRegionList = async (attempt) => {
466
401
  // its config): recoverable, there are simply no region alarm areas to load.
467
402
  let regList;
468
403
  try {
469
- regList = await server.resourcesApi.listResources('regions', undefined);
404
+ regList = await server.resourcesApi.listResources('regions', {});
470
405
  }
471
406
  catch (err) {
472
407
  if (!regionLoadActive) {
@@ -513,18 +448,17 @@ const isHazardRegion = (region) => region.feature.properties?.skIcon ===
513
448
  * @param region Region data
514
449
  */
515
450
  const processRegionUpdate = (id, region) => {
516
- if (alarmAreas.has(id)) {
451
+ const area = alarmAreas.get(id);
452
+ if (area) {
517
453
  if (!region) {
518
454
  deleteArea(id);
519
455
  }
520
456
  else if (!isHazardRegion(region)) {
521
457
  deleteArea(id);
522
458
  }
523
- else {
524
- const r = alarmAreas.get(id);
525
- r.coords = parseRegionCoords(region);
526
- r.name = region.name;
527
- alarmAreas.set(id, r);
459
+ else if (area.geometry !== 'circle') {
460
+ area.coords = parseRegionCoords(region);
461
+ area.name = region.name;
528
462
  }
529
463
  }
530
464
  else if (region) {
@@ -545,13 +479,13 @@ const processRegionUpdate = (id, region) => {
545
479
  * @param position Vessel position
546
480
  */
547
481
  const processVesselPositionUpdate = (position) => {
548
- if (!isValidPosition(position)) {
482
+ if (!Value.Check(PositionSchema, position)) {
549
483
  return;
550
484
  }
551
485
  alarmAreas.forEach((v, k) => {
552
486
  let condition;
553
487
  if (v.geometry === 'circle') {
554
- if ((0, geolib_1.isPointWithinRadius)(position, v.center, v.radius)) {
488
+ if (isPointWithinRadius(position, v.center, v.radius)) {
555
489
  condition = 'inside';
556
490
  server.debug(`Vessel inside alarm radius ${k}`);
557
491
  }
@@ -561,7 +495,7 @@ const processVesselPositionUpdate = (position) => {
561
495
  }
562
496
  }
563
497
  else {
564
- if ((0, geolib_1.isPointInPolygon)(position, v.coords)) {
498
+ if (isPointInPolygon(position, v.coords)) {
565
499
  condition = 'inside';
566
500
  server.debug(`Vessel inside alarm area ${k}`);
567
501
  }
@@ -0,0 +1,57 @@
1
+ import { Type } from 'typebox';
2
+ import { Value } from 'typebox/value';
3
+ export const PositionSchema = Type.Object({
4
+ latitude: Type.Number({ minimum: -90, maximum: 90 }),
5
+ longitude: Type.Number({ minimum: -180, maximum: 180 })
6
+ });
7
+ const AreaTriggerSchema = Type.Enum(['entry', 'exit'], { default: 'entry' });
8
+ // Picks the body schema below; geometry and trigger may be left out.
9
+ const AreaHeaderSchema = Type.Object({
10
+ geometry: Type.Enum(['polygon', 'circle', 'region'], { default: 'polygon' }),
11
+ trigger: AreaTriggerSchema
12
+ });
13
+ const CircleAreaBodySchema = Type.Object({
14
+ geometry: Type.Literal('circle'),
15
+ trigger: AreaTriggerSchema,
16
+ center: PositionSchema,
17
+ // An entry alarm can never fire inside a circle of zero or negative radius.
18
+ radius: Type.Number({ exclusiveMinimum: 0 }),
19
+ name: Type.Optional(Type.String())
20
+ });
21
+ const PolygonAreaBodySchema = Type.Object({
22
+ geometry: Type.Literal('polygon'),
23
+ trigger: AreaTriggerSchema,
24
+ // Fewer than three points enclose no area, so an entry alarm never fires.
25
+ coords: Type.Array(PositionSchema, { minItems: 3 }),
26
+ name: Type.Optional(Type.String())
27
+ });
28
+ // The area itself comes from the region resource named in the request path.
29
+ const RegionAreaBodySchema = Type.Object({
30
+ geometry: Type.Literal('region'),
31
+ trigger: AreaTriggerSchema,
32
+ name: Type.Optional(Type.String())
33
+ });
34
+ const AREA_BODY_SCHEMAS = {
35
+ circle: CircleAreaBodySchema,
36
+ polygon: PolygonAreaBodySchema,
37
+ region: RegionAreaBodySchema
38
+ };
39
+ function assertValid(schema, value) {
40
+ if (!Value.Check(schema, value)) {
41
+ const [error] = Value.Errors(schema, value);
42
+ throw new Error(`Area alarm is invalid: ${error.instancePath || 'body'} ${error.message}`);
43
+ }
44
+ }
45
+ /**
46
+ * Validate an area alarm request body
47
+ * @param body request body (untrusted)
48
+ * @returns the body with defaults applied and unknown fields removed
49
+ * @throws Error naming the first invalid field
50
+ */
51
+ export const parseAreaBody = (body) => {
52
+ const value = Value.Default(AreaHeaderSchema, Value.Clone(body));
53
+ assertValid(AreaHeaderSchema, value);
54
+ const schema = AREA_BODY_SCHEMAS[value.geometry];
55
+ assertValid(schema, value);
56
+ return Value.Clean(schema, value);
57
+ };
package/plugin/index.js CHANGED
@@ -1,30 +1,24 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
- const server_api_1 = require("@signalk/server-api");
5
- const alarms_1 = require("./alarms/alarms");
6
- const openapi = tslib_1.__importStar(require("./openApi.json"));
1
+ import { SKVersion } from '@signalk/server-api';
2
+ import { createRequire } from 'node:module';
3
+ import { Type } from 'typebox';
4
+ import { initAlarms, shutdownAlarms } from './alarms/alarms.js';
5
+ // Import attributes for JSON need Node 18.20 / 20.10; require works on every
6
+ // Node the engines range allows.
7
+ const openapi = createRequire(import.meta.url)('./openApi.json');
7
8
  //const defaultPollInterval = 60;
8
- const CONFIG_SCHEMA = {
9
- properties: {}
10
- };
9
+ // Nothing to configure: area alarms are always on.
10
+ const CONFIG_SCHEMA = Type.Object({});
11
11
  const CONFIG_UISCHEMA = {};
12
- module.exports = (server) => {
13
- // ** default configuration settings
14
- let settings = {
15
- alarms: {
16
- enable: true
17
- }
18
- };
12
+ const SETTINGS = { alarms: { enable: true } };
13
+ export default (server) => {
19
14
  // ******** REQUIRED PLUGIN DEFINITION *******
20
15
  const plugin = {
21
16
  id: 'freeboard-sk',
22
17
  name: 'Freeboard-SK',
23
18
  schema: () => CONFIG_SCHEMA,
24
19
  uiSchema: () => CONFIG_UISCHEMA,
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- start: (settings) => {
27
- doStartup(settings);
20
+ start: () => {
21
+ doStartup();
28
22
  },
29
23
  stop: () => {
30
24
  doShutdown();
@@ -35,41 +29,29 @@ module.exports = (server) => {
35
29
  getOpenApi: () => openapi
36
30
  };
37
31
  // ************************************
38
- const doStartup = (options) => {
32
+ const doStartup = () => {
39
33
  try {
40
34
  server.debug(`${plugin.name} starting.......`);
41
- if (typeof options !== 'undefined') {
42
- settings = options;
43
- }
44
35
  /**
45
36
  * emit metas for environment paths
46
37
  * @todo remove after merge of https://github.com/SignalK/specification/pull/662
47
38
  */
48
39
  emitMeteoMetas();
49
- settings.alarms = options.alarms ?? {
50
- enable: true
51
- };
52
- // const f = await server.getFeatures();
53
- settings.alarms.enable = true; //!f.apis.includes('notifications' as any);
54
- server.debug(`Applied config: ${JSON.stringify(settings)}`);
55
- if (settings.alarms.enable) {
56
- (0, alarms_1.initAlarms)(server, plugin.id);
57
- }
40
+ server.debug(`Applied config: ${JSON.stringify(SETTINGS)}`);
41
+ initAlarms(server, plugin.id);
58
42
  server.setPluginStatus('Started');
59
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
43
  }
61
44
  catch (error) {
62
45
  const msg = 'Started with errors!';
63
46
  server.setPluginError(msg);
64
47
  server.error('** EXCEPTION: **');
65
- server.error(error.stack);
66
- return error;
48
+ server.error(error instanceof Error ? (error.stack ?? error.message) : String(error));
67
49
  }
68
50
  };
69
51
  const doShutdown = () => {
70
52
  server.debug('** shutting down **');
71
53
  server.debug('** Un-subscribing from events **');
72
- (0, alarms_1.shutdownAlarms)();
54
+ shutdownAlarms();
73
55
  const msg = 'Stopped';
74
56
  server.setPluginStatus(msg);
75
57
  };
@@ -77,7 +59,7 @@ module.exports = (server) => {
77
59
  server.debug(`Initialising Freeboard-SK plugin endpoints.......`);
78
60
  router.get('/settings', (req, res) => {
79
61
  res.status(200).json({
80
- settings: settings
62
+ settings: SETTINGS
81
63
  });
82
64
  });
83
65
  };
@@ -334,7 +316,7 @@ module.exports = (server) => {
334
316
  meta: metas
335
317
  }
336
318
  ]
337
- }, server_api_1.SKVersion.v1);
319
+ }, SKVersion.v1);
338
320
  };
339
321
  return plugin;
340
322
  };
@@ -1,13 +1,9 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.post = exports.fetch = void 0;
4
- const tslib_1 = require("tslib");
5
- const https = tslib_1.__importStar(require("https"));
6
- const url = tslib_1.__importStar(require("url"));
7
- const http = tslib_1.__importStar(require("http"));
1
+ import * as https from 'https';
2
+ import * as url from 'url';
3
+ import * as http from 'http';
8
4
  // HTTP GET
9
5
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- const fetch = (href) => {
6
+ export const fetch = (href) => {
11
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
8
  const opt = url.parse(href);
13
9
  opt.headers = { 'User-Agent': 'Mozilla/5.0' };
@@ -35,9 +31,8 @@ const fetch = (href) => {
35
31
  });
36
32
  });
37
33
  };
38
- exports.fetch = fetch;
39
34
  // HTTP POST
40
- const post = (href, data) => {
35
+ export const post = (href, data) => {
41
36
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
37
  const opt = url.parse(href);
43
38
  opt.method = 'POST';
@@ -80,4 +75,3 @@ const post = (href, data) => {
80
75
  postReq.end();
81
76
  });
82
77
  };
83
- exports.post = post;
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  import { defineConfig } from 'vitest/config';
3
2
  // Standalone config for the helper plugin (the server-side code compiled by
4
3
  // `build:helper` into plugin/). It is plain Node, not part of the Angular app,