signalk-polar-performance-plugin 0.0.55 → 0.0.57

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.55",
3
+ "version": "0.0.57",
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
@@ -283,7 +283,7 @@ module.exports = function (app) {
283
283
  addValue('performance.gybeAngleVelocityMadeGood', perfObj.runVMG)
284
284
  if (options.targetTWA === true) {
285
285
  addValue('performance.targetVelocityMadeGood', perfObj.runVMG, {
286
- units: 'rad'
286
+ units: 'm/s'
287
287
  })
288
288
  }
289
289
  }
package/public/index.html CHANGED
@@ -1,51 +1,179 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
- <script type='text/javascript' src="./Chart.min.js"></script>
4
- <script type='text/javascript' src='/jquery/dist/jquery.min.js'></script>
5
- <title>Polar performance</title>
6
- <body style="background-color:#020202;">
3
+ <head>
4
+ <script type='text/javascript' src="./Chart.min.js"></script>
5
+ <script type='text/javascript' src="./jquery-3.7.1-min.js"></script>
6
+ <title>Polar performance</title>
7
+ <style>
8
+ body { background-color: #020202; margin: 0; padding: 0; color: white; font-family: Arial, sans-serif; }
9
+
10
+ .chart-container {
11
+ width: 100%;
12
+ max-width: 1200px;
13
+ margin: 0 auto; /* Center the whole thing */
14
+ }
15
+
16
+ /* New Toolbar Styling - sits below the chart */
17
+ .config-toolbar {
18
+ display: flex;
19
+ justify-content: flex-end; /* Aligns content to the right */
20
+ align-items: center;
21
+ padding: 10px 20px;
22
+ background-color: #0a0a0a; /* Slightly lighter than background */
23
+ border-top: 1px solid #222;
24
+ }
25
+
26
+ .config-toolbar label { font-size: 14px; color: #ccc; margin-right: 8px; }
27
+
28
+ .config-toolbar input {
29
+ width: 50px;
30
+ background: rgba(0, 0, 0, 0.5);
31
+ border: 1px solid #555;
32
+ color: white;
33
+ text-align: center;
34
+ border-radius: 2px;
35
+ padding: 4px;
36
+ font-size: 14px;
37
+ }
38
+ </style>
39
+ </head>
40
+ <body>
7
41
 
8
- <canvas id="myChart" style="width:100%;max-width:1200px"></canvas>
42
+ <div class="chart-container">
43
+ <canvas id="myChart"></canvas>
44
+
45
+ <!-- Configuration Toolbar (Now below the chart) -->
46
+ <div class="config-toolbar">
47
+ <label for="labelPos">Place polar wind speed label at TWA:</label>
48
+ <input type="number" id="labelPos" value="135" min="0" max="180" onchange="updateLabelPos(this.value)">
49
+ </div>
50
+ </div>
9
51
 
10
52
  <script>
11
53
 
12
- var polarSpeed, boatSpeed, TWA
54
+ var polarSpeed, boatSpeed, TWA, TWS
13
55
  var myChart, chartData
14
56
 
57
+ // GLOBAL CONFIG: Load from storage or default to 135
58
+ window.labelTWA = localStorage.getItem('polarLabelTWA') || 135;
59
+ document.getElementById('labelPos').value = window.labelTWA;
60
+
61
+ function updateLabelPos(val) {
62
+ window.labelTWA = val;
63
+ localStorage.setItem('polarLabelTWA', val);
64
+ if (myChart) myChart.update();
65
+ }
66
+
67
+ // --- CUSTOM PLUGIN: DRAW LABELS ---
68
+ Chart.plugins.register({
69
+ afterDraw: function(chartInstance) {
70
+ try {
71
+ var ctx = chartInstance.chart.ctx;
72
+ var targetAngle = parseFloat(window.labelTWA) || 135;
73
+
74
+ // 1. DRAW LABELS
75
+ chartInstance.data.datasets.forEach(function(dataset, i) {
76
+ if (i < 2) return;
77
+
78
+ var meta = chartInstance.getDatasetMeta(i);
79
+ if (meta.hidden) return;
80
+
81
+ var p1 = null, p2 = null;
82
+ var p1Meta = null, p2Meta = null;
83
+
84
+ // Find interpolation bracket
85
+ for (var j = 0; j < dataset.data.length - 1; j++) {
86
+ if (dataset.data[j].x <= targetAngle && dataset.data[j+1].x >= targetAngle) {
87
+ p1 = dataset.data[j];
88
+ p2 = dataset.data[j+1];
89
+ p1Meta = meta.data[j]._model;
90
+ p2Meta = meta.data[j+1]._model;
91
+ break;
92
+ }
93
+ }
94
+
95
+ var x, y;
96
+ if (p1 && p2) {
97
+ var ratio = (p2.x - p1.x === 0) ? 0 : (targetAngle - p1.x) / (p2.x - p1.x);
98
+ x = p1Meta.x + (p2Meta.x - p1Meta.x) * ratio;
99
+ y = p1Meta.y + (p2Meta.y - p1Meta.y) * ratio;
100
+ } else {
101
+ // Fallback
102
+ var closest = null, minDiff = 100;
103
+ dataset.data.forEach(function(d, idx) {
104
+ var diff = Math.abs(d.x - targetAngle);
105
+ if (diff < minDiff) { minDiff = diff; closest = meta.data[idx]._model; }
106
+ });
107
+ if (closest) { x = closest.x; y = closest.y; }
108
+ }
109
+
110
+ // Draw only if on screen and reasonably close to request
111
+ if (x > 0 && y > 0) {
112
+ ctx.save();
113
+ ctx.font = "bold 14px Arial";
114
+ ctx.textAlign = "center";
115
+ ctx.textBaseline = "middle";
116
+ var labelText = String(dataset.label);
117
+
118
+ // Box
119
+ var textWidth = ctx.measureText(labelText).width;
120
+ ctx.fillStyle = "rgba(2,2,2,0.9)";
121
+ ctx.fillRect(x - (textWidth/2) - 3, y - 9, textWidth + 6, 18);
122
+
123
+ // Text
124
+ ctx.fillStyle = dataset.borderColor || '#ffffff';
125
+ ctx.fillText(labelText, x, y);
126
+ ctx.restore();
127
+ }
128
+ });
129
+
130
+ // 2. REDRAW DOTS (Datasets 0 and 1)
131
+ [0, 1].forEach(function(i) {
132
+ var meta = chartInstance.getDatasetMeta(i);
133
+ if (!meta.hidden) {
134
+ meta.data.forEach(function(point) {
135
+ point.draw();
136
+ });
137
+ }
138
+ });
139
+
140
+ } catch (err) {
141
+ console.log("Label draw error: " + err);
142
+ }
143
+ }
144
+ });
145
+
15
146
  $.getJSON("/plugins/signalk-polar-performance-plugin/chartData", function(json) {
16
147
 
17
- // Add actual values dots
148
+ // 1. Add actual values dots to the BEGINNING (Indices 0 and 1)
18
149
  json.datasets.unshift({
19
150
  label: 'Polar Speed',
20
151
  backgroundColor: 'rgba(0,255,0,0.7)',
21
152
  borderColor: 'rgba(0,255,0,0.2)',
22
153
  borderWidth: 1,
23
- radius: 30,
24
- type: 'bubble',
25
- data: [
26
- {
27
- y: 0,
28
- x: 0,
29
- r: 10
30
- }
31
- ]
154
+ radius: 10,
155
+ pointStyle: 'circle',
156
+ data: [{ y: 0, x: 0, r: 10 }]
32
157
  },
33
158
  {
34
159
  label: 'Boat Speed',
35
160
  backgroundColor: 'rgba(0,150,255,0.7)',
36
161
  borderColor: 'rgba(0,150,255,0.2)',
37
162
  borderWidth: 1,
38
- radius: 30,
39
- type: 'bubble',
40
- data: [
41
- {
42
- y: 0,
43
- x: 0,
44
- r: 10
45
- }
46
- ]
163
+ radius: 10,
164
+ pointStyle: 'circle',
165
+ data: [{ y: 0, x: 0, r: 10 }]
47
166
  })
48
167
 
168
+ // 2. BACKUP ORIGINAL COLORS
169
+ for (var i = 2; i < json.datasets.length; i++) {
170
+ if (!json.datasets[i].borderColor) {
171
+ json.datasets[i].borderColor = 'rgba(100, 100, 100, 0.5)';
172
+ }
173
+ json.datasets[i]._originalColor = json.datasets[i].borderColor;
174
+ json.datasets[i].borderWidth = 1;
175
+ }
176
+
49
177
  chartData = json
50
178
  showChart(chartData)
51
179
  })
@@ -65,25 +193,8 @@ function showChart (data) {
65
193
  },
66
194
  spanGaps: true,
67
195
  scales: {
68
- x: {
69
- type: 'linear',
70
- ticks: {
71
- color: 'white',
72
- font: {
73
- size: 14
74
- }
75
- }
76
- },
77
- y: {
78
- type: 'linear',
79
- ticks: {
80
- color: 'white',
81
- font: {
82
- size: 14
83
- }
84
- }
85
- },
86
196
  xAxes: [{
197
+ type: 'linear',
87
198
  scaleLabel: {
88
199
  display: true,
89
200
  labelString: 'True wind angle (TWA)',
@@ -101,6 +212,7 @@ function showChart (data) {
101
212
  }
102
213
  }],
103
214
  yAxes: [{
215
+ type: 'linear',
104
216
  scaleLabel: {
105
217
  display: true,
106
218
  labelString: 'Target boat speed (POL SPD)',
@@ -122,8 +234,6 @@ function showChart (data) {
122
234
  enabled: true,
123
235
  callbacks: {
124
236
  label: function (tooltipItems, data) {
125
- console.log("tooltipItems: " + JSON.stringify(tooltipItems))
126
- console.log("data: " + data)
127
237
  let label = tooltipItems.xLabel + "° " + tooltipItems.yLabel + " kts"
128
238
  return label
129
239
  }
@@ -148,74 +258,61 @@ function connect () {
148
258
  console.log("connect()")
149
259
  ws = new WebSocket((window.location.protocol === 'https:' ? 'wss' : 'ws') + "://" + window.location.host + "/signalk/v1/stream?subscribe=none");
150
260
  ws.onopen = function() {
151
-
152
- // Start listening for value updates
153
261
  startListeners();
154
-
155
262
  ws.onmessage = function(event) {
156
263
  if (event.data.includes('signalk-server')) {
157
- welcomeMessage = event.data;
158
- console.log("Skipping welcome message: " + welcomeMessage)
264
+ console.log("Skipping welcome message")
159
265
  } else {
160
266
  handleData(JSON.parse(event.data));
161
267
  }
162
268
  }
163
-
164
- ws.onclose = function() {
165
- console.log("WebSocket closed")
166
- setTimeout(connect, 500)
167
- }
168
-
169
- ws.onerror = function(err) {
170
- console.log("WebSocket connection error: " + err.message + " - closing connection");
171
- setTimeout(connect, 500)
172
- }
173
-
269
+ ws.onclose = function() { setTimeout(connect, 500) }
270
+ ws.onerror = function(err) { setTimeout(connect, 500) }
174
271
  }
175
272
  }
176
273
 
177
274
  window.addEventListener('focus', function () {
178
275
  if (ws.readyState == 0) {
179
- console.log("Restarting websocket")
180
276
  connect()
181
277
  }
182
278
  })
183
279
 
184
280
  function startListeners () {
185
- var paths = [{'path': 'environment.wind.angleTrueWaterDamped'}, {'path': 'performance.polarSpeed'}, {'path': 'performance.boatSpeedDamped'}]
281
+ var paths = [
282
+ {'path': 'environment.wind.angleTrueWaterDamped'},
283
+ {'path': 'performance.polarSpeed'},
284
+ {'path': 'performance.boatSpeedDamped'},
285
+ {'path': 'environment.wind.speedTrue'}
286
+ ]
186
287
 
187
288
  var subscriptionObject = {
188
289
  "context": "vessels.self",
189
290
  "policy" : "ideal",
190
- "minPeriod": 2000,
291
+ "minPeriod": 1000,
191
292
  "subscribe": paths
192
293
  }
193
294
 
194
- var subscriptionMessage = JSON.stringify(subscriptionObject);
195
- console.log("subscriptionMessage: " + subscriptionMessage);
196
- ws.send(subscriptionMessage);
295
+ ws.send(JSON.stringify(subscriptionObject));
197
296
  }
198
297
 
199
298
  function handleData (data) {
200
- if (typeof data.updates[0].meta != 'undefined') {
201
- return
202
- }
299
+ if (typeof data.updates[0].meta != 'undefined') return
300
+
203
301
  var path = data.updates[0].values[0].path
204
302
  var value = data.updates[0].values[0].value
205
303
 
206
304
  if (path == 'performance.polarSpeed') {
207
305
  polarSpeed = roundDec(msToKts(value), 1)
208
- // console.log('polarSpeed set to %s', polarSpeed)
209
306
  chartData.datasets[0].data[0].y = polarSpeed
210
307
  } else if (path == 'environment.wind.angleTrueWaterDamped') {
211
308
  TWA = roundDec(Math.abs(radToDeg(value)),1)
212
- // console.log('TWA set to %s', TWA)
213
309
  chartData.datasets[0].data[0].x = TWA
214
310
  chartData.datasets[1].data[0].x = TWA
215
311
  } else if (path == 'performance.boatSpeedDamped') {
216
312
  boatSpeed = roundDec(msToKts(value),1)
217
- // console.log('boatSpeed set to %s', boatSpeed)
218
313
  chartData.datasets[1].data[0].y = boatSpeed
314
+ } else if (path == 'environment.wind.speedTrue') {
315
+ TWS = msToKts(value);
219
316
  }
220
317
  }
221
318
 
@@ -225,7 +322,72 @@ connect()
225
322
  setInterval(updateChart, 300)
226
323
 
227
324
  function updateChart () {
228
- myChart.update()
325
+ if (myChart) {
326
+ interpolateColors(TWS);
327
+ myChart.update();
328
+ }
329
+ }
330
+
331
+ // --- Interpolate Colors ---
332
+ function interpolateColors(tws) {
333
+ if (!tws || !chartData) return;
334
+
335
+ for (var i = 2; i < chartData.datasets.length; i++) {
336
+ var dataset = chartData.datasets[i];
337
+ var lineSpeed = parseFloat(dataset.label);
338
+
339
+ if (isNaN(lineSpeed)) continue;
340
+
341
+ var originalColor = dataset._originalColor;
342
+
343
+ if (!originalColor) {
344
+ originalColor = "rgba(100,100,100,0.5)";
345
+ dataset._originalColor = originalColor;
346
+ }
347
+
348
+ var diff = Math.abs(tws - lineSpeed);
349
+ var influenceRange = 2.5;
350
+ var weight = Math.max(0, 1 - (diff / influenceRange));
351
+
352
+ if (weight < 0.05) {
353
+ dataset.borderColor = originalColor;
354
+ dataset.borderWidth = 1;
355
+ dataset.pointBorderColor = originalColor;
356
+ dataset.pointBackgroundColor = originalColor;
357
+ } else {
358
+ var blended = blendColors(originalColor, "#FFFFFF", weight);
359
+ dataset.borderColor = blended;
360
+ dataset.borderWidth = 1 + (3 * weight);
361
+ dataset.pointBorderColor = blended;
362
+ dataset.pointBackgroundColor = blended;
363
+ }
364
+ }
365
+ }
366
+
367
+ function blendColors(color1, color2, weight) {
368
+ var c1 = parseColor(color1);
369
+ var c2 = parseColor(color2);
370
+ var r = Math.round(c1[0] + (c2[0] - c1[0]) * weight);
371
+ var g = Math.round(c1[1] + (c2[1] - c1[1]) * weight);
372
+ var b = Math.round(c1[2] + (c2[2] - c1[2]) * weight);
373
+ return "rgb(" + r + "," + g + "," + b + ")";
374
+ }
375
+
376
+ function parseColor(input) {
377
+ if (!input) return [100,100,100];
378
+ if (input.substr(0,1) == "#") {
379
+ var col = input.slice(1);
380
+ if (col.length == 3) col = col[0]+col[0]+col[1]+col[1]+col[2]+col[2];
381
+ var int = parseInt(col, 16);
382
+ return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
383
+ }
384
+ else if (input.substr(0,3) == "rgb") {
385
+ var parts = input.match(/(\d+)/g);
386
+ if (parts && parts.length >= 3) {
387
+ return [parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2])];
388
+ }
389
+ }
390
+ return [100,100,100];
229
391
  }
230
392
 
231
393
  function radToDeg(radians) {
@@ -246,4 +408,5 @@ function roundDec (value, decimals) {
246
408
  }
247
409
 
248
410
  </script>
249
-
411
+ </body>
412
+ </html>