signalk-polar-performance-plugin 0.0.10 → 0.0.13

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
@@ -33,11 +33,13 @@ In the plugin configuration you can toggle the following options:
33
33
  - Polar Speed Ratio (performance.polarSpeedRatio)
34
34
  - Plugin option to use SOG as boat speed
35
35
  - Wind angle for maximum speed at this wind speed
36
+ - Fill up the ends of the polar diagram
36
37
 
37
38
  ### To-do list
38
- - Fill up the ends of the polar diagram
39
39
  - Improved interpolation
40
40
  - Make moment to do calculation smarter/configurable
41
+ - API to see JSON of polar
42
+ - Visualisation of the polar diagram
41
43
  - Create polar from live data
42
44
  -- Save polar info to file
43
45
  -- Save new record speed for angle in polar
@@ -46,7 +48,6 @@ In the plugin configuration you can toggle the following options:
46
48
  -- Configure the resolution of the polar diagram
47
49
  - Support multiple polar diagrams
48
50
  - Capture heel in polar diagram
49
- - Visualisation of the polar diagram
50
51
 
51
52
  ## MFD configuration
52
53
 
@@ -59,15 +60,15 @@ To get the values calculated by this plugin from SignalK to your B&G MFD/Triton2
59
60
  To see lay lines you need to set:
60
61
  - Settings -> Chart -> Laylines -> Targets... -> True wind angle to 'Actual'
61
62
 
62
- ![](doc/BandG_Laylines_Target_TWA_to_Active.png)
63
+ ![](https://raw.githubusercontent.com/htool/signalk-polar-performance-plugin/main/doc/BandG_Laylines_Target_TWA_to_Active.png)
63
64
 
64
65
  - SailSteer screen -> Long press tile to add 'Performance -> Target TWA -> decollapse, choose SignalK'
65
66
 
66
- ![](doc/BandG_Target_TWA_to_SignalK.png)
67
+ ![](https://raw.githubusercontent.com/htool/signalk-polar-performance-plugin/main/doc/BandG_Target_TWA_to_SignalK.png)
67
68
 
68
69
  Now the Target TWA is coming from SignalK and the laylines will be drawn based on it's value.
69
70
 
70
- ![](doc/BandG_Sailsteer_with_laylines.png)
71
+ ![](https://raw.githubusercontent.com/htool/signalk-polar-performance-plugin/main/doc/BandG_Sailsteer_with_laylines.png)
71
72
 
72
73
  ### Raymarine
73
74
  If you have a Raymarine MFD and can tell more about this, please add to the README or tell me.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-polar-performance-plugin",
3
- "version": "0.0.10",
3
+ "version": "0.0.13",
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
@@ -61,8 +61,7 @@ module.exports = function (app) {
61
61
 
62
62
  // Global variables
63
63
  var BSP, STW, TWA, TWS, port // Angles in rad, speed in m/s
64
- var Pi = Math.PI
65
- var halfPi = Pi / 2
64
+ var halfPi = Math.PI / 2
66
65
 
67
66
  // Subscribe to paths
68
67
  let localSubscription = {
@@ -195,79 +194,106 @@ module.exports = function (app) {
195
194
  let gap = upper - lower
196
195
  let twsGapRatio = (1 / gap) * (TWS - lower)
197
196
  //app.debug('twsGapRatio: %d', twsGapRatio)
198
- // Calculate beat/run angle
197
+ // Calculate beat/run angle
199
198
  if (TWA < halfPi) {
200
199
  // app.debug('Upwind')
201
200
  if (options.beatAngle == true) {
202
- // Calculate beat angle
203
- let beatLower = polar[indexTWS]['Beat angle']
204
- let beatUpper = polar[indexTWS+1]['Beat angle']
205
- performance.beatAngle = beatLower + ((beatUpper - beatLower) * twsGapRatio)
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
+ }
206
207
  }
207
208
  // Calculate optimum wind angle (B&G thing)
208
209
  if (options.optimumWindAngle == true) {
209
- performance.optimumWindAngle = (TWA - performance.beatAngle) * port
210
+ if (typeof performance.beatAngle != 'undefined') {
211
+ performance.optimumWindAngle = (TWA - performance.beatAngle) * port
212
+ }
210
213
  }
211
214
  // Calculate beat VMG
212
215
  if (options.beatVMG == true) {
213
- let VMGLower = polar[indexTWS]['Beat VMG']
214
- let VMGUpper = polar[indexTWS+1]['Beat VMG']
215
- performance.beatVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
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
+ }
216
221
  }
217
222
  } else {
218
223
  // app.debug('Downwind')
219
224
  if (options.beatAngle == true) {
220
- // Calculate run angle
221
- let runLower = polar[indexTWS]['Run angle']
222
- let runUpper = polar[indexTWS+1]['Run angle']
223
- //app.debug('runLower: %s runUpper: %s', runLower, runUpper)
224
- performance.runAngle = runLower + ((runUpper - runLower) * twsGapRatio)
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
+ }
225
232
  }
226
233
  if (options.optimumWindAngle == true) {
227
- // Calculate optimum wind angle
228
- performance.optimumWindAngle = (performance.runAngle - TWA) * port * -1
234
+ if (typeof performance.runAngle != 'undefined') {
235
+ // Calculate optimum wind angle
236
+ performance.optimumWindAngle = (performance.runAngle - TWA) * port * -1
237
+ }
229
238
  }
230
239
  if (options.beatVMG == true) {
231
240
  // Calculate run VMG
232
- let VMGLower = polar[indexTWS]['Run VMG']
233
- let VMGUpper = polar[indexTWS+1]['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']
234
244
  //app.debug('VMGLower: %s VMGUpper: %s', VMGLower, VMGUpper)
235
- performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
245
+ performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
246
+ }
236
247
  }
237
248
  }
238
249
  // Calculate polar target boat speed
239
250
 
240
- // Define the 4 near data points
251
+ // Interpolate Define the 4 near data points
241
252
  let lowerTWA = polar[indexTWS].twa
242
253
  let upperTWA = polar[indexTWS+1].twa
243
254
  // app.debug('TWAlower: %s', JSON.stringify(TWAlower))
255
+ // First find lowerTWA
244
256
  for (let indexTWA = 0 ; indexTWA < lowerTWA.length-1; indexTWA++) {
245
257
  let lowerTWAlower = lowerTWA[indexTWA]
246
258
  let lowerTWAupper = lowerTWA[indexTWA+1]
247
- let upperTWAlower = upperTWA[indexTWA]
248
- let upperTWAupper = upperTWA[indexTWA+1]
249
-
250
259
  // app.debug('lowerTWAlower: %s lowerTWAupper: %s', lowerTWAlower, lowerTWAupper)
251
260
  if (TWA >= lowerTWAlower.twa && TWA <= lowerTWAupper.twa) {
252
- // app.debug('lowerTWAlower: %d TWA: %d lowerTWAupper: %d', lowerTWAlower.twa, TWA, lowerTWAupper.twa)
253
- // Calculate gap ratio
254
- let gap = lowerTWAupper.twa - lowerTWAlower.twa
255
- let twaGapRatio = (1 / gap) * (TWA - lowerTWAlower.twa)
256
- // app.debug('twaGapRatio: %d', twaGapRatio)
257
- // Calculate lower tws boat speed
258
- let lowerTBS = lowerTWAlower.tbs + ((lowerTWAupper.tbs - lowerTWAlower.tbs) * twaGapRatio)
259
- // Calculate upper tws boat speed
260
- let upperTBS = upperTWAlower.tbs + ((upperTWAupper.tbs - upperTWAlower.tbs) * twaGapRatio)
261
-
262
- // Calculate polar performance
263
- performance.polarSpeed = lowerTBS + ((upperTBS - lowerTBS) * twsGapRatio)
264
- // app.debug('lowerTBS: %d TBS: %d upperTBS: %d', lowerTBS, performance.polarSpeed, upperTBS)
265
-
266
- // Calculate polar performance ratio
267
- performance.polarSpeedRatio = (1 / performance.polarSpeed) * BSP
261
+ // Now find upperTWA
262
+ for (let indexTWA = 0 ; indexTWA < upperTWA.length-1; indexTWA++) {
263
+ let upperTWAlower = upperTWA[indexTWA]
264
+ let upperTWAupper = upperTWA[indexTWA+1]
265
+ if (TWA >= upperTWAlower.twa && TWA <= upperTWAupper.twa) {
266
+ // Found the 4 points
267
+ // app.debug('lowerTWAlower: %d TWA: %d lowerTWAupper: %d', lowerTWAlower.twa, TWA, lowerTWAupper.twa)
268
+ // Calculate gap ratio
269
+ let gap = lowerTWAupper.twa - lowerTWAlower.twa
270
+ let twaGapRatio = (1 / gap) * (TWA - lowerTWAlower.twa)
271
+ // app.debug('twaGapRatio: %d', twaGapRatio)
272
+ // Calculate lower tws boat speed
273
+ let lowerTBS = lowerTWAlower.tbs + ((lowerTWAupper.tbs - lowerTWAlower.tbs) * twaGapRatio)
274
+ // Calculate upper tws boat speed
275
+ let upperTBS = upperTWAlower.tbs + ((upperTWAupper.tbs - upperTWAlower.tbs) * twaGapRatio)
276
+ // Calculate polar boat speed
277
+ performance.polarSpeed = lowerTBS + ((upperTBS - lowerTBS) * twsGapRatio)
278
+ // app.debug('lowerTBS: %d TBS: %d upperTBS: %d', lowerTBS, performance.polarSpeed, upperTBS)
279
+ break
280
+ }
281
+ }
268
282
  break
269
283
  }
270
284
  }
285
+ if (typeof performance.polarSpeed == 'undefined') {
286
+ if (TWA < performance.beatAngle) {
287
+ // In case TWA < lowest polar angle
288
+ app.debug('Low angle %d not present in table', radToDeg(TWA))
289
+ // performance.polarSpeed =
290
+ } else if (TWA > performance.runAngle) {
291
+ // In case TWA > highest polar angle
292
+ app.debug('High angle %d not present in table', radToDeg(TWA))
293
+ // performance.polarSpeed =
294
+ }
295
+ }
296
+
271
297
  if (options.maxSpeed == true) {
272
298
  let lowerMax = polar[indexTWS]['Max speed']
273
299
  let upperMax = polar[indexTWS+1]['Max speed']
@@ -278,10 +304,20 @@ module.exports = function (app) {
278
304
  let maxSpeedAngle = lowerMaxAngle + ((upperMaxAngle - lowerMaxAngle) * twsGapRatio)
279
305
  performance.maxSpeedAngle = maxSpeedAngle
280
306
  }
281
- } else {
282
- // app.debug('%d not >= %d && <= %d', TWS, lower, upper)
307
+ } else if (indexTWS == 0 && TWS < lower) {
308
+ app.debug('No data for low wind speed (%d kts)', msToKts(TWS))
309
+ } else if (indexTWS == polar.length-1 && TWS > upper) {
310
+ app.debug('No data for high wind speed (%d kts)', msToKts(TWS))
283
311
  }
284
312
  }
313
+ // Calculate polar performance ratio
314
+ if (typeof performance.polarSpeed != 'undefined') {
315
+ performance.polarSpeedRatio = (1 / performance.polarSpeed) * BSP
316
+ } else {
317
+ // No value would create stale values
318
+ performance.polarSpeed = 0
319
+ performance.polarSpeedRatio = 0
320
+ }
285
321
  return performance
286
322
  }
287
323
 
@@ -322,35 +358,37 @@ module.exports = function (app) {
322
358
  angleName = 'Run angle'
323
359
  VMGName = 'Run VMG'
324
360
  }
325
- for (let index = 1; index < row.length; index++) {
326
- if (row[index] != 0) {
327
- polar[index-1][angleName] = degToRad(angle)
328
- polar[index-1][VMGName] = roundDec(Number((row[index]) * Math.abs(Math.cos(angle))))
329
- if (typeof polar[index-1]['twa'] == 'undefined') {
330
- polar[index-1]['twa'] = []
361
+ for (let index = 0; index < row.length-1; index++) {
362
+ 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))))
365
+ if (typeof polar[index]['twa'] == 'undefined') {
366
+ polar[index]['twa'] = []
331
367
  }
332
- let tbs = ktsToMs(Number(row[index]))
368
+ let tbs = ktsToMs(Number(row[index+1]))
333
369
  let Obj = {"twa": degToRad(angle), "tbs": tbs}
334
- polar[index-1]['twa'] = polar[index-1]['twa'].concat(Obj)
335
- if (typeof polar[index-1]['Max speed'] == 'undefined' || tbs > polar[index-1]['Max speed']) {
336
- polar[index-1]['Max speed'] = tbs
337
- polar[index-1]['Max speed angle'] = degToRad(angle)
370
+ polar[index]['twa'] = polar[index]['twa'].concat(Obj)
371
+ if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
372
+ polar[index]['Max speed'] = tbs
373
+ polar[index]['Max speed angle'] = degToRad(angle)
338
374
  }
339
375
  }
340
376
  }
341
377
  } else {
342
378
  // Normal line
343
379
  let angle = degToRad(Number(row[0])) // CSV deg to internal rad
344
- for (let index = 1; index < row.length; index++) {
345
- if (typeof polar[index-1].twa == 'undefined') {
346
- polar[index-1].twa = []
380
+ for (let index = 0; index < row.length-1; index++) {
381
+ if (typeof polar[index].twa == 'undefined') {
382
+ polar[index].twa = []
347
383
  }
348
- let tbs = ktsToMs(Number(row[index]))
384
+ let tbs = ktsToMs(Number(row[index+1]))
349
385
  let Obj = {"twa": angle, "tbs": tbs}
350
- polar[index-1]['twa'] = polar[index-1]['twa'].concat(Obj)
351
- if (tbs > polar[index-1]['Max speed']) {
352
- polar[index-1]['Max speed'] = tbs
353
- polar[index-1]['Max speed angle'] = angle
386
+ app.debug('Adding Obj: %s', JSON.stringify(Obj))
387
+ polar[index]['twa'] = polar[index]['twa'].concat(Obj)
388
+ // See if this a new Max speed
389
+ if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
390
+ polar[index]['Max speed'] = tbs
391
+ polar[index]['Max speed angle'] = angle
354
392
  }
355
393
  }
356
394
  }
@@ -364,14 +402,50 @@ module.exports = function (app) {
364
402
  tws.twa = twaArray
365
403
  })
366
404
 
405
+ // And now fill in some missing ends to avoid doing expensive calculations in the main loop
406
+ for (let index = 0; index < polar.length-1; index++) {
407
+ // Sorted on angle, so first is lowest
408
+ let twaArray = polar[index].twa
409
+ let lowTBS = twaArray[0].tbs
410
+ let lowTWA = twaArray[0].twa
411
+
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))
413
+
414
+ // Now put some extra values at the beginning
415
+ var topArray = []
416
+ for (let angle = 0; angle < lowTWA; angle = angle + degToRad(5)) {
417
+ let tbs = (angle / lowTWA) * Math.pow(Math.cos((-1*lowTWA + angle)*3),3) * lowTBS
418
+ let Obj = {'twa': angle, 'tbs': tbs}
419
+ // app.debug('Adding Obj: %s', JSON.stringify(Obj))
420
+ topArray.push(Obj)
421
+ // app.debug('twaArray: %s', JSON.stringify(twaArray))
422
+ }
423
+ twaArray = topArray.concat(twaArray)
424
+ // app.debug('twaArray: %s', JSON.stringify(polar[index].twa))
425
+ }
426
+
427
+ for (let index = 0; index < polar.length-1; index++) {
428
+ let twaArray = polar[index].twa
429
+ let highTBS = twaArray[twaArray.length-1].tbs
430
+ let highTWA = twaArray[twaArray.length-1].twa
431
+
432
+ // app.debug('Padding polar from highTWA to last given angle (%d, %d kts) to twaArray %s' , highTWA, msToKts(highTBS), JSON.stringify(twaArray))
433
+
434
+ // Now put some extra values at the beginning
435
+ var tailArray = []
436
+ for (let angle = Math.PI; angle > highTWA; angle = angle - degToRad(5)) {
437
+ let tbs = Math.pow(highTWA/angle, 2) * highTBS
438
+ let Obj = {'twa': angle, 'tbs': tbs}
439
+ tailArray.unshift(Obj)
440
+ }
441
+ twaArray = twaArray.concat(tailArray)
442
+ }
443
+
367
444
  // app.debug(JSON.stringify(polar))
368
445
  return (polar)
369
446
  }
370
-
371
-
372
447
  }
373
448
 
374
-
375
449
  plugin.stop = function () {
376
450
  // Here we put logic we need when the plugin stops
377
451
  app.debug('Plugin stopped');