speedandcurrent 1.4.2 → 1.5.1
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/LICENSE +1 -1
- package/README.md +179 -78
- package/correctionTable.js +149 -6
- package/index.js +363 -430
- package/leakyExtremes.js +98 -0
- package/package.json +12 -7
- package/public/TableRenderer.js +135 -83
- package/public/app.js +214 -55
- package/public/index.html +20 -5
- package/public/main.css +2 -2
- package/CHANGELOG.md +0 -28
- package/correctionModel.png +0 -0
- package/currentModel.png +0 -0
- package/models.md +0 -26
- package/public/surfaceplot.html +0 -78
package/index.js
CHANGED
|
@@ -1,55 +1,87 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const {
|
|
2
|
+
SmoothedHeading,
|
|
3
|
+
SmoothedAttitude,
|
|
4
|
+
SmoothedSpeedThroughWater,
|
|
5
|
+
SmoothedGroundSpeed,
|
|
6
|
+
SI,
|
|
7
|
+
MessageHandler,
|
|
8
|
+
Polar,
|
|
9
|
+
Reporter,
|
|
10
|
+
BaseSmoother,
|
|
11
|
+
MovingAverageSmoother,
|
|
12
|
+
KalmanSmoother,
|
|
13
|
+
PolarSmoother,
|
|
14
|
+
createSmoothedPolar,
|
|
15
|
+
createSmoothedHandler
|
|
16
|
+
} = require('signalkutilities');
|
|
6
17
|
|
|
18
|
+
const { CorrectionTable } = require('./correctionTable.js');
|
|
7
19
|
|
|
8
20
|
module.exports = function (app) {
|
|
9
21
|
|
|
10
|
-
let options = {};
|
|
11
22
|
let isRunning = false;
|
|
12
|
-
let
|
|
13
|
-
let
|
|
14
|
-
let
|
|
15
|
-
let
|
|
16
|
-
let
|
|
17
|
-
let
|
|
18
|
-
let boatSpeed = null;
|
|
19
|
-
let boatSpeedStat = null;
|
|
23
|
+
let smoothedHeading = null;
|
|
24
|
+
let stability = null;
|
|
25
|
+
let smoothedAttitude = null;
|
|
26
|
+
let rawCurrent = null;
|
|
27
|
+
let smoothedCurrent = null;
|
|
28
|
+
let smoothedBoatSpeed = null;
|
|
20
29
|
let correctedBoatSpeed = null;
|
|
21
30
|
let boatSpeedRefGround = null;
|
|
22
|
-
let
|
|
23
|
-
let groundSpeedStat = null;
|
|
31
|
+
let smoothedGroundSpeed = null;
|
|
24
32
|
let speedCorrection = null;
|
|
25
33
|
let residual = null;
|
|
26
34
|
let reportFull = null;
|
|
27
35
|
let reportVector = null;
|
|
28
36
|
let table = null;
|
|
29
37
|
|
|
38
|
+
// add missing declarations
|
|
39
|
+
let rawHeading = null;
|
|
40
|
+
let rawAttitude = null;
|
|
41
|
+
let noCurrent = null;
|
|
42
|
+
let rawBoatSpeed = null;
|
|
43
|
+
let rawGroundSpeed = null;
|
|
44
|
+
let started = null;
|
|
45
|
+
|
|
30
46
|
const plugin = {};
|
|
31
47
|
plugin.id = "SpeedAndCurrent";
|
|
32
48
|
plugin.name = "Speed and current";
|
|
33
|
-
plugin.description = "
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
plugin.description = "A plugin that uses sensor fusion to get boat speed, current and leeway.";
|
|
36
50
|
|
|
37
51
|
plugin.schema = {
|
|
38
52
|
type: "object",
|
|
39
53
|
properties: {
|
|
40
|
-
|
|
41
|
-
type: "
|
|
42
|
-
title: "
|
|
43
|
-
description: "
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
startWithNewTable: {
|
|
55
|
+
type: "boolean",
|
|
56
|
+
title: "Start with new table",
|
|
57
|
+
description: "Start with a new correction table (overrides existing table).",
|
|
58
|
+
default: false
|
|
59
|
+
},
|
|
60
|
+
estimateBoatSpeed: {
|
|
61
|
+
type: "boolean",
|
|
62
|
+
title: "Estimate Boat Speed",
|
|
63
|
+
description: "Enable estimation of boat speed.",
|
|
64
|
+
default: false
|
|
65
|
+
},
|
|
66
|
+
updateCorrectionTable: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
title: "Update Correction Table",
|
|
69
|
+
description: "Enable updating of the correction table.",
|
|
70
|
+
default: true
|
|
71
|
+
},
|
|
72
|
+
stability: {
|
|
73
|
+
type: "number",
|
|
74
|
+
title: "Correction Table Stability",
|
|
75
|
+
description: "Stability of the corrections (higher means that corrections are changed at a slower rate).",
|
|
76
|
+
default: 7,
|
|
77
|
+
minimum: 1,
|
|
78
|
+
maximum: 20
|
|
79
|
+
},
|
|
80
|
+
assumeCurrent: {
|
|
81
|
+
type: "boolean",
|
|
82
|
+
title: "Assume Current",
|
|
83
|
+
description: "Assume there is a current present when updating the correction table.",
|
|
84
|
+
default: false
|
|
53
85
|
},
|
|
54
86
|
heelStep: {
|
|
55
87
|
type: "number",
|
|
@@ -75,144 +107,98 @@ module.exports = function (app) {
|
|
|
75
107
|
description: "Correction table maximum speed.",
|
|
76
108
|
default: 9
|
|
77
109
|
},
|
|
78
|
-
|
|
79
110
|
headingSource: {
|
|
80
111
|
type: "string",
|
|
81
112
|
title: "Heading Source",
|
|
82
113
|
description: "Source to use for navigation.heading (optional).",
|
|
83
114
|
default: " "
|
|
84
115
|
},
|
|
85
|
-
|
|
86
116
|
boatSpeedSource: {
|
|
87
117
|
type: "string",
|
|
88
118
|
title: "Boat Speed Source",
|
|
89
119
|
description: "Source to use for navigation.speedThroughWater (optional).",
|
|
90
120
|
default: " "
|
|
91
121
|
},
|
|
92
|
-
|
|
93
122
|
COGSource: {
|
|
94
123
|
type: "string",
|
|
95
124
|
title: "Course over ground",
|
|
96
125
|
description: "Source to use for navigation.courseOverGroundTrue (optional)",
|
|
97
126
|
default: " "
|
|
98
127
|
},
|
|
99
|
-
|
|
100
128
|
SOGSource: {
|
|
101
129
|
type: "string",
|
|
102
130
|
title: "Ground Speed Source",
|
|
103
131
|
description: "Source to use for navigation.speedOverGround (optional)",
|
|
104
132
|
default: " "
|
|
105
133
|
},
|
|
106
|
-
|
|
107
134
|
attitudeSource: {
|
|
108
135
|
type: "string",
|
|
109
136
|
title: "Attitude Source",
|
|
110
137
|
description: "Source to use for navigation.attitude (optional)",
|
|
111
138
|
default: " "
|
|
112
139
|
},
|
|
113
|
-
|
|
114
140
|
preventDuplication: {
|
|
115
141
|
type: "boolean",
|
|
116
142
|
title: "Prevent duplication of boat speed",
|
|
117
143
|
description: "Overwrite boat speed from sensor with corrected boat speed.",
|
|
118
|
-
default: true
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
updateCorrectionTable: {
|
|
122
|
-
type: "boolean",
|
|
123
|
-
title: "Update correction table",
|
|
124
|
-
description: "Update estimations in the correction table.",
|
|
125
|
-
default: true,
|
|
126
|
-
},
|
|
127
|
-
estimateBoatSpeed: {
|
|
128
|
-
type: "boolean",
|
|
129
|
-
title: "Correct boat speed",
|
|
130
|
-
description: "Correct boat speed using correction table. This includes adding leeway.",
|
|
131
|
-
default: true,
|
|
132
|
-
},
|
|
133
|
-
correctionStability: {
|
|
134
|
-
type: "number",
|
|
135
|
-
title: "Stability of correction estimation",
|
|
136
|
-
description: `The stability of the correction estimation indicates how much the estimation is trusted
|
|
137
|
-
opposed to the calculation of . Biggeboat speed. Bigger values give more trust to the estimation and more stable estimations`,
|
|
138
|
-
default: 7,
|
|
139
|
-
minimum: 5,
|
|
140
|
-
maximum: 12
|
|
141
|
-
},
|
|
142
|
-
doStartFresh: {
|
|
143
|
-
type: "boolean",
|
|
144
|
-
title: "Empty correction table",
|
|
145
|
-
description: "Restart with a fresh correction table. WARNING: all current corrections will be lost permanently!",
|
|
146
|
-
default: false
|
|
147
|
-
},
|
|
148
|
-
assumeCurrent: {
|
|
149
|
-
type: "boolean",
|
|
150
|
-
title: "assume current",
|
|
151
|
-
description: "The estimator assumes that there is current.",
|
|
152
|
-
default: false,
|
|
153
|
-
},
|
|
154
|
-
estimateCurrent: {
|
|
155
|
-
type: "boolean",
|
|
156
|
-
title: "Estimate current",
|
|
157
|
-
description: "Estimate water current.",
|
|
158
|
-
default: true,
|
|
159
|
-
},
|
|
160
|
-
currentStability: {
|
|
161
|
-
type: "number",
|
|
162
|
-
title: "Stability of current estimation ",
|
|
163
|
-
description: `The stability of the current estimation indicates how much the estimation is trusted
|
|
164
|
-
opposed to the calculation of current. Bigger values give more trust to the estimation and more sable estimations`,
|
|
165
|
-
default: 5,
|
|
166
|
-
minimum: 1,
|
|
167
|
-
maximum: 8
|
|
144
|
+
default: true
|
|
168
145
|
}
|
|
169
146
|
}
|
|
170
147
|
};
|
|
171
148
|
|
|
172
149
|
plugin.uiSchema = {
|
|
173
|
-
'ui:order': [
|
|
174
|
-
|
|
175
|
-
"
|
|
150
|
+
'ui:order': [
|
|
151
|
+
"estimateBoatSpeed",
|
|
152
|
+
"updateCorrectionTable",
|
|
153
|
+
"assumeCurrent",
|
|
154
|
+
"startWithNewTable",
|
|
155
|
+
"stability",
|
|
156
|
+
"maxSpeed",
|
|
157
|
+
"speedStep",
|
|
158
|
+
"maxHeel",
|
|
159
|
+
"heelStep",
|
|
160
|
+
"headingSource",
|
|
161
|
+
"boatSpeedSource",
|
|
162
|
+
"COGSource",
|
|
163
|
+
"SOGSource",
|
|
164
|
+
"attitudeSource",
|
|
165
|
+
"preventDuplication"
|
|
166
|
+
],
|
|
167
|
+
startWithNewTable: {
|
|
168
|
+
"ui:widget": "checkbox"
|
|
169
|
+
},
|
|
170
|
+
estimateBoatSpeed: {
|
|
171
|
+
"ui:widget": "checkbox"
|
|
172
|
+
},
|
|
173
|
+
updateCorrectionTable: {
|
|
174
|
+
"ui:widget": "checkbox"
|
|
175
|
+
},
|
|
176
|
+
stability: {
|
|
177
|
+
"ui:widget": "updown"
|
|
178
|
+
},
|
|
179
|
+
assumeCurrent: {
|
|
180
|
+
"ui:widget": "checkbox"
|
|
176
181
|
},
|
|
177
182
|
heelStep: {
|
|
178
|
-
"ui:widget": "updown"
|
|
183
|
+
"ui:widget": "updown"
|
|
179
184
|
},
|
|
180
185
|
maxHeel: {
|
|
181
|
-
"ui:widget": "updown"
|
|
186
|
+
"ui:widget": "updown"
|
|
182
187
|
},
|
|
183
188
|
speedStep: {
|
|
184
|
-
"ui:widget": "updown"
|
|
189
|
+
"ui:widget": "updown"
|
|
185
190
|
},
|
|
186
191
|
maxSpeed: {
|
|
187
|
-
"ui:widget": "updown"
|
|
192
|
+
"ui:widget": "updown"
|
|
188
193
|
},
|
|
189
194
|
mode: {
|
|
190
|
-
"ui:widget": "select"
|
|
191
|
-
},
|
|
192
|
-
updateCorrectionTable: {
|
|
193
|
-
"ui:widget": "hidden",
|
|
194
|
-
},
|
|
195
|
-
estimateBoatSpeed: {
|
|
196
|
-
"ui:widget": "hidden",
|
|
197
|
-
},
|
|
198
|
-
correctionStability: {
|
|
199
|
-
"ui:widget": "hidden",
|
|
195
|
+
"ui:widget": "select"
|
|
200
196
|
},
|
|
201
197
|
preventDuplication: {
|
|
202
|
-
"ui:widget": "checkbox"
|
|
203
|
-
},
|
|
204
|
-
assumeCurrent: {
|
|
205
|
-
"ui:widget": "hidden",
|
|
206
|
-
},
|
|
207
|
-
estimateCurrent: {
|
|
208
|
-
"ui:widget": "hidden",
|
|
209
|
-
},
|
|
210
|
-
currentStability: {
|
|
211
|
-
"ui:widget": "hidden",
|
|
198
|
+
"ui:widget": "checkbox"
|
|
212
199
|
}
|
|
213
200
|
};
|
|
214
201
|
|
|
215
|
-
|
|
216
202
|
plugin.registerWithRouter = function (router) {
|
|
217
203
|
app.debug('registerWithRouter');
|
|
218
204
|
|
|
@@ -236,91 +222,66 @@ module.exports = function (app) {
|
|
|
236
222
|
|
|
237
223
|
}
|
|
238
224
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
app.savePluginOptions(options, () => { app.debug('Correction table saved') });
|
|
244
|
-
return new Date();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
plugin.start = (opts) => {
|
|
249
|
-
app.setPluginStatus("starting");
|
|
250
|
-
app.debug("plugin starting");
|
|
251
|
-
let outputs = [];
|
|
252
|
-
|
|
253
|
-
// make options survive to the stop function
|
|
254
|
-
options = opts;
|
|
255
|
-
loadPresets();
|
|
256
|
-
|
|
257
|
-
// correction table
|
|
258
|
-
table = loadTable(options);
|
|
225
|
+
plugin.start = (settings) => {
|
|
226
|
+
app.setPluginStatus("Starting");
|
|
227
|
+
app.debug("Starting");
|
|
228
|
+
table = loadTable(settings);
|
|
259
229
|
|
|
230
|
+
//#region Handler and Polar Initialization
|
|
231
|
+
let smootherOptions = { timeConstant: 1, processVariance: 1, measurementVariance: 20, timeSpan: 5 };
|
|
260
232
|
|
|
261
233
|
// heading
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
};
|
|
234
|
+
smoothedHeading = new SmoothedHeading(app, plugin.id, settings.headingSource, true, MovingAverageSmoother, smootherOptions);
|
|
235
|
+
rawHeading = smoothedHeading.polar.angleHandler;
|
|
236
|
+
|
|
237
|
+
//attitude
|
|
238
|
+
smoothedAttitude = new SmoothedAttitude(app, plugin.id, settings.attitudeSource, true, MovingAverageSmoother, smootherOptions);
|
|
239
|
+
rawAttitude = smoothedAttitude.handler;
|
|
269
240
|
|
|
270
|
-
// attitude
|
|
271
|
-
attitude = new MessageHandler("attitude", "navigation.attitude", options.attitudeSource);
|
|
272
|
-
attitude.value = { pitch: 0, roll: 0, yaw: 0 }; //prevents errors when there is no attitude sensor
|
|
273
|
-
attitude.setDisplayAttributes({ label: "attitude" });
|
|
274
|
-
attitude.subscribe(app, plugin.id, true);
|
|
275
|
-
attitudeStat = new PolarDamped("attitudeDamped", attitude, 1);
|
|
276
|
-
attitude.onChange = () => {
|
|
277
|
-
attitudeStat.sample();
|
|
278
|
-
};
|
|
279
241
|
|
|
280
242
|
// current
|
|
281
|
-
|
|
282
|
-
current.
|
|
283
|
-
current.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
243
|
+
// send metadata for current
|
|
244
|
+
MessageHandler.setMeta(app, plugin.id, "environment.current.drift", {units: "m/s", type: "number", description: "Speed of the current"});
|
|
245
|
+
MessageHandler.setMeta(app, plugin.id, "environment.current.setTrue", { units: "rad", type: "number", description: "Direction of the current" });
|
|
246
|
+
rawCurrent = new Polar("current", "self.environment.current.drift", "self.environment.current.setTrue");
|
|
247
|
+
rawCurrent.setDisplayAttributes({ label: "current", plane: "Ground" });
|
|
248
|
+
rawCurrent.setAngleRange('0to2pi');
|
|
249
|
+
smoothedCurrent = new PolarSmoother("currentDamped", rawCurrent, KalmanSmoother, { processVariance: 0.000001, measurementVariance: 0.01 });
|
|
250
|
+
smoothedCurrent.setAngleRange('0to2pi');
|
|
251
|
+
smoothedCurrent.setDisplayAttributes({ label: "Current", plane: "Ground" });
|
|
287
252
|
// Current should be initialised as no current
|
|
288
|
-
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
253
|
+
rawCurrent.setVectorValue({ x: 0, y: 0 });
|
|
254
|
+
// Strongly assume no current at start
|
|
255
|
+
smoothedCurrent.xSmoother.reset(0, 0.00000001);
|
|
256
|
+
smoothedCurrent.ySmoother.reset(0, 0.00000001);
|
|
257
|
+
// no current
|
|
258
|
+
noCurrent = createSmoothedPolar({
|
|
259
|
+
id: "boatSpeed",
|
|
260
|
+
pathMagnitude: "self.environment.current.drift",
|
|
261
|
+
pathAngle: "self.environment.current.setTrue",
|
|
262
|
+
subscribe: false,
|
|
263
|
+
app,
|
|
264
|
+
pluginId: plugin.id,
|
|
265
|
+
SmootherClass: BaseSmoother,
|
|
266
|
+
smootherOptions: smootherOptions,
|
|
267
|
+
displayAttributes: { label: "NoCurrent", plane: "Ground" },
|
|
268
|
+
});
|
|
269
|
+
noCurrent.xSmoother.reset(0,0);
|
|
270
|
+
noCurrent.ySmoother.reset(0,0);
|
|
271
|
+
PolarSmoother.send(app, plugin.id, [noCurrent]);
|
|
292
272
|
|
|
293
|
-
// boat speed
|
|
294
|
-
boatSpeed = new Polar("boatSpeed", "navigation.speedThroughWater", options.boatSpeedSource);
|
|
295
|
-
boatSpeed.subscribe(app, plugin.id, true, false, !options.preventDuplication || !options.estimateBoatSpeed);
|
|
296
|
-
boatSpeed.setDisplayAttributes({ label: "boat speed", plane: "Boat" });
|
|
297
|
-
boatSpeedStat = new PolarDamped("boatSpeedDamped", boatSpeed, 1);
|
|
298
|
-
boatSpeedStat.setDisplayAttributes({ label: "boat speed (damped)", plane: "Boat" });
|
|
299
|
-
boatSpeed.onChange = () => {
|
|
300
|
-
boatSpeedStat.sample();
|
|
301
|
-
};
|
|
302
273
|
|
|
274
|
+
// boat speed
|
|
275
|
+
smoothedBoatSpeed = new SmoothedSpeedThroughWater(app, plugin.id, settings.boatSpeedSource, true, MovingAverageSmoother, smootherOptions, !settings.preventDuplication);
|
|
276
|
+
rawBoatSpeed = smoothedBoatSpeed.polar;
|
|
303
277
|
correctedBoatSpeed = new Polar("correctedBoatSpeed", "navigation.speedThroughWater", "navigation.leewayAngle");
|
|
304
278
|
correctedBoatSpeed.setDisplayAttributes({ label: "corrected boat speed", plane: "Boat" });
|
|
305
|
-
outputs.push(correctedBoatSpeed);
|
|
306
|
-
//const correctedBoatSpeedStat = new PolarDamped("correctedBoatSpeedDamped", correctedBoatSpeed);
|
|
307
|
-
|
|
308
|
-
// TODO: Add navigation.speedThroughWaterTransverse and navigation.speedThroughWaterLongitudinal
|
|
309
|
-
|
|
310
279
|
boatSpeedRefGround = new Polar("boatSpeedRefGround");
|
|
311
|
-
boatSpeedRefGround.setDisplayAttributes("
|
|
312
|
-
|
|
280
|
+
boatSpeedRefGround.setDisplayAttributes("Boat speed over ground", "Ground");
|
|
281
|
+
|
|
313
282
|
// ground speed
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
groundSpeed.setDisplayAttributes({ label: "ground speed", plane: "Ground" });
|
|
317
|
-
groundSpeed.setAngleRange('0to2pi');
|
|
318
|
-
groundSpeedStat = new PolarDamped("groundSpeedDamped", groundSpeed, 1);
|
|
319
|
-
groundSpeedStat.setDisplayAttributes({ label: "ground speed (damped)", plane: "Ground" });
|
|
320
|
-
groundSpeedStat.setAngleRange('0to2pi');
|
|
321
|
-
groundSpeed.onChange = () => {
|
|
322
|
-
groundSpeedStat.sample();
|
|
323
|
-
};
|
|
283
|
+
smoothedGroundSpeed = new SmoothedGroundSpeed(app, plugin.id, settings.SOGSource, true, MovingAverageSmoother, smootherOptions, !settings.preventDuplication);
|
|
284
|
+
rawGroundSpeed = smoothedGroundSpeed.polar;
|
|
324
285
|
|
|
325
286
|
// correction vector
|
|
326
287
|
speedCorrection = new Polar("speedCorrection");
|
|
@@ -331,273 +292,93 @@ module.exports = function (app) {
|
|
|
331
292
|
residual = new Polar("residual");
|
|
332
293
|
residual.setDisplayAttributes({ label: "residual", plane: "Ground" });
|
|
333
294
|
|
|
334
|
-
|
|
295
|
+
//#endregion
|
|
296
|
+
|
|
297
|
+
//#region Reporting
|
|
335
298
|
reportFull = new Reporter();
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
299
|
+
|
|
300
|
+
if (settings.estimateBoatSpeed) {
|
|
301
|
+
reportFull.addDelta(rawHeading);
|
|
302
|
+
reportFull.addAttitude(rawAttitude);
|
|
303
|
+
reportFull.addPolar(rawBoatSpeed);
|
|
304
|
+
reportFull.addPolar(speedCorrection);
|
|
305
|
+
reportFull.addPolar(correctedBoatSpeed);
|
|
306
|
+
reportFull.addPolar(rawGroundSpeed);
|
|
307
|
+
reportFull.addPolar(smoothedCurrent);
|
|
308
|
+
}
|
|
309
|
+
if (settings.updateCorrectionTable) {
|
|
310
|
+
reportFull.addDelta(smoothedHeading);
|
|
311
|
+
reportFull.addAttitude(smoothedAttitude);
|
|
312
|
+
reportFull.addPolar(smoothedBoatSpeed);
|
|
313
|
+
reportFull.addPolar(smoothedGroundSpeed);
|
|
314
|
+
|
|
315
|
+
if (settings.assumeCurrent) {
|
|
316
|
+
reportFull.addPolar(boatSpeedRefGround);
|
|
317
|
+
reportFull.addPolar(smoothedCurrent);
|
|
318
|
+
}
|
|
319
|
+
reportFull.addPolar(speedCorrection);
|
|
320
|
+
reportFull.addPolar(residual);
|
|
321
|
+
}
|
|
346
322
|
reportFull.addTable(table);
|
|
347
323
|
|
|
324
|
+
// Make reporting object for webApp
|
|
325
|
+
|
|
326
|
+
|
|
348
327
|
reportVector = new Reporter();
|
|
349
|
-
reportVector.addDelta(
|
|
328
|
+
reportVector.addDelta(rawHeading);
|
|
350
329
|
reportVector.addPolar(residual);
|
|
351
330
|
reportVector.addPolar(speedCorrection);
|
|
352
|
-
reportVector.addPolar(
|
|
331
|
+
if (settings.assumeCurrent) reportVector.addPolar(smoothedCurrent);
|
|
353
332
|
reportVector.addPolar(correctedBoatSpeed);
|
|
354
|
-
reportVector.addPolar(
|
|
355
|
-
reportVector.addPolar(
|
|
356
|
-
|
|
357
|
-
boatSpeed.onChange = () => {
|
|
358
|
-
boatSpeedStat.sample();
|
|
359
|
-
calculate();
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
|
|
333
|
+
reportVector.addPolar(rawBoatSpeed);
|
|
334
|
+
reportVector.addPolar(rawGroundSpeed);
|
|
335
|
+
//#endregion
|
|
363
336
|
|
|
364
337
|
isRunning = true;
|
|
338
|
+
started = new Date();
|
|
365
339
|
app.setPluginStatus("Running");
|
|
366
340
|
app.debug("Running");
|
|
367
341
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
if (!Number.isFinite(speed)) {
|
|
384
|
-
app.debug("Speed is not a valid number, skipping calculation");
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
if (!Number.isFinite(theta)) {
|
|
388
|
-
app.debug("Heading is not a valid number, skipping calculation");
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// update correction table
|
|
393
|
-
if (options.updateCorrectionTable) {
|
|
394
|
-
table.update(speed, heel, groundSpeedStat, currentStat, boatSpeedStat, theta);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// correct and estimate boat speed
|
|
398
|
-
estimateBoatSpeed();
|
|
399
|
-
|
|
400
|
-
if (options.assumeCurrent) {
|
|
401
|
-
// estimate current ;
|
|
402
|
-
estimateCurrent();
|
|
403
|
-
}
|
|
404
|
-
calcResidual();
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// Send calculated delta's
|
|
408
|
-
updates = [];
|
|
409
|
-
if (options.estimateCurrent) updates.push(current);
|
|
410
|
-
if (options.estimateBoatSpeed) updates.push(correctedBoatSpeed);
|
|
411
|
-
if (updates.length) Polar.send(app, plugin.id, updates);
|
|
412
|
-
|
|
413
|
-
// Save correction table
|
|
414
|
-
// periodically
|
|
415
|
-
if (options.updateCorrectionTable && new Date() - lastSave > 5 * 60 * 1000) {
|
|
416
|
-
lastSave = saveTable(options, table);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// estimates boat speed from observed boatSpeed and correction
|
|
421
|
-
function estimateBoatSpeed() {
|
|
422
|
-
cor = table.getKalmanCorrection(speed, heel);
|
|
423
|
-
correctedBoatSpeed.copyFrom(boatSpeed);
|
|
424
|
-
speedCorrection.setVectorValue(cor);
|
|
425
|
-
correctedBoatSpeed.add(speedCorrection);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// estimates current from observed groundspeed and estimated boatspeed
|
|
429
|
-
function estimateCurrent() {
|
|
430
|
-
boatSpeedRefGround.copyFrom(correctedBoatSpeed);
|
|
431
|
-
boatSpeedRefGround.rotate(theta);
|
|
432
|
-
current.copyFrom(groundSpeed);
|
|
433
|
-
current.substract(boatSpeedRefGround);
|
|
434
|
-
currentStat.sample();
|
|
435
|
-
current.copyFrom(currentStat);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Calculate what ground speed is observed that is not attributed to estimated boat speed or estimated current
|
|
439
|
-
function calcResidual() {
|
|
440
|
-
residual.copyFrom(groundSpeed);
|
|
441
|
-
boatSpeedRefGround.copyFrom(correctedBoatSpeed);
|
|
442
|
-
boatSpeedRefGround.rotate(theta);
|
|
443
|
-
residual.substract(boatSpeedRefGround);
|
|
444
|
-
residual.substract(current);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Make sure the table matches the settings
|
|
448
|
-
function enforceConsistancy(row, col) {
|
|
449
|
-
// function is bugged due to IS units vs knots and degrees
|
|
450
|
-
if (!options?.correctionTable) return false;
|
|
451
|
-
table = options.correctionTable;
|
|
452
|
-
if (!table) return false;
|
|
453
|
-
if (!table.row) return false;
|
|
454
|
-
if (table.row.min != row.min) return false;
|
|
455
|
-
if (table.row.max != row.max) return false;
|
|
456
|
-
if (table.row.step != row.step) return false;
|
|
457
|
-
if (table.col.min != col.min) return false;
|
|
458
|
-
if (table.col.max != col.max) return false;
|
|
459
|
-
if (table.col.step != col.step) return false;
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function loadTable(options) {
|
|
464
|
-
const row = { min: 0, max: SI.fromKnots(options.maxSpeed), step: SI.fromKnots(options.speedStep) };
|
|
465
|
-
const col = { min: -SI.fromDegrees(options.maxHeel), max: SI.fromDegrees(options.maxHeel), step: SI.fromDegrees(options.heelStep) };
|
|
466
|
-
let table;
|
|
467
|
-
if (enforceConsistancy(row, col) && !options.doStartFresh) {
|
|
468
|
-
table = CorrectionTable.fromJSON(options.correctionTable, options.correctionStability);
|
|
469
|
-
app.debug("Correction table loaded");
|
|
470
|
-
lastSave = new Date();
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
table = new CorrectionTable("correctionTable", row, col, options.correctionStability);
|
|
474
|
-
app.debug("Correction table created");
|
|
475
|
-
options.doStartFresh = false;
|
|
476
|
-
lastSave = saveTable(options, table);
|
|
477
|
-
}
|
|
478
|
-
table.setDisplayAttributes({ label: "correction table" });
|
|
479
|
-
return table;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
function loadPresets() {
|
|
484
|
-
const modeEnum = plugin.schema.properties.mode.enum;
|
|
485
|
-
app.debug("Loading presests for mode: " + options.mode);
|
|
486
|
-
|
|
487
|
-
switch (modeEnum.indexOf(options.mode)) {
|
|
488
|
-
case -1:
|
|
489
|
-
app.debug("Invalid mode");
|
|
490
|
-
app.debug(options);
|
|
491
|
-
//options.mode = modeEnum[2];
|
|
492
|
-
break;
|
|
493
|
-
case 0:
|
|
494
|
-
// new table, no current
|
|
495
|
-
options.doStartFresh = true;
|
|
496
|
-
options.updateCorrectionTable = true;
|
|
497
|
-
options.estimateBoatSpeed = false;
|
|
498
|
-
options.assumeCurrent = false;
|
|
499
|
-
options.estimateCurrent = false;
|
|
500
|
-
options.correctionStability = 5;
|
|
501
|
-
options.mode = modeEnum[2];
|
|
502
|
-
break;
|
|
503
|
-
case 1:
|
|
504
|
-
// new table, with current
|
|
505
|
-
options.doStartFresh = true;
|
|
506
|
-
options.updateCorrectionTable = true;
|
|
507
|
-
options.estimateBoatSpeed = false;
|
|
508
|
-
options.assumeCurrent = true;
|
|
509
|
-
options.estimateCurrent = false;
|
|
510
|
-
options.correctionStability = 5;
|
|
511
|
-
options.currentStability = 7;
|
|
512
|
-
options.mode = modeEnum[3];
|
|
513
|
-
break;
|
|
514
|
-
case 2:
|
|
515
|
-
// fresh table, no current
|
|
516
|
-
options.doStartFresh = false;
|
|
517
|
-
options.updateCorrectionTable = true;
|
|
518
|
-
options.estimateBoatSpeed = false;
|
|
519
|
-
options.assumeCurrent = false;
|
|
520
|
-
options.estimateCurrent = false;
|
|
521
|
-
options.correctionStability = 6;
|
|
522
|
-
break;
|
|
523
|
-
case 3:
|
|
524
|
-
// fresh table, with current
|
|
525
|
-
options.doStartFresh = false;
|
|
526
|
-
options.updateCorrectionTable = true;
|
|
527
|
-
options.estimateBoatSpeed = false;
|
|
528
|
-
options.assumeCurrent = true;
|
|
529
|
-
options.estimateCurrent = false;
|
|
530
|
-
options.correctionStability = 6;
|
|
531
|
-
options.currentStability = 6;
|
|
532
|
-
break;
|
|
533
|
-
case 4:
|
|
534
|
-
// mature table, no current
|
|
535
|
-
options.doStartFresh = false;
|
|
536
|
-
options.updateCorrectionTable = true;
|
|
537
|
-
options.estimateBoatSpeed = true;
|
|
538
|
-
options.assumeCurrent = false;
|
|
539
|
-
options.estimateCurrent = false;
|
|
540
|
-
options.correctionStability = 10;
|
|
541
|
-
break;
|
|
542
|
-
case 5:
|
|
543
|
-
// mature table, with current
|
|
544
|
-
options.doStartFresh = false;
|
|
545
|
-
options.updateCorrectionTable = true;
|
|
546
|
-
options.estimateBoatSpeed = true;
|
|
547
|
-
options.assumeCurrent = true;
|
|
548
|
-
options.estimateCurrent = true;
|
|
549
|
-
options.correctionStability = 10;
|
|
550
|
-
options.currentStability = 3;
|
|
551
|
-
break;
|
|
552
|
-
case 6:
|
|
553
|
-
// locked table
|
|
554
|
-
options.doStartFresh = false;
|
|
555
|
-
options.updateCorrectionTable = false;
|
|
556
|
-
options.estimateBoatSpeed = true;
|
|
557
|
-
options.assumeCurrent = true;
|
|
558
|
-
options.estimateCurrent = true;
|
|
559
|
-
options.currentStability = 2;
|
|
560
|
-
break;
|
|
561
|
-
case 7:
|
|
562
|
-
// all manual configuration;
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// compare floats
|
|
568
|
-
function isEqual(a, b) {
|
|
569
|
-
const TOLERANCE = 1e-5;
|
|
570
|
-
if (Math.abs(a - b) < TOLERANCE) {
|
|
571
|
-
return true;
|
|
342
|
+
let lastSave = 0;
|
|
343
|
+
smoothedBoatSpeed.onChange = () => {
|
|
344
|
+
const wellUnderway = started < new Date() - 60 * 1000;
|
|
345
|
+
const minSpeed = SI.fromKnots(settings.speedStep / 2);
|
|
346
|
+
if (settings.estimateBoatSpeed) correct(wellUnderway);
|
|
347
|
+
if (settings.updateCorrectionTable && wellUnderway) {
|
|
348
|
+
updateTable(settings.assumeCurrent, minSpeed);
|
|
349
|
+
// Save correction table periodically
|
|
350
|
+
const now = new Date();
|
|
351
|
+
if (now - lastSave > 5 * 1000) {
|
|
352
|
+
saveTable(settings, table);
|
|
353
|
+
lastSave = now;
|
|
354
|
+
}
|
|
572
355
|
}
|
|
573
|
-
|
|
574
|
-
}
|
|
356
|
+
};
|
|
575
357
|
}
|
|
576
358
|
|
|
577
|
-
|
|
578
359
|
plugin.stop = () => {
|
|
579
360
|
return new Promise((resolve, reject) => {
|
|
580
361
|
try {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
attitude = attitude?.terminate(app);
|
|
588
|
-
current = current?.terminate(app);
|
|
589
|
-
currentStat = null;
|
|
590
|
-
boatSpeed = boatSpeed?.terminate(app);
|
|
591
|
-
boatSpeedStat = null;
|
|
362
|
+
smoothedHeading = smoothedHeading?.terminate(app);
|
|
363
|
+
stability = stability?.terminate?.(app);
|
|
364
|
+
smoothedAttitude = smoothedAttitude?.terminate(app);
|
|
365
|
+
rawCurrent = rawCurrent?.terminate(app);
|
|
366
|
+
smoothedCurrent = smoothedCurrent?.terminate?.(app);
|
|
367
|
+
smoothedBoatSpeed = smoothedBoatSpeed?.terminate(app);
|
|
592
368
|
correctedBoatSpeed = correctedBoatSpeed?.terminate(app);
|
|
593
369
|
boatSpeedRefGround = boatSpeedRefGround?.terminate(app);
|
|
594
|
-
|
|
595
|
-
groundSpeedStat = null;
|
|
370
|
+
smoothedGroundSpeed = smoothedGroundSpeed?.terminate(app);
|
|
596
371
|
speedCorrection = speedCorrection?.terminate(app);
|
|
597
372
|
residual = residual?.terminate(app);
|
|
598
373
|
reportFull = null;
|
|
599
374
|
reportVector = null;
|
|
600
375
|
table = null;
|
|
376
|
+
rawHeading = null;
|
|
377
|
+
rawAttitude = null;
|
|
378
|
+
noCurrent = null;
|
|
379
|
+
rawBoatSpeed = null;
|
|
380
|
+
rawGroundSpeed = null;
|
|
381
|
+
started = null;
|
|
601
382
|
app.setPluginStatus("Stopped");
|
|
602
383
|
app.debug("Stopped");
|
|
603
384
|
|
|
@@ -608,5 +389,157 @@ module.exports = function (app) {
|
|
|
608
389
|
}
|
|
609
390
|
});
|
|
610
391
|
};
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Estimates and applies corrections to boat speed, leeway, and current.
|
|
395
|
+
*
|
|
396
|
+
* This function uses the current attitude, boat speed, and heading to compute a correction
|
|
397
|
+
* vector from the correction table, applies it to the observed boat speed, and sends the
|
|
398
|
+
* corrected value. If the vessel is well underway, it also estimates the current vector
|
|
399
|
+
* by comparing the corrected boat speed (rotated to heading) with the ground speed.
|
|
400
|
+
*
|
|
401
|
+
* @param {boolean} wellUnderway - True if the vessel is considered underway and current estimation should be performed.
|
|
402
|
+
*/
|
|
403
|
+
function correct(wellUnderway) {
|
|
404
|
+
// prepare iteration
|
|
405
|
+
const heel = rawAttitude.value?.roll;
|
|
406
|
+
const speed = rawBoatSpeed.magnitude;
|
|
407
|
+
const theta = rawHeading.value;
|
|
408
|
+
//app.debug(`Heel: ${SI.toDegrees(heel).toFixed(1)}°, Speed: ${SI.toKnots(speed).toFixed(2)} kn, Heading: ${SI.toDegrees(theta).toFixed(1)}°`);
|
|
409
|
+
if (!Number.isFinite(heel) || !Number.isFinite(speed) || !Number.isFinite(theta)) {
|
|
410
|
+
//app.debug("Invalid input data");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
correctedBoatSpeed.copyFrom(rawBoatSpeed);
|
|
415
|
+
speedCorrection.setVectorValue({ x: 0, y: 0 }) ;
|
|
416
|
+
if (speed > 0 && !rawAttitude.stale ) {
|
|
417
|
+
const { correction, variance } = table.getCorrection(speed, heel);
|
|
418
|
+
speedCorrection.setVectorValue(correction, variance);
|
|
419
|
+
correctedBoatSpeed.add(speedCorrection);
|
|
420
|
+
}
|
|
421
|
+
Polar.send(app, plugin.id, [correctedBoatSpeed]);
|
|
422
|
+
// estimate current
|
|
423
|
+
if (wellUnderway && !rawGroundSpeed.stale) {
|
|
424
|
+
boatSpeedRefGround.copyFrom(correctedBoatSpeed);
|
|
425
|
+
boatSpeedRefGround.rotate(theta);
|
|
426
|
+
rawCurrent.copyFrom(rawGroundSpeed);
|
|
427
|
+
rawCurrent.substract(boatSpeedRefGround);
|
|
428
|
+
smoothedCurrent.sample();
|
|
429
|
+
}
|
|
430
|
+
PolarSmoother.send(app, plugin.id, [smoothedCurrent]);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Updates the correction table and calculates the residual error.
|
|
435
|
+
*
|
|
436
|
+
* This function uses the current smoothed attitude, boat speed, and heading to update the correction table
|
|
437
|
+
* if the boat speed exceeds the specified minimum. It also calculates the residual between the measured ground speed
|
|
438
|
+
* and the expected ground speed (corrected boat speed plus current). The correction table is periodically saved.
|
|
439
|
+
*
|
|
440
|
+
* @param {boolean} [assumeCurrent=false] - If true, uses the smoothed current for correction; otherwise, assumes no current.
|
|
441
|
+
* @param {number} [minSpeed=0] - The minimum speed threshold (in SI units) required to update the correction table.
|
|
442
|
+
*/
|
|
443
|
+
function updateTable(assumeCurrent = false, minSpeed = 0) {
|
|
444
|
+
// prepare iteration
|
|
445
|
+
const heel = smoothedAttitude.value.roll;
|
|
446
|
+
const speed = smoothedBoatSpeed.magnitude;
|
|
447
|
+
const theta = smoothedHeading.value;
|
|
448
|
+
|
|
449
|
+
if (!Number.isFinite(heel) || !Number.isFinite(speed) || !Number.isFinite(theta)) return;
|
|
450
|
+
|
|
451
|
+
// update correction table
|
|
452
|
+
if (speed > minSpeed && !smoothedAttitude.stale && !smoothedHeading.stale && !smoothedGroundSpeed.stale) {
|
|
453
|
+
table.update(speed, heel, smoothedGroundSpeed, assumeCurrent ? smoothedCurrent : noCurrent, smoothedBoatSpeed, theta);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// calculate residual
|
|
457
|
+
residual.copyFrom(smoothedGroundSpeed);
|
|
458
|
+
boatSpeedRefGround.copyFrom(correctedBoatSpeed);
|
|
459
|
+
boatSpeedRefGround.rotate(theta);
|
|
460
|
+
residual.substract(boatSpeedRefGround);
|
|
461
|
+
residual.substract(smoothedCurrent);
|
|
462
|
+
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Checks if the correction table in settings matches the expected row and column configuration.
|
|
467
|
+
*
|
|
468
|
+
* This function compares the min, max, and step values of the row and column definitions
|
|
469
|
+
* in the current correction table against the provided row and col objects. Returns true
|
|
470
|
+
* if all values match, otherwise false. If the table is missing or not properly structured,
|
|
471
|
+
* returns false.
|
|
472
|
+
*
|
|
473
|
+
* @param {Object} row - The expected row configuration ({ min, max, step }).
|
|
474
|
+
* @param {Object} col - The expected column configuration ({ min, max, step }).
|
|
475
|
+
* @returns {boolean} True if the table matches the configuration, false otherwise.
|
|
476
|
+
*/
|
|
477
|
+
function enforceConsistancy(options, row, col) {
|
|
478
|
+
if (!options?.correctionTable) return false;
|
|
479
|
+
table = options.correctionTable;
|
|
480
|
+
if (!table) return false;
|
|
481
|
+
if (!table.row) return false;
|
|
482
|
+
if (table.row.min != row.min) return false;
|
|
483
|
+
if (table.row.max != row.max) return false;
|
|
484
|
+
if (table.row.step != row.step) return false;
|
|
485
|
+
if (table.col.min != col.min) return false;
|
|
486
|
+
if (table.col.max != col.max) return false;
|
|
487
|
+
if (table.col.step != col.step) return false;
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Loads or creates a correction table based on the provided options.
|
|
493
|
+
*
|
|
494
|
+
* This function checks if the correction table in the options matches the expected row and column configuration.
|
|
495
|
+
* If so, it loads the table from JSON; otherwise, it creates a new correction table with the specified parameters.
|
|
496
|
+
* The table is labeled and, if newly created, saved to the plugin options.
|
|
497
|
+
*
|
|
498
|
+
* @param {Object} options - The plugin options containing correction table settings and parameters.
|
|
499
|
+
* @returns {CorrectionTable} The loaded or newly created CorrectionTable instance.
|
|
500
|
+
*/
|
|
501
|
+
function loadTable(options) {
|
|
502
|
+
const row = { min: 0, max: SI.fromKnots(options.maxSpeed), step: SI.fromKnots(options.speedStep) };
|
|
503
|
+
const col = { min: -SI.fromDegrees(options.maxHeel), max: SI.fromDegrees(options.maxHeel), step: SI.fromDegrees(options.heelStep) };
|
|
504
|
+
let table;
|
|
505
|
+
const stability = (options.stability !== undefined) ? options.stability : 6;
|
|
506
|
+
const VAR_FLOOR = 1e-4;
|
|
507
|
+
if (enforceConsistancy(options, row, col) && !options.startWithNewTable) {
|
|
508
|
+
table = CorrectionTable.fromJSON(options.correctionTable, stability);
|
|
509
|
+
app.debug("Correction table loaded");
|
|
510
|
+
}
|
|
511
|
+
else if (options.correctionTable && !options.startWithNewTable) {
|
|
512
|
+
// Resample existing table to new dimensions conservatively (N=0, diag covariance with floor)
|
|
513
|
+
table = CorrectionTable.resampleFromJSON(options.correctionTable, row, col, stability, VAR_FLOOR);
|
|
514
|
+
app.debug("Correction table migrated to new dimensions");
|
|
515
|
+
options.startWithNewTable = false;
|
|
516
|
+
saveTable(options, table);
|
|
517
|
+
} else {
|
|
518
|
+
table = new CorrectionTable("correctionTable", row, col, stability);
|
|
519
|
+
app.debug("Correction table created");
|
|
520
|
+
// doStartFresh is not user-settable, so no need to reset it here
|
|
521
|
+
options.startWithNewTable = false;
|
|
522
|
+
saveTable(options, table);
|
|
523
|
+
}
|
|
524
|
+
table.setDisplayAttributes({ label: "correction table" });
|
|
525
|
+
return table;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Saves the correction table to the plugin options and logs the save event.
|
|
530
|
+
*
|
|
531
|
+
* This function serializes the provided correction table, updates the options object,
|
|
532
|
+
* and triggers the plugin's save mechanism. It returns the current date/time to indicate
|
|
533
|
+
* when the save occurred.
|
|
534
|
+
*
|
|
535
|
+
* @param {Object} options - The plugin options object to update.
|
|
536
|
+
* @param {CorrectionTable} correctionTable - The correction table instance to save.
|
|
537
|
+
* @returns {Date} The date and time when the table was saved.
|
|
538
|
+
*/
|
|
539
|
+
function saveTable(options, correctionTable) {
|
|
540
|
+
options.correctionTable = correctionTable.toJSON();
|
|
541
|
+
app.savePluginOptions(options, () => { /*app.debug("Correction table saved")*/; });
|
|
542
|
+
}
|
|
543
|
+
|
|
611
544
|
return plugin;
|
|
612
545
|
};
|