@signalk/freeboard-sk 3.4.0-beta.1 → 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.
- package/package.json +21 -13
- package/plugin/alarms/alarms.js +46 -112
- package/plugin/alarms/area-schemas.js +57 -0
- package/plugin/index.js +20 -38
- package/plugin/lib/fetch.js +5 -11
- package/plugin/vitest.config.mjs +0 -1
- package/public/3rdpartylicenses.txt +256 -251
- package/public/assets/features/imported-tracks-1.jpg +0 -0
- package/public/assets/features/imported-tracks-2.jpg +0 -0
- package/public/assets/features/own-vessel-display-2.jpg +0 -0
- package/public/assets/features/own-vessel-display-3.jpg +0 -0
- package/public/assets/features/own-vessel-display-4.jpg +0 -0
- package/public/assets/features-corpus.json +1 -1
- package/public/assets/help/index.html +49 -61
- package/public/assets/img/vessels/ATTRIBUTION.md +9 -0
- package/public/assets/img/vessels/self_catamaran.svg +2 -0
- package/public/assets/img/vessels/self_monohull.svg +2 -0
- package/public/assets/img/vessels/self_powerboat.svg +11 -0
- package/public/chunk-26ZBWNUQ.js +1 -0
- package/public/{chunk-L4ONBCRR.js → chunk-2DNFYKJI.js} +1 -1
- package/public/{chunk-3HO5JDYF.js → chunk-2YSXXOPC.js} +1 -1
- package/public/chunk-3D457EZY.js +1 -0
- package/public/chunk-3E3U7Z7O.js +1 -0
- package/public/chunk-3PBISOLT.js +4 -0
- package/public/{chunk-LNU63734.js → chunk-6FLQJLM2.js} +1 -1
- package/public/chunk-6W3TVG4T.js +1 -0
- package/public/chunk-6XBJQDQZ.js +1 -0
- package/public/chunk-7FANVIZX.js +1 -0
- package/public/{chunk-AQKT2J2C.js → chunk-7PF3OZ2P.js} +2 -2
- package/public/{chunk-CXU2QR7B.js → chunk-A7RX2LBX.js} +7 -7
- package/public/chunk-B5DI22LT.js +1 -0
- package/public/chunk-BZGM3F3K.js +1 -0
- package/public/chunk-CI2BDMX2.js +1 -0
- package/public/chunk-CKZDFNLJ.js +1 -0
- package/public/{chunk-J6MIIPAV.js → chunk-CSVULD7M.js} +1 -1
- package/public/chunk-CZWRDBMY.js +1 -0
- package/public/{chunk-QWUBBFQO.js → chunk-EKGFBNKZ.js} +1 -1
- package/public/{chunk-M4D7Z434.js → chunk-EX2EX65O.js} +3 -3
- package/public/chunk-FFPCEPCR.js +7 -0
- package/public/chunk-FX2SMI5S.js +1 -0
- package/public/chunk-GJFI75CP.js +1706 -0
- package/public/{chunk-3UKG3D3M.js → chunk-GKG3KTBW.js} +1 -1
- package/public/{chunk-OMAUXPDZ.js → chunk-I3RHGD62.js} +1 -1
- package/public/{chunk-RABBT5ST.js → chunk-I6KNDLKZ.js} +1 -1
- package/public/chunk-IZEHULJO.js +2 -0
- package/public/chunk-L766X4HH.js +1 -0
- package/public/{chunk-5WUOKMEL.js → chunk-M4EXFQJE.js} +5 -5
- package/public/chunk-N6FC7TMF.js +1 -0
- package/public/chunk-NUFT6SHL.js +1 -0
- package/public/{chunk-U7APAW37.js → chunk-O2XN7DIT.js} +1 -1
- package/public/chunk-ON6WKWFV.js +106 -0
- package/public/chunk-P2M2PBKT.js +1 -0
- package/public/chunk-PKC2MOZE.js +1 -0
- package/public/{chunk-LFAWLMM4.js → chunk-PU65TI55.js} +1 -1
- package/public/chunk-RYULE37H.js +1 -0
- package/public/{chunk-DD47QU4L.js → chunk-TKZSDI3W.js} +1 -1
- package/public/{chunk-DXAIA7TN.js → chunk-UBRYY245.js} +6 -6
- package/public/chunk-UIEE3YOF.js +1 -0
- package/public/chunk-UXZI7A6H.js +2 -0
- package/public/{chunk-XRUZGBPD.js → chunk-V44P57GA.js} +1 -1
- package/public/chunk-VAE47BFB.js +1 -0
- package/public/chunk-VCZ35SJT.js +1 -0
- package/public/chunk-VG2HTHDN.js +4402 -0
- package/public/chunk-XPTNBYFH.js +1 -0
- package/public/{chunk-XSBLUIEK.js → chunk-Z2OUIJPL.js} +1 -1
- package/public/{chunk-GVRPUNRX.js → chunk-ZFWDWQNT.js} +1 -1
- package/public/chunk-ZHXEZBYI.js +1 -0
- package/public/index.html +1 -1
- package/public/main-YMAIP3FL.js +358 -0
- package/public/{worker-UJD2XPGR.js → worker-2QCQHBWQ.js} +1 -1
- package/public/{worker-W3UMLYIU.js → worker-6BUXF663.js} +6 -6
- package/public/worker-CDFJSLG6.js +3 -0
- package/public/{worker-E75PSRY4.js → worker-OJDHVISM.js} +1 -1
- package/public/assets/img/vessels/self_blur.png +0 -0
- package/public/chunk-5B3P22GG.js +0 -1
- package/public/chunk-D7NY4CGQ.js +0 -1
- package/public/chunk-FESHPCKG.js +0 -106
- package/public/chunk-LZQNRNTA.js +0 -1
- package/public/chunk-M3QKKNN7.js +0 -1
- package/public/chunk-NCEQPEJE.js +0 -1
- package/public/chunk-NL5F2VEH.js +0 -4399
- package/public/chunk-SU5TGPFJ.js +0 -1
- package/public/chunk-W2UT4HJG.js +0 -2
- package/public/chunk-XHD652B2.js +0 -1
- package/public/chunk-XHH2BWBG.js +0 -1709
- package/public/chunk-XIEUBZE5.js +0 -1
- package/public/chunk-YP3ILUPZ.js +0 -1
- package/public/chunk-YXEZPMKV.js +0 -1
- package/public/chunk-ZJLAX3N2.js +0 -1
- package/public/main-C7W4SDMX.js +0 -358
- package/public/worker-KSHRWMJ6.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.
|
|
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.
|
|
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
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
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.
|
|
67
|
-
"test:tools": "vitest run --config dev-tools/changelog/vitest.config.
|
|
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.
|
|
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": "^
|
|
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.
|
|
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"
|
package/plugin/alarms/alarms.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
31
|
+
const area = alarmAreas.get(id);
|
|
32
|
+
if (!area) {
|
|
36
33
|
return;
|
|
37
34
|
}
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
|
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 ?
|
|
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: [
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
258
|
-
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,
|
|
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,
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
9
|
+
// Nothing to configure: area alarms are always on.
|
|
10
|
+
const CONFIG_SCHEMA = Type.Object({});
|
|
11
11
|
const CONFIG_UISCHEMA = {};
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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 = (
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
},
|
|
319
|
+
}, SKVersion.v1);
|
|
338
320
|
};
|
|
339
321
|
return plugin;
|
|
340
322
|
};
|
package/plugin/lib/fetch.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
package/plugin/vitest.config.mjs
CHANGED