signalk-polar-performance-plugin 0.0.13 → 0.0.15

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-polar-performance-plugin",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "A plugin that calculates performance information based on a (CSV) polar diagram.",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
package/plugin/index.js CHANGED
@@ -18,11 +18,19 @@ 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)'
30
+ },
31
+ tackTrue: {
32
+ type: 'boolean',
33
+ title: 'Enable calculating Opposite Tack True (performance.tackTrue)'
26
34
  },
27
35
  optimumWindAngle: {
28
36
  type: 'boolean',
@@ -60,7 +68,7 @@ module.exports = function (app) {
60
68
  app.debug('polar: %s', JSON.stringify(polar))
61
69
 
62
70
  // Global variables
63
- var BSP, STW, TWA, TWS, port // Angles in rad, speed in m/s
71
+ var BSP, STW, TWA, targetTWA, TWS, HDG, port // Angles in rad, speed in m/s
64
72
  var halfPi = Math.PI / 2
65
73
 
66
74
  // Subscribe to paths
@@ -89,6 +97,12 @@ module.exports = function (app) {
89
97
  policy: 'instant'
90
98
  })
91
99
  }
100
+ if (options.tackTrue == true) {
101
+ localSubscription.subscribe.push({
102
+ path: 'navigation.headingTrue',
103
+ policy: 'instant'
104
+ })
105
+ }
92
106
 
93
107
  app.subscriptionmanager.subscribe(
94
108
  localSubscription,
@@ -116,6 +130,9 @@ module.exports = function (app) {
116
130
  SOG = delta.value
117
131
  BSP = SOG
118
132
  // app.debug('speedThroughWater (STW): %d', STW)
133
+ } else if (delta.path == 'navigation.headingTrue') {
134
+ HDG = delta.value
135
+ // app.debug('heading (HDG): %d', HDG)
119
136
  } else if (delta.path == 'environment.wind.speedTrue') {
120
137
  TWS = delta.value
121
138
  // app.debug('environment.wind.speedTrue (TWS): %d', TWS)
@@ -136,22 +153,34 @@ module.exports = function (app) {
136
153
  let values = []
137
154
  let metas = []
138
155
  if (typeof perfObj.beatAngle != 'undefined') {
139
- values.push({path: 'performance.beatAngle', value: roundDec(perfObj.beatAngle)})
140
- values.push({path: 'performance.targetAngle', value: roundDec(perfObj.beatAngle)})
156
+ if (options.beatAngle == true) {
157
+ values.push({path: 'performance.beatAngle', value: roundDec(perfObj.beatAngle)})
158
+ }
159
+ if (options.targetTWA == true) {
160
+ values.push({path: 'performance.targetAngle', value: roundDec(perfObj.beatAngle)})
161
+ }
141
162
  }
142
163
  if (typeof perfObj.beatVMG != 'undefined') {
143
164
  values.push({path: 'performance.beatAngleVelocityMadeGood', value: roundDec(perfObj.beatVMG)})
144
- values.push({path: 'performance.targetVelocityMadeGood', value: roundDec(perfObj.beatVMG)})
145
- metas.push({path: 'performance.targetVelocityMadeGood', value: {"units": "rad"}})
165
+ if (options.targetTWA == true) {
166
+ values.push({path: 'performance.targetVelocityMadeGood', value: roundDec(perfObj.beatVMG)})
167
+ metas.push({path: 'performance.targetVelocityMadeGood', value: {"units": "rad"}})
168
+ }
146
169
  }
147
170
  if (typeof perfObj.runAngle != 'undefined') {
148
- values.push({path: 'performance.gybeAngle', value: roundDec(perfObj.runAngle)})
149
- values.push({path: 'performance.targetAngle', value: roundDec(perfObj.runAngle)})
171
+ if (options.beatAngle == true) {
172
+ values.push({path: 'performance.gybeAngle', value: roundDec(perfObj.runAngle)})
173
+ }
174
+ if (options.targetTWA == true) {
175
+ values.push({path: 'performance.targetAngle', value: roundDec(perfObj.runAngle)})
176
+ }
150
177
  }
151
178
  if (typeof perfObj.runVMG != 'undefined') {
152
179
  values.push({path: 'performance.gybeAngleVelocityMadeGood', value: roundDec(perfObj.runVMG)})
153
- values.push({path: 'performance.targetVelocityMadeGood', value: roundDec(perfObj.runVMG)})
154
- metas.push({path: 'performance.targetVelocityMadeGood', value: {"units": "rad"}})
180
+ if (options.targetTWA == true) {
181
+ values.push({path: 'performance.targetVelocityMadeGood', value: roundDec(perfObj.runVMG)})
182
+ metas.push({path: 'performance.targetVelocityMadeGood', value: {"units": "rad"}})
183
+ }
155
184
  }
156
185
  if (typeof perfObj.optimumWindAngle != 'undefined') {
157
186
  values.push({path: 'performance.optimumWindAngle', value: roundDec(perfObj.optimumWindAngle)})
@@ -170,6 +199,11 @@ module.exports = function (app) {
170
199
  values.push({path: 'performance.maxSpeedAngle', value: roundDec(perfObj.maxSpeedAngle)})
171
200
  metas.push({path: 'performance.maxSpeedAngle', value: {"units": "rad"}})
172
201
  }
202
+ if (options.tackTrue == true) {
203
+ if (typeof perfObj.tackTrue != 'undefined') {
204
+ values.push({path: 'performance.tackTrue', value: roundDec(perfObj.tackTrue)})
205
+ }
206
+ }
173
207
 
174
208
  app.debug('sendUpdates: %s', JSON.stringify(values))
175
209
  app.handleMessage(plugin.id, {
@@ -197,13 +231,12 @@ module.exports = function (app) {
197
231
  // Calculate beat/run angle
198
232
  if (TWA < halfPi) {
199
233
  // app.debug('Upwind')
200
- if (options.beatAngle == true) {
201
- // Take beat angle
202
- if (typeof polar[indexTWS]['Beat angle'] != 'undefined') {
203
- let beatLower = polar[indexTWS]['Beat angle']
204
- let beatUpper = polar[indexTWS+1]['Beat angle']
205
- performance.beatAngle = beatLower + ((beatUpper - beatLower) * twsGapRatio)
206
- }
234
+ // Take beat angle
235
+ if (typeof polar[indexTWS]['Beat angle'] != 'undefined') {
236
+ let beatLower = polar[indexTWS]['Beat angle']
237
+ let beatUpper = polar[indexTWS+1]['Beat angle']
238
+ performance.beatAngle = beatLower + ((beatUpper - beatLower) * twsGapRatio)
239
+ targetTWA = performance.beatAngle
207
240
  }
208
241
  // Calculate optimum wind angle (B&G thing)
209
242
  if (options.optimumWindAngle == true) {
@@ -212,23 +245,20 @@ module.exports = function (app) {
212
245
  }
213
246
  }
214
247
  // Calculate beat VMG
215
- if (options.beatVMG == true) {
216
- if (typeof polar[indexTWS]['Beat VMG'] != 'undefined') {
217
- let VMGLower = polar[indexTWS]['Beat VMG']
218
- let VMGUpper = polar[indexTWS+1]['Beat VMG']
219
- performance.beatVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
220
- }
248
+ if (typeof polar[indexTWS]['Beat VMG'] != 'undefined') {
249
+ let VMGLower = polar[indexTWS]['Beat VMG']
250
+ let VMGUpper = polar[indexTWS+1]['Beat VMG']
251
+ performance.beatVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
221
252
  }
222
253
  } else {
223
254
  // app.debug('Downwind')
224
- if (options.beatAngle == true) {
225
- if (typeof polar[indexTWS]['Run angle'] != 'undefined') {
226
- // Calculate run angle
227
- let runLower = polar[indexTWS]['Run angle']
228
- let runUpper = polar[indexTWS+1]['Run angle']
229
- //app.debug('runLower: %s runUpper: %s', runLower, runUpper)
230
- performance.runAngle = runLower + ((runUpper - runLower) * twsGapRatio)
231
- }
255
+ if (typeof polar[indexTWS]['Run angle'] != 'undefined') {
256
+ // Calculate run angle
257
+ let runLower = polar[indexTWS]['Run angle']
258
+ let runUpper = polar[indexTWS+1]['Run angle']
259
+ //app.debug('runLower: %s runUpper: %s', runLower, runUpper)
260
+ performance.runAngle = runLower + ((runUpper - runLower) * twsGapRatio)
261
+ targetTWA = performance.runAngle
232
262
  }
233
263
  if (options.optimumWindAngle == true) {
234
264
  if (typeof performance.runAngle != 'undefined') {
@@ -236,16 +266,29 @@ module.exports = function (app) {
236
266
  performance.optimumWindAngle = (performance.runAngle - TWA) * port * -1
237
267
  }
238
268
  }
239
- if (options.beatVMG == true) {
240
- // Calculate run VMG
241
- if (typeof polar[indexTWS]['Run VMG'] != 'undefined') {
242
- let VMGLower = polar[indexTWS]['Run VMG']
243
- let VMGUpper = polar[indexTWS+1]['Run VMG']
244
- //app.debug('VMGLower: %s VMGUpper: %s', VMGLower, VMGUpper)
245
- performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
246
- }
269
+ // Calculate run VMG
270
+ if (typeof polar[indexTWS]['Run VMG'] != 'undefined') {
271
+ let VMGLower = polar[indexTWS]['Run VMG']
272
+ let VMGUpper = polar[indexTWS+1]['Run VMG']
273
+ //app.debug('VMGLower: %s VMGUpper: %s', VMGLower, VMGUpper)
274
+ performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
247
275
  }
248
276
  }
277
+ // Calculate opposite Tack True
278
+ // app.debug('tackTrue: port: %d HDG: %d targetTWA: %d', port, radToDeg(HDG), radToDeg(targetTWA))
279
+ if (port) {
280
+ let tackTrue = HDG - targetTWA
281
+ if (tackTrue < 0) {
282
+ tackTrue = tackTrue + (2*Math.PI)
283
+ }
284
+ performance.tackTrue = tackTrue
285
+ } else {
286
+ let tackTrue = HDG + targetTWA
287
+ if (tackTrue > (2*Math.PI)) {
288
+ tackTrue = tackTrue - (2*Math.PI)
289
+ }
290
+ performance.tackTrue = tackTrue
291
+ }
249
292
  // Calculate polar target boat speed
250
293
 
251
294
  // Interpolate Define the 4 near data points
@@ -348,10 +391,10 @@ module.exports = function (app) {
348
391
  app.debug('beat and run angles are included')
349
392
  } else if (row.includes('0')) {
350
393
  // Beat / Run angle
351
- let angle = Number(row[0])
394
+ let angle = degToRad(Number(row[0]))
352
395
  let angleName
353
396
  let VMGName
354
- if (angle < 90) {
397
+ if (angle < halfPi) {
355
398
  angleName = 'Beat angle'
356
399
  VMGName = 'Beat VMG'
357
400
  } else {
@@ -360,17 +403,18 @@ module.exports = function (app) {
360
403
  }
361
404
  for (let index = 0; index < row.length-1; index++) {
362
405
  if (row[index+1] != 0) {
363
- polar[index][angleName] = degToRad(angle)
364
- polar[index][VMGName] = roundDec(Number((row[index+1]) * Math.abs(Math.cos(angle))))
406
+ polar[index][angleName] = angle
407
+ let tbs = ktsToMs(Number(row[index+1]))
408
+ let vmg = tbs * Math.abs(Math.cos(angle))
409
+ polar[index][VMGName] = roundDec(vmg)
365
410
  if (typeof polar[index]['twa'] == 'undefined') {
366
411
  polar[index]['twa'] = []
367
412
  }
368
- let tbs = ktsToMs(Number(row[index+1]))
369
- let Obj = {"twa": degToRad(angle), "tbs": tbs}
413
+ let Obj = {"twa": angle, "tbs": tbs, "vmg": vmg}
370
414
  polar[index]['twa'] = polar[index]['twa'].concat(Obj)
371
415
  if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
372
416
  polar[index]['Max speed'] = tbs
373
- polar[index]['Max speed angle'] = degToRad(angle)
417
+ polar[index]['Max speed angle'] = angle
374
418
  }
375
419
  }
376
420
  }
@@ -382,9 +426,13 @@ module.exports = function (app) {
382
426
  polar[index].twa = []
383
427
  }
384
428
  let tbs = ktsToMs(Number(row[index+1]))
385
- let Obj = {"twa": angle, "tbs": tbs}
386
- app.debug('Adding Obj: %s', JSON.stringify(Obj))
429
+ let vmg = tbs * Math.abs(Math.cos(angle))
430
+ let Obj = {"twa": angle, "tbs": tbs, "vmg": vmg}
431
+ // app.debug('Adding Obj: %s', JSON.stringify(Obj))
387
432
  polar[index]['twa'] = polar[index]['twa'].concat(Obj)
433
+ // See if we need to set/overwrite beat/run angle
434
+
435
+
388
436
  // See if this a new Max speed
389
437
  if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
390
438
  polar[index]['Max speed'] = tbs
@@ -402,34 +450,68 @@ module.exports = function (app) {
402
450
  tws.twa = twaArray
403
451
  })
404
452
 
453
+ // Find the beat/run angle if not set yet.
454
+ if (typeof polar[0].beatAngle == 'undefined') {
455
+ for (let index = 0; index < polar.length-1; index++) {
456
+ let tws = polar[index].tws
457
+ let beatVMG = 0
458
+ let beatElement = 0
459
+ let runVMG = 0
460
+ let runElement = 0
461
+
462
+ let twaArray = polar[index].twa
463
+ for (let element = 0; element < twaArray.length-1; element++) {
464
+ let Obj = twaArray[element]
465
+ if (Obj.twa < halfPi) {
466
+ // Beat
467
+ if (Obj.vmg > beatVMG) {
468
+ beatElement = element
469
+ }
470
+ } else {
471
+ // Run
472
+ if (Obj.vmg > runVMG) {
473
+ runElement = element
474
+ }
475
+ }
476
+ }
477
+ app.debug('beatVMG for %d is %d (angel %d)', tws, twaArray[beatElement].vmg, twaArray[beatElement].twa)
478
+ app.debug(' runVMG for %d is %d (angel %d)', tws, twaArray[runElement].vmg, twaArray[runElement].twa)
479
+ polar[index]['Beat angle'] = twaArray[beatElement].twa
480
+ polar[index]['Beat VMG'] = twaArray[beatElement].vmg
481
+ polar[index]['Run angle'] = twaArray[runElement].twa
482
+ polar[index]['Run VMG'] = twaArray[runElement].vmg
483
+ }
484
+
485
+ }
486
+
405
487
  // And now fill in some missing ends to avoid doing expensive calculations in the main loop
406
488
  for (let index = 0; index < polar.length-1; index++) {
407
489
  // Sorted on angle, so first is lowest
490
+ let tws = polar[index].tws
408
491
  let twaArray = polar[index].twa
409
492
  let lowTBS = twaArray[0].tbs
410
493
  let lowTWA = twaArray[0].twa
411
494
 
412
- // app.debug('Padding polar from 0 to first given angle (%d deg, %d kts) to twaArray %s' , radToDeg(lowTWA), msToKts(lowTBS), JSON.stringify(twaArray))
495
+ 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
496
 
414
497
  // Now put some extra values at the beginning
415
498
  var topArray = []
416
499
  for (let angle = 0; angle < lowTWA; angle = angle + degToRad(5)) {
417
500
  let tbs = (angle / lowTWA) * Math.pow(Math.cos((-1*lowTWA + angle)*3),3) * lowTBS
418
501
  let Obj = {'twa': angle, 'tbs': tbs}
419
- // app.debug('Adding Obj: %s', JSON.stringify(Obj))
420
502
  topArray.push(Obj)
421
- // app.debug('twaArray: %s', JSON.stringify(twaArray))
422
503
  }
423
- twaArray = topArray.concat(twaArray)
424
- // app.debug('twaArray: %s', JSON.stringify(polar[index].twa))
504
+ // app.debug('Adding values: %s', JSON.stringify(topArray))
505
+ polar[index].twa = topArray.concat(twaArray)
425
506
  }
426
507
 
427
508
  for (let index = 0; index < polar.length-1; index++) {
509
+ let tws = polar[index].tws
428
510
  let twaArray = polar[index].twa
429
511
  let highTBS = twaArray[twaArray.length-1].tbs
430
512
  let highTWA = twaArray[twaArray.length-1].twa
431
513
 
432
- // app.debug('Padding polar from highTWA to last given angle (%d, %d kts) to twaArray %s' , highTWA, msToKts(highTBS), JSON.stringify(twaArray))
514
+ app.debug('Padding polar for %s from highTWA to last given angle (%d, %d kts)', msToKts(tws).toFixed(0), highTWA, msToKts(highTBS))
433
515
 
434
516
  // Now put some extra values at the beginning
435
517
  var tailArray = []
@@ -438,7 +520,8 @@ module.exports = function (app) {
438
520
  let Obj = {'twa': angle, 'tbs': tbs}
439
521
  tailArray.unshift(Obj)
440
522
  }
441
- twaArray = twaArray.concat(tailArray)
523
+ // app.debug('Adding values: %s', JSON.stringify(tailArray))
524
+ polar[index].twa = twaArray.concat(tailArray)
442
525
  }
443
526
 
444
527
  // app.debug(JSON.stringify(polar))