signalk-polar-performance-plugin 0.0.12 → 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 +98 -36
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,25 +213,27 @@ module.exports = function (app) {
|
|
|
197
213
|
// Calculate beat/run angle
|
|
198
214
|
if (TWA < halfPi) {
|
|
199
215
|
// app.debug('Upwind')
|
|
200
|
-
|
|
201
|
-
|
|
216
|
+
// Take beat angle
|
|
217
|
+
if (typeof polar[indexTWS]['Beat angle'] != 'undefined') {
|
|
202
218
|
let beatLower = polar[indexTWS]['Beat angle']
|
|
203
219
|
let beatUpper = polar[indexTWS+1]['Beat angle']
|
|
204
220
|
performance.beatAngle = beatLower + ((beatUpper - beatLower) * twsGapRatio)
|
|
205
221
|
}
|
|
206
222
|
// Calculate optimum wind angle (B&G thing)
|
|
207
223
|
if (options.optimumWindAngle == true) {
|
|
208
|
-
|
|
224
|
+
if (typeof performance.beatAngle != 'undefined') {
|
|
225
|
+
performance.optimumWindAngle = (TWA - performance.beatAngle) * port
|
|
226
|
+
}
|
|
209
227
|
}
|
|
210
228
|
// Calculate beat VMG
|
|
211
|
-
|
|
229
|
+
if (typeof polar[indexTWS]['Beat VMG'] != 'undefined') {
|
|
212
230
|
let VMGLower = polar[indexTWS]['Beat VMG']
|
|
213
231
|
let VMGUpper = polar[indexTWS+1]['Beat VMG']
|
|
214
232
|
performance.beatVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
215
233
|
}
|
|
216
234
|
} else {
|
|
217
235
|
// app.debug('Downwind')
|
|
218
|
-
if (
|
|
236
|
+
if (typeof polar[indexTWS]['Run angle'] != 'undefined') {
|
|
219
237
|
// Calculate run angle
|
|
220
238
|
let runLower = polar[indexTWS]['Run angle']
|
|
221
239
|
let runUpper = polar[indexTWS+1]['Run angle']
|
|
@@ -223,14 +241,16 @@ module.exports = function (app) {
|
|
|
223
241
|
performance.runAngle = runLower + ((runUpper - runLower) * twsGapRatio)
|
|
224
242
|
}
|
|
225
243
|
if (options.optimumWindAngle == true) {
|
|
226
|
-
|
|
227
|
-
|
|
244
|
+
if (typeof performance.runAngle != 'undefined') {
|
|
245
|
+
// Calculate optimum wind angle
|
|
246
|
+
performance.optimumWindAngle = (performance.runAngle - TWA) * port * -1
|
|
247
|
+
}
|
|
228
248
|
}
|
|
229
|
-
|
|
230
|
-
|
|
249
|
+
// Calculate run VMG
|
|
250
|
+
if (typeof polar[indexTWS]['Run VMG'] != 'undefined') {
|
|
231
251
|
let VMGLower = polar[indexTWS]['Run VMG']
|
|
232
252
|
let VMGUpper = polar[indexTWS+1]['Run VMG']
|
|
233
|
-
|
|
253
|
+
//app.debug('VMGLower: %s VMGUpper: %s', VMGLower, VMGUpper)
|
|
234
254
|
performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
235
255
|
}
|
|
236
256
|
}
|
|
@@ -336,10 +356,10 @@ module.exports = function (app) {
|
|
|
336
356
|
app.debug('beat and run angles are included')
|
|
337
357
|
} else if (row.includes('0')) {
|
|
338
358
|
// Beat / Run angle
|
|
339
|
-
let angle = Number(row[0])
|
|
359
|
+
let angle = degToRad(Number(row[0]))
|
|
340
360
|
let angleName
|
|
341
361
|
let VMGName
|
|
342
|
-
if (angle <
|
|
362
|
+
if (angle < halfPi) {
|
|
343
363
|
angleName = 'Beat angle'
|
|
344
364
|
VMGName = 'Beat VMG'
|
|
345
365
|
} else {
|
|
@@ -348,17 +368,18 @@ module.exports = function (app) {
|
|
|
348
368
|
}
|
|
349
369
|
for (let index = 0; index < row.length-1; index++) {
|
|
350
370
|
if (row[index+1] != 0) {
|
|
351
|
-
polar[index][angleName] =
|
|
352
|
-
|
|
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)
|
|
353
375
|
if (typeof polar[index]['twa'] == 'undefined') {
|
|
354
376
|
polar[index]['twa'] = []
|
|
355
377
|
}
|
|
356
|
-
let
|
|
357
|
-
let Obj = {"twa": degToRad(angle), "tbs": tbs}
|
|
378
|
+
let Obj = {"twa": angle, "tbs": tbs, "vmg": vmg}
|
|
358
379
|
polar[index]['twa'] = polar[index]['twa'].concat(Obj)
|
|
359
380
|
if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
|
|
360
381
|
polar[index]['Max speed'] = tbs
|
|
361
|
-
polar[index]['Max speed angle'] =
|
|
382
|
+
polar[index]['Max speed angle'] = angle
|
|
362
383
|
}
|
|
363
384
|
}
|
|
364
385
|
}
|
|
@@ -370,9 +391,15 @@ module.exports = function (app) {
|
|
|
370
391
|
polar[index].twa = []
|
|
371
392
|
}
|
|
372
393
|
let tbs = ktsToMs(Number(row[index+1]))
|
|
373
|
-
let
|
|
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))
|
|
374
397
|
polar[index]['twa'] = polar[index]['twa'].concat(Obj)
|
|
375
|
-
if
|
|
398
|
+
// See if we need to set/overwrite beat/run angle
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
// See if this a new Max speed
|
|
402
|
+
if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
|
|
376
403
|
polar[index]['Max speed'] = tbs
|
|
377
404
|
polar[index]['Max speed angle'] = angle
|
|
378
405
|
}
|
|
@@ -388,34 +415,68 @@ module.exports = function (app) {
|
|
|
388
415
|
tws.twa = twaArray
|
|
389
416
|
})
|
|
390
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
|
+
|
|
391
452
|
// And now fill in some missing ends to avoid doing expensive calculations in the main loop
|
|
392
453
|
for (let index = 0; index < polar.length-1; index++) {
|
|
393
454
|
// Sorted on angle, so first is lowest
|
|
455
|
+
let tws = polar[index].tws
|
|
394
456
|
let twaArray = polar[index].twa
|
|
395
457
|
let lowTBS = twaArray[0].tbs
|
|
396
458
|
let lowTWA = twaArray[0].twa
|
|
397
459
|
|
|
398
|
-
|
|
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))
|
|
399
461
|
|
|
400
462
|
// Now put some extra values at the beginning
|
|
401
463
|
var topArray = []
|
|
402
464
|
for (let angle = 0; angle < lowTWA; angle = angle + degToRad(5)) {
|
|
403
465
|
let tbs = (angle / lowTWA) * Math.pow(Math.cos((-1*lowTWA + angle)*3),3) * lowTBS
|
|
404
466
|
let Obj = {'twa': angle, 'tbs': tbs}
|
|
405
|
-
// app.debug('Adding Obj: %s', JSON.stringify(Obj))
|
|
406
467
|
topArray.push(Obj)
|
|
407
|
-
// app.debug('twaArray: %s', JSON.stringify(twaArray))
|
|
408
468
|
}
|
|
409
|
-
|
|
410
|
-
|
|
469
|
+
// app.debug('Adding values: %s', JSON.stringify(topArray))
|
|
470
|
+
polar[index].twa = topArray.concat(twaArray)
|
|
411
471
|
}
|
|
412
472
|
|
|
413
473
|
for (let index = 0; index < polar.length-1; index++) {
|
|
474
|
+
let tws = polar[index].tws
|
|
414
475
|
let twaArray = polar[index].twa
|
|
415
476
|
let highTBS = twaArray[twaArray.length-1].tbs
|
|
416
477
|
let highTWA = twaArray[twaArray.length-1].twa
|
|
417
478
|
|
|
418
|
-
|
|
479
|
+
app.debug('Padding polar for %s from highTWA to last given angle (%d, %d kts)', msToKts(tws).toFixed(0), highTWA, msToKts(highTBS))
|
|
419
480
|
|
|
420
481
|
// Now put some extra values at the beginning
|
|
421
482
|
var tailArray = []
|
|
@@ -424,7 +485,8 @@ module.exports = function (app) {
|
|
|
424
485
|
let Obj = {'twa': angle, 'tbs': tbs}
|
|
425
486
|
tailArray.unshift(Obj)
|
|
426
487
|
}
|
|
427
|
-
|
|
488
|
+
// app.debug('Adding values: %s', JSON.stringify(tailArray))
|
|
489
|
+
polar[index].twa = twaArray.concat(tailArray)
|
|
428
490
|
}
|
|
429
491
|
|
|
430
492
|
// app.debug(JSON.stringify(polar))
|