signalk-polar-performance-plugin 0.0.58 → 0.0.60

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-polar-performance-plugin",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
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
@@ -116,13 +116,25 @@ module.exports = function (app) {
116
116
  plugin.registerWithRouter = function (router) {
117
117
  // Will appear here; plugins/signalk-polar-performance-plugin/
118
118
  app.debug('registerWithRouter')
119
+ // Middleware to set Origin-Agent-Cluster on every response from this plugin
120
+ router.use((req, res, next) => {
121
+ res.set('Origin-Agent-Cluster', '?1')
122
+ next()
123
+ })
119
124
  router.get('/polar', (req, res) => {
120
125
  res.contentType('application/json')
121
126
  res.send(JSON.stringify(polar))
122
127
  })
123
128
  router.get('/chartData', (req, res) => {
124
- res.contentType('application/json')
125
- res.send(JSON.stringify(getChartData()))
129
+ const chart = getChartData();
130
+ if (!chart || !chart.datasets || chart.datasets.length === 0) {
131
+ return res.status(500).json({
132
+ error: "No polar data",
133
+ reason: "Polar CSV is empty or invalid. Please configure a valid polar in plugin settings."
134
+ })
135
+ }
136
+ res.contentType('application/json');
137
+ res.send(JSON.stringify(chart));
126
138
  })
127
139
  }
128
140
 
@@ -472,7 +484,7 @@ module.exports = function (app) {
472
484
  }
473
485
  // Calculate opposite Tack True
474
486
  // app.debug('tackTrue: port: %d HDG: %d targetTWA: %d', port, radToDeg(HDG), radToDeg(targetTWA))
475
- if (port) {
487
+ if (port < 0) {
476
488
  let tackTrue = HDG - targetTWA
477
489
  if (tackTrue < 0) {
478
490
  tackTrue = tackTrue + 2 * Math.PI
@@ -588,15 +600,39 @@ module.exports = function (app) {
588
600
  var polar = {}
589
601
  var beat = false
590
602
 
603
+ function upsertTwa (polarIndex, twaObj, angleDeg) {
604
+ const twaArray = polar[polarIndex].twa
605
+ const existingIndex = twaArray.findIndex(entry => entry.twa === twaObj.twa)
606
+ if (existingIndex >= 0) {
607
+ app.error(
608
+ 'Duplicate TWA %s deg for TWS %s kts, keeping last value',
609
+ angleDeg,
610
+ msToKts(polar[polarIndex].tws).toFixed(1)
611
+ )
612
+ twaArray[existingIndex] = twaObj
613
+ } else {
614
+ twaArray.push(twaObj)
615
+ }
616
+ }
617
+
591
618
  // Create array of arrays from csv
592
619
  csv.split('\n').forEach(row => {
593
- csvArray.push(row.split(';'))
620
+ const trimmedRow = row.trim()
621
+ if (!trimmedRow) {
622
+ return
623
+ }
624
+ const cells = trimmedRow.split(';').map(cell => cell.trim())
625
+ if (!cells[0]) {
626
+ return
627
+ }
628
+ csvArray.push(cells)
594
629
  })
595
630
  // app.debug('csvArray: %s', JSON.stringify(csvArray))
596
631
 
597
632
  // Populate the polar object from CSV rows
598
633
  csvArray.forEach(row => {
599
- if (row[0] == 'twa/tws') {
634
+ const rowKey = row[0].trim().toLowerCase()
635
+ if (rowKey == 'twa/tws') {
600
636
  // The row with TWS columns
601
637
  // Create empty TWS objects in polar object
602
638
  app.debug('First row with TWS columns')
@@ -609,7 +645,8 @@ module.exports = function (app) {
609
645
  } else if (row.filter(i => i === '0').length > 1) {
610
646
  app.debug('beat and run angles are included')
611
647
  // Beat / Run angle
612
- let angle = degToRad(Number(row[0]))
648
+ let angleDeg = Number(row[0])
649
+ let angle = degToRad(angleDeg)
613
650
  let angleName
614
651
  let VMGName
615
652
  app.debug('angle < halfPi %d < %d', angle, halfPi)
@@ -624,17 +661,23 @@ module.exports = function (app) {
624
661
  }
625
662
 
626
663
  for (let index = 0; index < row.length - 1; index++) {
627
- if (row[index + 1] != 0) {
664
+ if (!row[index + 1]) {
665
+ continue
666
+ }
667
+ const tbsCell = Number(row[index + 1])
668
+ if (Number.isNaN(tbsCell) || tbsCell === 0) {
669
+ continue
670
+ }
671
+ if (tbsCell != 0) {
628
672
  polar[index][angleName] = angle
629
- let tbs =
630
- ktsToMs(Number(row[index + 1])) * (options.perfAdjust || 1)
673
+ let tbs = ktsToMs(tbsCell) * (options.perfAdjust || 1)
631
674
  let vmg = tbs * Math.abs(Math.cos(angle))
632
675
  polar[index][VMGName] = roundDec(vmg)
633
676
  if (typeof polar[index]['twa'] == 'undefined') {
634
677
  polar[index]['twa'] = []
635
678
  }
636
679
  let Obj = { twa: angle, tbs: tbs, vmg: vmg }
637
- polar[index]['twa'] = polar[index]['twa'].concat(Obj)
680
+ upsertTwa(index, Obj, angleDeg)
638
681
  app.debug('Finding max speed')
639
682
  if (
640
683
  typeof polar[index]['Max speed'] == 'undefined' ||
@@ -648,17 +691,24 @@ module.exports = function (app) {
648
691
  }
649
692
  } else {
650
693
  // Normal line
651
- let angle = degToRad(Number(row[0])) // CSV deg to internal rad
694
+ let angleDeg = Number(row[0])
695
+ let angle = degToRad(angleDeg) // CSV deg to internal rad
652
696
  for (let index = 0; index < row.length - 1; index++) {
653
697
  if (typeof polar[index].twa == 'undefined') {
654
698
  polar[index].twa = []
655
699
  }
656
- let tbs =
657
- ktsToMs(Number(row[index + 1])) * (options.perfAdjust || 1)
700
+ if (!row[index + 1]) {
701
+ continue
702
+ }
703
+ const tbsCell = Number(row[index + 1])
704
+ if (Number.isNaN(tbsCell)) {
705
+ continue
706
+ }
707
+ let tbs = ktsToMs(tbsCell) * (options.perfAdjust || 1)
658
708
  let vmg = tbs * Math.abs(Math.cos(angle))
659
709
  let Obj = { twa: angle, tbs: tbs, vmg: vmg }
660
710
  // app.debug('Adding Obj: %s', JSON.stringify(Obj))
661
- polar[index]['twa'] = polar[index]['twa'].concat(Obj)
711
+ upsertTwa(index, Obj, angleDeg)
662
712
  // See if we need to set/overwrite beat/run angle
663
713
 
664
714
  // See if this a new Max speed
package/public/index.html CHANGED
@@ -35,6 +35,26 @@
35
35
  padding: 4px;
36
36
  font-size: 14px;
37
37
  }
38
+
39
+ #error-banner {
40
+ position: fixed;
41
+ bottom: 0;
42
+ left: 0;
43
+ right: 0;
44
+ background: rgba(220, 60, 60, 0.95);
45
+ color: white;
46
+ text-align: center;
47
+ padding: 14px 20px;
48
+ font-size: 15px;
49
+ z-index: 1000;
50
+ box-shadow: 0 -2px 8px rgba(0,0,0,0.3);
51
+ display: none;
52
+ }
53
+
54
+ #error-banner.show {
55
+ display: block;
56
+ }
57
+
38
58
  </style>
39
59
  </head>
40
60
  <body>
@@ -47,12 +67,17 @@
47
67
  <label for="labelPos">Place polar wind speed label at TWA:</label>
48
68
  <input type="number" id="labelPos" value="135" min="0" max="180" onchange="updateLabelPos(this.value)">
49
69
  </div>
70
+
71
+ <div id="error-banner"></div>
50
72
  </div>
51
73
 
52
74
  <script>
53
75
 
54
- var polarSpeed, boatSpeed, TWA, TWS
55
76
  var myChart, chartData
77
+ let ws
78
+ let polarSpeed = 0, boatSpeed = 0, TWA = 0, TWS = 0
79
+ let dataReceived = { polar: false, boatSpeed: false, twa: false }
80
+ let dataTimeout
56
81
 
57
82
  // GLOBAL CONFIG: Load from storage or default to 135
58
83
  window.labelTWA = localStorage.getItem('polarLabelTWA') || 135;
@@ -143,41 +168,80 @@ Chart.plugins.register({
143
168
  }
144
169
  });
145
170
 
146
- $.getJSON("/plugins/signalk-polar-performance-plugin/chartData", function(json) {
147
-
148
- // 1. Add actual values dots to the BEGINNING (Indices 0 and 1)
149
- json.datasets.unshift({
150
- label: 'Polar Speed',
151
- backgroundColor: 'rgba(0,255,0,0.7)',
152
- borderColor: 'rgba(0,255,0,0.2)',
153
- borderWidth: 1,
154
- radius: 10,
155
- pointStyle: 'circle',
156
- data: [{ y: 0, x: 0, r: 10 }]
157
- },
158
- {
159
- label: 'Boat Speed',
160
- backgroundColor: 'rgba(0,150,255,0.7)',
161
- borderColor: 'rgba(0,150,255,0.2)',
162
- borderWidth: 1,
163
- radius: 10,
164
- pointStyle: 'circle',
165
- data: [{ y: 0, x: 0, r: 10 }]
166
- })
171
+ $.getJSON("/plugins/signalk-polar-performance-plugin/chartData")
172
+ .done(function(json) {
173
+ if (!json || !json.datasets || json.datasets.length < 3) {
174
+ showError("No polar diagram data available.<br><br>Make sure you have entered a valid polar CSV in the plugin settings.");
175
+ return;
176
+ }
167
177
 
168
- // 2. BACKUP ORIGINAL COLORS
169
- for (var i = 2; i < json.datasets.length; i++) {
178
+ // Add actual values dots (Polar Speed + Boat Speed)
179
+ json.datasets.unshift({
180
+ label: 'Polar Speed',
181
+ backgroundColor: 'rgba(0,255,0,0.7)',
182
+ borderColor: 'rgba(0,255,0,0.2)',
183
+ borderWidth: 1,
184
+ radius: 10,
185
+ pointStyle: 'circle',
186
+ data: [{ y: 0, x: 0, r: 10 }]
187
+ },
188
+ {
189
+ label: 'Boat Speed',
190
+ backgroundColor: 'rgba(0,150,255,0.7)',
191
+ borderColor: 'rgba(0,150,255,0.2)',
192
+ borderWidth: 1,
193
+ radius: 10,
194
+ pointStyle: 'circle',
195
+ data: [{ y: 0, x: 0, r: 10 }]
196
+ });
197
+
198
+ // Backup original colors
199
+ for (var i = 2; i < json.datasets.length; i++) {
170
200
  if (!json.datasets[i].borderColor) {
171
- json.datasets[i].borderColor = 'rgba(100, 100, 100, 0.5)';
201
+ json.datasets[i].borderColor = 'rgba(100, 100, 100, 0.5)';
172
202
  }
173
203
  json.datasets[i]._originalColor = json.datasets[i].borderColor;
174
204
  json.datasets[i].borderWidth = 1;
175
- }
205
+ }
176
206
 
177
- chartData = json
178
- showChart(chartData)
179
- })
207
+ chartData = json;
208
+ showChart(chartData);
209
+ })
210
+ .fail(function(jqXHR, textStatus, errorThrown) {
211
+ let reason = "Unknown error";
212
+
213
+ if (jqXHR.status === 404) {
214
+ reason = "Plugin endpoint not found — is the plugin enabled?";
215
+ } else if (jqXHR.status === 500) {
216
+ reason = "Server error — check plugin configuration (invalid CSV polar?)";
217
+ } else if (textStatus === "parsererror") {
218
+ reason = "Invalid data received from server";
219
+ }
220
+
221
+ showError(`Failed to load polar data.<br><br>Reason: ${reason}`);
222
+ });
180
223
 
224
+ // New error display function
225
+ function showError(message) {
226
+ let banner = document.getElementById('error-banner');
227
+
228
+ if (!banner) {
229
+ // Fallback: create banner dynamically if missing
230
+ banner = document.createElement('div');
231
+ banner.id = 'error-banner';
232
+ document.body.appendChild(banner);
233
+ }
234
+
235
+ banner.innerHTML = `
236
+ ⚠️ ${message}
237
+ &nbsp;&nbsp;
238
+ <button onclick="location.reload()" style="background:white; color:#c33; border:none; padding:4px 10px; border-radius:4px; cursor:pointer;">
239
+ Reload
240
+ </button>
241
+ `;
242
+
243
+ banner.classList.add('show');
244
+ }
181
245
  function showChart (data) {
182
246
  console.log(data)
183
247
  myChart = new Chart("myChart", {
@@ -293,6 +357,11 @@ function startListeners () {
293
357
  }
294
358
 
295
359
  ws.send(JSON.stringify(subscriptionObject));
360
+
361
+ // Start timeout to detect missing data
362
+ dataReceived = { polar: false, boatSpeed: false, twa: false, tws: false };
363
+ clearTimeout(dataTimeout);
364
+ dataTimeout = setTimeout(checkDataReceived, 20000);
296
365
  }
297
366
 
298
367
  function handleData (data) {
@@ -304,18 +373,51 @@ function handleData (data) {
304
373
  if (path == 'performance.polarSpeed') {
305
374
  polarSpeed = roundDec(msToKts(value), 1)
306
375
  chartData.datasets[0].data[0].y = polarSpeed
307
- } else if (path == 'environment.wind.angleTrueWaterDamped') {
376
+ dataReceived.polar = true;
377
+ } else if (path === 'environment.wind.angleTrueWaterDamped') {
308
378
  TWA = roundDec(Math.abs(radToDeg(value)),1)
309
379
  chartData.datasets[0].data[0].x = TWA
310
380
  chartData.datasets[1].data[0].x = TWA
381
+ dataReceived.twa = true;
311
382
  } else if (path == 'performance.boatSpeedDamped') {
312
383
  boatSpeed = roundDec(msToKts(value),1)
313
384
  chartData.datasets[1].data[0].y = boatSpeed
385
+ dataReceived.boatSpeed = true;
314
386
  } else if (path == 'environment.wind.speedTrue') {
315
387
  TWS = msToKts(value);
388
+ dataReceived.tws = true;
316
389
  }
317
390
  }
318
391
 
392
+ function checkDataReceived() {
393
+ let missing = [];
394
+
395
+ if (!dataReceived.polar) missing.push("Polar Speed (performance.polarSpeed)");
396
+ if (!dataReceived.boatSpeed) missing.push("Boat Speed (performance.boatSpeedDamped)");
397
+ if (!dataReceived.twa) missing.push("True Wind Angle (environment.wind.angleTrueWater)");
398
+ if (!dataReceived.tws) missing.push("True Wind Speed (environment.wind.speedTrue)");
399
+
400
+ if (missing.length > 0) {
401
+ showError(`Missing data: ${missing.join(', ')}. Check your instruments and polar settings.`);
402
+ }
403
+ }
404
+
405
+ // Enhanced error display
406
+ function showError(message) {
407
+ const container = document.querySelector('.chart-container');
408
+ container.innerHTML = `
409
+ <div style="padding: 40px 20px; text-align: center; color: #ff7777;">
410
+ <h2 style="color: #ff6666;">⚠️ Cannot display Polar Performance</h2>
411
+ <div style="margin: 25px 0; line-height: 1.6; font-size: 17px;">
412
+ ${message}
413
+ </div>
414
+ <button onclick="location.reload()"
415
+ style="padding: 12px 24px; font-size: 16px; background: #333; color: white; border: 1px solid #666; border-radius: 6px; cursor: pointer;">
416
+ Reload Page
417
+ </button>
418
+ </div>
419
+ `;
420
+ }
319
421
 
320
422
  connect()
321
423