speedandcurrent 1.4.2 → 1.5.1

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.
@@ -0,0 +1,98 @@
1
+ class LeakyExtremes {
2
+ /**
3
+ * @param {Object} opts
4
+ * @param {number} opts.initialMin
5
+ * @param {number} opts.initialMax
6
+ * @param {number} opts.tau - time constant in seconds for exponential decay
7
+ * @param {number} opts.catchupTau - time constant in seconds for fast catchup
8
+ * @param {boolean} opts.isAngle - true if data is angular (radians)
9
+ * @param {number} opts.period - period for angle wrapping (e.g., 2*Math.PI)
10
+ */
11
+ constructor({ initialMin=0, initialMax=0, tau = .15, catchupTau=.5, isAngle = false, period = 2 * Math.PI }) {
12
+ this.min = initialMin;
13
+ this.max = initialMax;
14
+ this.tau = tau;
15
+ this.catchupTau = catchupTau;
16
+ this.isAngle = isAngle;
17
+ this.period = period;
18
+ this.lastUpdate = Date.now();
19
+ }
20
+
21
+ // Helper for angle difference in [-period/2, period/2]
22
+ angleDiff(a, b) {
23
+ let d = a - b;
24
+ while (d > this.period / 2) d -= this.period;
25
+ while (d < -this.period / 2) d += this.period;
26
+ return d;
27
+ }
28
+
29
+ // Helper for angle normalization to [0, period)
30
+ normalizeAngle(a) {
31
+ while (a < 0) a += this.period;
32
+ while (a >= this.period) a -= this.period;
33
+ return a;
34
+ }
35
+
36
+ update(obs) {
37
+ if (!isFinite(obs) ) return;
38
+ const now = Date.now();
39
+ const dt = (now - this.lastUpdate) / 1000;
40
+ if (dt==0) return;
41
+ this.lastUpdate = now;
42
+
43
+ if (!this.isAngle) {
44
+ // Linear data
45
+ // Update min
46
+ if (obs < this.min) {
47
+ const alpha = 1 - Math.exp(-dt / this.catchupTau);
48
+ this.min += (obs - this.min) * alpha;
49
+ } else {
50
+ const alpha = 1 - Math.exp(-dt / this.tau);
51
+ this.min += (obs - this.min) * alpha;
52
+ }
53
+ // Update max
54
+ if (obs > this.max) {
55
+ const alpha = 1 - Math.exp(-dt / this.catchupTau);
56
+ this.max += (obs - this.max) * alpha;
57
+ } else {
58
+ const alpha = 1 - Math.exp(-dt / this.tau);
59
+ this.max += (obs - this.max) * alpha;
60
+ }
61
+ } else {
62
+ // Angular data
63
+ obs=this.normalizeAngle(obs);
64
+ let minDiff = this.angleDiff(obs, this.min);
65
+ let maxDiff = this.angleDiff(obs, this.max);
66
+
67
+ // Update min
68
+ if (minDiff < 0) {
69
+ const alpha = 1 - Math.exp(-dt / this.catchupTau);
70
+ this.min = this.normalizeAngle(this.min + minDiff * alpha);
71
+ } else {
72
+ const alpha = 1 - Math.exp(-dt / this.tau);
73
+ this.min = this.normalizeAngle(this.min + minDiff * alpha);
74
+ }
75
+ // Update max
76
+ if (maxDiff > 0) {
77
+ const alpha = 1 - Math.exp(-dt / this.catchupTau);
78
+ this.max = this.normalizeAngle(this.max + maxDiff * alpha);
79
+ } else {
80
+ const alpha = 1 - Math.exp(-dt / this.tau);
81
+ this.max = this.normalizeAngle(this.max + maxDiff * alpha);
82
+ }
83
+
84
+ }
85
+ }
86
+
87
+ // For angles, returns the minimal arc between min and max
88
+ get range() {
89
+ if (!this.isAngle) {
90
+ return this.max - this.min;
91
+ } else {
92
+ let diff = this.angleDiff(this.max, this.min);
93
+ return Math.abs(diff);
94
+ }
95
+ }
96
+ }
97
+
98
+ module.exports = { LeakyExtremes };
package/package.json CHANGED
@@ -1,17 +1,16 @@
1
1
  {
2
2
  "name": "speedandcurrent",
3
- "version": "1.4.2",
3
+ "version": "1.5.1",
4
4
  "description": "A plugin that automatically corrects boat speed and calculates current and leeway using sensor fusion.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
8
8
  },
9
9
  "author": "aswin.bouwmeester@gmail.com",
10
- "license": "ISC",
10
+ "license": "MIT",
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "git+https://github.com/Asw1n/speedandcurrent.git",
14
- "directory": "./CHANGELOG.md"
13
+ "url": "git+https://github.com/Asw1n/speedandcurrent.git"
15
14
  },
16
15
  "bugs": {
17
16
  "url": "https://github.com/Asw1n/speedandcurrent/issues"
@@ -24,11 +23,17 @@
24
23
  ],
25
24
  "signalk-plugin-enabled-by-default": false,
26
25
  "signalk": {
27
- "appIcon": "./icon.png",
26
+ "appIcon": "icon.png",
28
27
  "displayName": "Speed and current"
29
28
  },
30
29
  "dependencies": {
31
30
  "kalman-filter": "^2.3.0",
32
- "signalkutilities": "^1.4.0"
33
- }
31
+ "signalkutilities": "^1.5.1"
32
+ },
33
+ "files": [
34
+ "index.js",
35
+ "correctionTable.js",
36
+ "leakyExtremes.js",
37
+ "public/"
38
+ ]
34
39
  }
@@ -3,8 +3,8 @@ class TableRenderer {
3
3
  this.formatColumnHeader = this.defaultDisplay;
4
4
  this.formatRowHeader = this.defaultDisplay;
5
5
  this.formatCell = this.defaultDisplay;
6
- this.formatCellX = this.defaultDisplay;
7
- this.formatCellY = this.defaultDisplay;
6
+ this.colorMode = 'none';
7
+ this.cachedExtents = {}; // store min/max per mode
8
8
  }
9
9
 
10
10
  setColumnHeaderFormat(columnHeader) {
@@ -19,18 +19,15 @@ class TableRenderer {
19
19
  cell != null ? this.formatCell = cell : this.formatCell = this.defaultDisplay;
20
20
  }
21
21
 
22
- setCellFormatX(cellX) {
23
- cellX != null ? this.formatCellX = cellX : this.formatCellX = this.defaultDisplay;
24
- }
25
-
26
- setCellFormatY(cellY) {
27
- cellY != null ? this.formatCellY = cellY : this.formatCellY = this.defaultDisplay;
28
- }
22
+ // Removed legacy X/Y specific formatting.
29
23
 
30
24
  render(data) {
31
25
  const {id, row, col, table, displayAttributes } = data;
32
26
  this.data = data;
33
27
  const label = displayAttributes && displayAttributes.label ? displayAttributes.label : "";
28
+
29
+ // Pre-compute extents for color modes (skip weight which is already normalized)
30
+ this.computeExtents(table);
34
31
 
35
32
  // Create a table element
36
33
  const tableElement = document.createElement('table');
@@ -90,7 +87,12 @@ class TableRenderer {
90
87
  else {
91
88
  cell.classList.add('emptyCell');
92
89
  }
93
- if (value.displayAttributes && value.displayAttributes.selected) cell.classList.add('selectedCell');
90
+ if (value.displayAttributes && value.displayAttributes.selected) {
91
+ cell.classList.add('selectedCell');
92
+ //console.log(value);
93
+ }
94
+ // Apply coloring based on selected color mode
95
+ this.applyColor(cell, value, table);
94
96
 
95
97
  rowElement.appendChild(cell);
96
98
  cIndex++;
@@ -102,86 +104,136 @@ class TableRenderer {
102
104
  return value.toFixed(2);
103
105
  }
104
106
 
105
- createSurfacePlot() {
106
- const { row, col, table, id } = this.data;
107
+ setColorMode(mode) {
108
+ this.colorMode = mode || 'none';
109
+ }
107
110
 
108
- // Format the axes values
109
- const x = [];
110
- for (let c = col.min; c <= col.max + 0.01; c += col.step) {
111
- x.push(this.formatColumnHeader(c));
111
+ computeExtents(table) {
112
+ if (!Array.isArray(table)) return;
113
+ const modesToCompute = ['leeway','factor','trace'];
114
+ const accum = {};
115
+ for (const m of modesToCompute) {
116
+ accum[m] = {min: Infinity, max: -Infinity};
112
117
  }
113
-
114
- const y = [];
115
- for (let r = row.min; r <= row.max + 0.01; r += row.step) {
116
- y.push(this.formatRowHeader(r));
118
+ for (const row of table) {
119
+ for (const cell of row) {
120
+ if (!cell || cell.N === 0) continue;
121
+ const {x,y} = cell;
122
+ // Prefer server-provided leeway for consistent scaling; fallback to placeholder
123
+ const leeway = Number.isFinite(cell.leeway)
124
+ ? cell.leeway
125
+ : (Number.isFinite(y) && Number.isFinite(x) ? Math.atan2(y, 1) : null);
126
+ if (leeway != null) {
127
+ if (leeway < accum.leeway.min) accum.leeway.min = leeway;
128
+ if (leeway > accum.leeway.max) accum.leeway.max = leeway;
129
+ }
130
+ if (Number.isFinite(cell.factor)) {
131
+ const f = cell.factor;
132
+ if (f < accum.factor.min) accum.factor.min = f;
133
+ if (f > accum.factor.max) accum.factor.max = f;
134
+ }
135
+ if (Number.isFinite(cell.trace)) {
136
+ const t = cell.trace;
137
+ if (t < accum.trace.min) accum.trace.min = t;
138
+ if (t > accum.trace.max) accum.trace.max = t;
139
+ }
140
+ }
117
141
  }
118
-
142
+ this.cachedExtents = accum;
143
+ }
119
144
 
120
- const zX = table.map(row => row.map(cell => cell.N > 0 ? cell.x : null)); // Assuming 'x' values for the surface plot
121
- const zY = table.map(row => row.map(cell => cell.N > 0 ? cell.y : null)); // Assuming 'y' values for the surface plot
122
-
123
- // Open a new window
124
- const newWindow = window.open('', 'Correction Plot', 'width=1024,height=800');
125
- newWindow.document.title = 'Correction Plot';
126
-
127
- // Create div elements for the plots
128
- const plotDivX = newWindow.document.createElement('div');
129
- plotDivX.id = 'surfacePlotX';
130
- plotDivX.style.width = '100%';
131
- plotDivX.style.height = '400px';
132
-
133
- const plotDivY = newWindow.document.createElement('div');
134
- plotDivY.id = 'surfacePlotY';
135
- plotDivY.style.width = '100%';
136
- plotDivY.style.height = '400px';
137
-
138
- newWindow.document.body.appendChild(plotDivX);
139
- newWindow.document.body.appendChild(plotDivY);
140
-
141
- // Data for the surface plots
142
- const plotDataX = [{
143
- type: 'surface',
144
- x: x,
145
- y: y,
146
- z: zX
147
- }];
148
-
149
- const plotDataY = [{
150
- type: 'surface',
151
- x: x,
152
- y: y,
153
- z: zY
154
- }];
155
-
156
- // Layout for the surface plots
157
- const layoutX = {
158
- title: 'X-value',
159
- autosize: true,
160
- scene: {
161
- xaxis: { title: 'Speed' },
162
- yaxis: { title: 'Heel' },
163
- zaxis: { title: 'Correction' }
164
- }
165
- };
166
-
167
- const layoutY = {
168
- title: 'Y-value',
169
- autosize: true,
170
- scene: {
171
- xaxis: { title: 'Speed' },
172
- yaxis: { title: 'Heel' },
173
- zaxis: { title: 'Correction' }
145
+ applyColor(cell, value, table) {
146
+ if (this.colorMode === 'none') return;
147
+ // weight uses provided normWeight
148
+ if (this.colorMode === 'weight') {
149
+ const weight = value.displayAttributes && Number.isFinite(value.displayAttributes.normWeight) ? Math.min(1, value.displayAttributes.normWeight) : 0;
150
+ const scaled = Math.pow(weight, 0.4);
151
+ cell.style.backgroundColor = this.blueScale(scaled);
152
+ return;
153
+ }
154
+ if (value.N === 0) return;
155
+ let v = 0;
156
+ if (this.colorMode === 'leeway') v = Number.isFinite(value.leeway) ? value.leeway : Math.atan2(value.y, 1); // prefer server-provided leeway
157
+ else if (this.colorMode === 'factor') v = Number.isFinite(value.factor) ? value.factor : null;
158
+ else if (this.colorMode === 'trace') v = Number.isFinite(value.trace) ? value.trace : null;
159
+
160
+ const extent = this.cachedExtents[this.colorMode];
161
+ if (!extent || !Number.isFinite(v) || extent.min === Infinity) return;
162
+
163
+ let color;
164
+ {
165
+ // sequential for magnitude
166
+ let norm;
167
+ if (this.colorMode === 'leeway') {
168
+ // leeway uses absolute value; recompute extent assuming cached min/max are signed
169
+ const maxAbs = Math.max(Math.abs(extent.min), Math.abs(extent.max));
170
+ norm = maxAbs > 0 ? Math.min(1, Math.abs(v) / maxAbs) : 0;
171
+ if (norm === 0) {
172
+ return; // blank for zero leeway
173
+ }
174
+ } else if (this.colorMode === 'factor') {
175
+ // Asymmetric: baseline 1 -> blank. <1 use orange scale, >1 use green scale.
176
+ if (!Number.isFinite(v) || v <= 0) return; // invalid or zero forward -> blank
177
+ if (Math.abs(v - 1) < 1e-6) return; // exactly baseline
178
+ // Determine deviation span using max distance from 1 among sampled extents
179
+ const maxDev = Math.max(Math.abs(extent.min - 1), Math.abs(extent.max - 1));
180
+ if (maxDev <= 0) return;
181
+ const dev = (v - 1) / maxDev; // in [-1,1]
182
+ const a = Math.min(1, Math.abs(dev));
183
+ if (dev < 0) {
184
+ // compression (factor<1): white -> orange (#FFA500)
185
+ const r = Math.round(255 - (255-255)*a); // stays 255
186
+ const g = Math.round(255 - (255-165)*a); // 255 -> 165
187
+ const b = Math.round(255 - (255-0)*a); // 255 -> 0
188
+ color = `rgb(${r},${g},${b})`;
189
+ } else {
190
+ // expansion (factor>1): white -> green (#00A050 slightly toned)
191
+ const target = {r:0,g:160,b:80};
192
+ const r = Math.round(255 - (255-target.r)*a);
193
+ const g = Math.round(255 - (255-target.g)*a);
194
+ const b = Math.round(255 - (255-target.b)*a);
195
+ color = `rgb(${r},${g},${b})`;
196
+ }
197
+ cell.style.backgroundColor = color;
198
+ return;
199
+ } else if (this.colorMode === 'trace') {
200
+ if (!Number.isFinite(v) || v <= 0) return; // blank for no/zero trace
201
+ // Scale 0..1
202
+ const normTrace = (v - extent.min) / (extent.max - extent.min || 1);
203
+ if (normTrace <= 0) return;
204
+ const t = Math.pow(Math.min(1, Math.max(0, normTrace)), 0.5); // gamma 0.5 for contrast
205
+ // White -> Purple (#8000FF) gradient
206
+ const target = { r: 128, g: 0, b: 255 };
207
+ const r = Math.round(255 - (255 - target.r) * t);
208
+ const g = Math.round(255 - (255 - target.g) * t);
209
+ const b = Math.round(255 - (255 - target.b) * t);
210
+ cell.style.backgroundColor = `rgb(${r},${g},${b})`;
211
+ return;
212
+ } else {
213
+ norm = (v - extent.min) / (extent.max - extent.min || 1); // 0..1
174
214
  }
175
- };
215
+ color = this.blueScale(Math.pow(norm,0.5));
216
+ }
217
+ cell.style.backgroundColor = color;
218
+ }
176
219
 
177
- const config = {
178
- displayModeBar: false,
179
- responsive: true // Ensure the plot is responsive
180
- };
220
+ blueScale(t) {
221
+ // t in [0,1]; 0 = white, 1 = deep blue
222
+ if (!Number.isFinite(t)) t = 0;
223
+ if (t < 0) t = 0; else if (t > 1) t = 1;
224
+ const r = Math.round(255 - 255 * t);
225
+ const g = Math.round(255 - 255 * t);
226
+ const b = 255;
227
+ return `rgb(${r},${g},${b})`;
228
+ }
181
229
 
182
- // Render the plots using Plotly
183
- Plotly.newPlot(plotDivX, plotDataX, layoutX, config);
184
- Plotly.newPlot(plotDivY, plotDataY, layoutY, config);
230
+ redBlueDiverging(n) {
231
+ // n in [-1,1]; negative=blue positive=red, 0=white
232
+ const t = (n+1)/2; // 0..1
233
+ const r = Math.round(255 * t);
234
+ const g = Math.round(255 * (1 - Math.abs(n)));
235
+ const b = Math.round(255 * (1 - t));
236
+ return `rgb(${r},${g},${b})`;
185
237
  }
186
238
  }
187
239