signalk-polar-performance-plugin 0.0.13 → 0.0.14
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/README.md +6 -2
- package/package.json +1 -1
- package/plugin/index.js +103 -55
package/README.md
CHANGED
|
@@ -17,8 +17,11 @@ The polar diagram can be configured through CSV notation as used on [ORC sailboa
|
|
|
17
17
|
|
|
18
18
|
### Plugin options
|
|
19
19
|
In the plugin configuration you can toggle the following options:
|
|
20
|
-
- Enable calculation of beat/upwind and run/gybe/downwind angle
|
|
21
|
-
- Enable calculation of beat/upwind and run/gybe/downwind VMG
|
|
20
|
+
- Enable calculation/sending of beat/upwind and run/gybe/downwind angle
|
|
21
|
+
- Enable calculation/sending of beat/upwind and run/gybe/downwind VMG
|
|
22
|
+
- Enable sending Target TWA
|
|
23
|
+
- Enable calculation of Optimum Wind Angle (difference between TWA and beat/run angle (depends on beat/run angle)
|
|
24
|
+
- Enable sending of maximum speed angle and boat speed for a given TWS
|
|
22
25
|
- Enable calculation of Optimum Wind Angle (difference between TWA and beat/run angle (depends on beat/run angle)
|
|
23
26
|
|
|
24
27
|
## Calculated performance data
|
|
@@ -40,6 +43,7 @@ In the plugin configuration you can toggle the following options:
|
|
|
40
43
|
- Make moment to do calculation smarter/configurable
|
|
41
44
|
- API to see JSON of polar
|
|
42
45
|
- Visualisation of the polar diagram
|
|
46
|
+
- Extrapolation of polar data
|
|
43
47
|
- Create polar from live data
|
|
44
48
|
-- Save polar info to file
|
|
45
49
|
-- Save new record speed for angle in polar
|
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -18,11 +18,15 @@ module.exports = function (app) {
|
|
|
18
18
|
properties: {
|
|
19
19
|
beatAngle: {
|
|
20
20
|
type: 'boolean',
|
|
21
|
-
title: 'Enable calculation of beat/upwind and run/gybe/downwind angle'
|
|
21
|
+
title: 'Enable calculation/sending of beat/upwind and run/gybe/downwind angle'
|
|
22
22
|
},
|
|
23
23
|
beatVMG: {
|
|
24
24
|
type: 'boolean',
|
|
25
|
-
title: 'Enable calculation of beat/upwind and run/gybe/downwind VMG'
|
|
25
|
+
title: 'Enable calculation/sending of beat/upwind and run/gybe/downwind VMG'
|
|
26
|
+
},
|
|
27
|
+
targetTWA: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
title: 'Enable sending Target TWA (performance.targetAngle)'
|
|
26
30
|
},
|
|
27
31
|
optimumWindAngle: {
|
|
28
32
|
type: 'boolean',
|
|
@@ -136,22 +140,34 @@ module.exports = function (app) {
|
|
|
136
140
|
let values = []
|
|
137
141
|
let metas = []
|
|
138
142
|
if (typeof perfObj.beatAngle != 'undefined') {
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
if (options.beatAngle == true) {
|
|
144
|
+
values.push({path: 'performance.beatAngle', value: roundDec(perfObj.beatAngle)})
|
|
145
|
+
}
|
|
146
|
+
if (options.targetTWA == true) {
|
|
147
|
+
values.push({path: 'performance.targetAngle', value: roundDec(perfObj.beatAngle)})
|
|
148
|
+
}
|
|
141
149
|
}
|
|
142
150
|
if (typeof perfObj.beatVMG != 'undefined') {
|
|
143
151
|
values.push({path: 'performance.beatAngleVelocityMadeGood', value: roundDec(perfObj.beatVMG)})
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
if (options.targetTWA == true) {
|
|
153
|
+
values.push({path: 'performance.targetVelocityMadeGood', value: roundDec(perfObj.beatVMG)})
|
|
154
|
+
metas.push({path: 'performance.targetVelocityMadeGood', value: {"units": "rad"}})
|
|
155
|
+
}
|
|
146
156
|
}
|
|
147
157
|
if (typeof perfObj.runAngle != 'undefined') {
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
if (options.beatAngle == true) {
|
|
159
|
+
values.push({path: 'performance.gybeAngle', value: roundDec(perfObj.runAngle)})
|
|
160
|
+
}
|
|
161
|
+
if (options.targetTWA == true) {
|
|
162
|
+
values.push({path: 'performance.targetAngle', value: roundDec(perfObj.runAngle)})
|
|
163
|
+
}
|
|
150
164
|
}
|
|
151
165
|
if (typeof perfObj.runVMG != 'undefined') {
|
|
152
166
|
values.push({path: 'performance.gybeAngleVelocityMadeGood', value: roundDec(perfObj.runVMG)})
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
if (options.targetTWA == true) {
|
|
168
|
+
values.push({path: 'performance.targetVelocityMadeGood', value: roundDec(perfObj.runVMG)})
|
|
169
|
+
metas.push({path: 'performance.targetVelocityMadeGood', value: {"units": "rad"}})
|
|
170
|
+
}
|
|
155
171
|
}
|
|
156
172
|
if (typeof perfObj.optimumWindAngle != 'undefined') {
|
|
157
173
|
values.push({path: 'performance.optimumWindAngle', value: roundDec(perfObj.optimumWindAngle)})
|
|
@@ -197,13 +213,11 @@ module.exports = function (app) {
|
|
|
197
213
|
// Calculate beat/run angle
|
|
198
214
|
if (TWA < halfPi) {
|
|
199
215
|
// app.debug('Upwind')
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
performance.beatAngle = beatLower + ((beatUpper - beatLower) * twsGapRatio)
|
|
206
|
-
}
|
|
216
|
+
// Take beat angle
|
|
217
|
+
if (typeof polar[indexTWS]['Beat angle'] != 'undefined') {
|
|
218
|
+
let beatLower = polar[indexTWS]['Beat angle']
|
|
219
|
+
let beatUpper = polar[indexTWS+1]['Beat angle']
|
|
220
|
+
performance.beatAngle = beatLower + ((beatUpper - beatLower) * twsGapRatio)
|
|
207
221
|
}
|
|
208
222
|
// Calculate optimum wind angle (B&G thing)
|
|
209
223
|
if (options.optimumWindAngle == true) {
|
|
@@ -212,23 +226,19 @@ module.exports = function (app) {
|
|
|
212
226
|
}
|
|
213
227
|
}
|
|
214
228
|
// Calculate beat VMG
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
performance.beatVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
220
|
-
}
|
|
229
|
+
if (typeof polar[indexTWS]['Beat VMG'] != 'undefined') {
|
|
230
|
+
let VMGLower = polar[indexTWS]['Beat VMG']
|
|
231
|
+
let VMGUpper = polar[indexTWS+1]['Beat VMG']
|
|
232
|
+
performance.beatVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
221
233
|
}
|
|
222
234
|
} else {
|
|
223
235
|
// app.debug('Downwind')
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
performance.runAngle = runLower + ((runUpper - runLower) * twsGapRatio)
|
|
231
|
-
}
|
|
236
|
+
if (typeof polar[indexTWS]['Run angle'] != 'undefined') {
|
|
237
|
+
// Calculate run angle
|
|
238
|
+
let runLower = polar[indexTWS]['Run angle']
|
|
239
|
+
let runUpper = polar[indexTWS+1]['Run angle']
|
|
240
|
+
//app.debug('runLower: %s runUpper: %s', runLower, runUpper)
|
|
241
|
+
performance.runAngle = runLower + ((runUpper - runLower) * twsGapRatio)
|
|
232
242
|
}
|
|
233
243
|
if (options.optimumWindAngle == true) {
|
|
234
244
|
if (typeof performance.runAngle != 'undefined') {
|
|
@@ -236,14 +246,12 @@ module.exports = function (app) {
|
|
|
236
246
|
performance.optimumWindAngle = (performance.runAngle - TWA) * port * -1
|
|
237
247
|
}
|
|
238
248
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
246
|
-
}
|
|
249
|
+
// Calculate run VMG
|
|
250
|
+
if (typeof polar[indexTWS]['Run VMG'] != 'undefined') {
|
|
251
|
+
let VMGLower = polar[indexTWS]['Run VMG']
|
|
252
|
+
let VMGUpper = polar[indexTWS+1]['Run VMG']
|
|
253
|
+
//app.debug('VMGLower: %s VMGUpper: %s', VMGLower, VMGUpper)
|
|
254
|
+
performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
247
255
|
}
|
|
248
256
|
}
|
|
249
257
|
// Calculate polar target boat speed
|
|
@@ -348,10 +356,10 @@ module.exports = function (app) {
|
|
|
348
356
|
app.debug('beat and run angles are included')
|
|
349
357
|
} else if (row.includes('0')) {
|
|
350
358
|
// Beat / Run angle
|
|
351
|
-
let angle = Number(row[0])
|
|
359
|
+
let angle = degToRad(Number(row[0]))
|
|
352
360
|
let angleName
|
|
353
361
|
let VMGName
|
|
354
|
-
if (angle <
|
|
362
|
+
if (angle < halfPi) {
|
|
355
363
|
angleName = 'Beat angle'
|
|
356
364
|
VMGName = 'Beat VMG'
|
|
357
365
|
} else {
|
|
@@ -360,17 +368,18 @@ module.exports = function (app) {
|
|
|
360
368
|
}
|
|
361
369
|
for (let index = 0; index < row.length-1; index++) {
|
|
362
370
|
if (row[index+1] != 0) {
|
|
363
|
-
polar[index][angleName] =
|
|
364
|
-
|
|
371
|
+
polar[index][angleName] = angle
|
|
372
|
+
let tbs = ktsToMs(Number(row[index+1]))
|
|
373
|
+
let vmg = tbs * Math.abs(Math.cos(angle))
|
|
374
|
+
polar[index][VMGName] = roundDec(vmg)
|
|
365
375
|
if (typeof polar[index]['twa'] == 'undefined') {
|
|
366
376
|
polar[index]['twa'] = []
|
|
367
377
|
}
|
|
368
|
-
let
|
|
369
|
-
let Obj = {"twa": degToRad(angle), "tbs": tbs}
|
|
378
|
+
let Obj = {"twa": angle, "tbs": tbs, "vmg": vmg}
|
|
370
379
|
polar[index]['twa'] = polar[index]['twa'].concat(Obj)
|
|
371
380
|
if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
|
|
372
381
|
polar[index]['Max speed'] = tbs
|
|
373
|
-
polar[index]['Max speed angle'] =
|
|
382
|
+
polar[index]['Max speed angle'] = angle
|
|
374
383
|
}
|
|
375
384
|
}
|
|
376
385
|
}
|
|
@@ -382,9 +391,13 @@ module.exports = function (app) {
|
|
|
382
391
|
polar[index].twa = []
|
|
383
392
|
}
|
|
384
393
|
let tbs = ktsToMs(Number(row[index+1]))
|
|
385
|
-
let
|
|
386
|
-
|
|
394
|
+
let vmg = tbs * Math.abs(Math.cos(angle))
|
|
395
|
+
let Obj = {"twa": angle, "tbs": tbs, "vmg": vmg}
|
|
396
|
+
// app.debug('Adding Obj: %s', JSON.stringify(Obj))
|
|
387
397
|
polar[index]['twa'] = polar[index]['twa'].concat(Obj)
|
|
398
|
+
// See if we need to set/overwrite beat/run angle
|
|
399
|
+
|
|
400
|
+
|
|
388
401
|
// See if this a new Max speed
|
|
389
402
|
if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
|
|
390
403
|
polar[index]['Max speed'] = tbs
|
|
@@ -402,34 +415,68 @@ module.exports = function (app) {
|
|
|
402
415
|
tws.twa = twaArray
|
|
403
416
|
})
|
|
404
417
|
|
|
418
|
+
// Find the beat/run angle if not set yet.
|
|
419
|
+
if (typeof polar[0].beatAngle == 'undefined') {
|
|
420
|
+
for (let index = 0; index < polar.length-1; index++) {
|
|
421
|
+
let tws = polar[index].tws
|
|
422
|
+
let beatVMG = 0
|
|
423
|
+
let beatVMGelement = 0
|
|
424
|
+
let runVMG = 0
|
|
425
|
+
let runVMGelement = 0
|
|
426
|
+
|
|
427
|
+
let twaArray = polar[index].twa
|
|
428
|
+
for (let element = 0; element < twaArray.length-1; element++) {
|
|
429
|
+
let Obj = twaArray[element]
|
|
430
|
+
if (Obj.twa < halfPi) {
|
|
431
|
+
// Beat
|
|
432
|
+
if (Obj.vmg > beatVMG) {
|
|
433
|
+
beatElement = element
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
// Run
|
|
437
|
+
if (Obj.vmg > runVMG) {
|
|
438
|
+
runElement = element
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
app.debug('beatVMG for %d is %d (angel %d)', tws, twaArray[beatVMGelement].vmg, twaArray[beatVMGelement].twa)
|
|
443
|
+
app.debug(' runVMG for %d is %d (angel %d)', tws, twaArray[runVMGelement].vmg, twaArray[runVMGelement].twa)
|
|
444
|
+
polar[index]['Beat angle'] = twaArray[beatVMGelement].twa
|
|
445
|
+
polar[index]['Beat VMG'] = twaArray[beatVMGelement].vmg
|
|
446
|
+
polar[index]['Run angle'] = twaArray[runVMGelement].twa
|
|
447
|
+
polar[index]['Run VMG'] = twaArray[runVMGelement].vmg
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
}
|
|
451
|
+
|
|
405
452
|
// And now fill in some missing ends to avoid doing expensive calculations in the main loop
|
|
406
453
|
for (let index = 0; index < polar.length-1; index++) {
|
|
407
454
|
// Sorted on angle, so first is lowest
|
|
455
|
+
let tws = polar[index].tws
|
|
408
456
|
let twaArray = polar[index].twa
|
|
409
457
|
let lowTBS = twaArray[0].tbs
|
|
410
458
|
let lowTWA = twaArray[0].twa
|
|
411
459
|
|
|
412
|
-
|
|
460
|
+
app.debug('Padding polar for %s from 0 to first given angle (%d deg, %d kts)', msToKts(tws).toFixed(0), radToDeg(lowTWA), msToKts(lowTBS))
|
|
413
461
|
|
|
414
462
|
// Now put some extra values at the beginning
|
|
415
463
|
var topArray = []
|
|
416
464
|
for (let angle = 0; angle < lowTWA; angle = angle + degToRad(5)) {
|
|
417
465
|
let tbs = (angle / lowTWA) * Math.pow(Math.cos((-1*lowTWA + angle)*3),3) * lowTBS
|
|
418
466
|
let Obj = {'twa': angle, 'tbs': tbs}
|
|
419
|
-
// app.debug('Adding Obj: %s', JSON.stringify(Obj))
|
|
420
467
|
topArray.push(Obj)
|
|
421
|
-
// app.debug('twaArray: %s', JSON.stringify(twaArray))
|
|
422
468
|
}
|
|
423
|
-
|
|
424
|
-
|
|
469
|
+
// app.debug('Adding values: %s', JSON.stringify(topArray))
|
|
470
|
+
polar[index].twa = topArray.concat(twaArray)
|
|
425
471
|
}
|
|
426
472
|
|
|
427
473
|
for (let index = 0; index < polar.length-1; index++) {
|
|
474
|
+
let tws = polar[index].tws
|
|
428
475
|
let twaArray = polar[index].twa
|
|
429
476
|
let highTBS = twaArray[twaArray.length-1].tbs
|
|
430
477
|
let highTWA = twaArray[twaArray.length-1].twa
|
|
431
478
|
|
|
432
|
-
|
|
479
|
+
app.debug('Padding polar for %s from highTWA to last given angle (%d, %d kts)', msToKts(tws).toFixed(0), highTWA, msToKts(highTBS))
|
|
433
480
|
|
|
434
481
|
// Now put some extra values at the beginning
|
|
435
482
|
var tailArray = []
|
|
@@ -438,7 +485,8 @@ module.exports = function (app) {
|
|
|
438
485
|
let Obj = {'twa': angle, 'tbs': tbs}
|
|
439
486
|
tailArray.unshift(Obj)
|
|
440
487
|
}
|
|
441
|
-
|
|
488
|
+
// app.debug('Adding values: %s', JSON.stringify(tailArray))
|
|
489
|
+
polar[index].twa = twaArray.concat(tailArray)
|
|
442
490
|
}
|
|
443
491
|
|
|
444
492
|
// app.debug(JSON.stringify(polar))
|