speedandcurrent 2.3.3 → 2.4.0

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": "speedandcurrent",
3
- "version": "2.3.3",
3
+ "version": "2.4.0",
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": {
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "kalman-filter": "^2.3.0",
44
- "signalkutilities": "^3.1.2"
44
+ "signalkutilities": "^3.1.6"
45
45
  },
46
46
  "files": [
47
47
  "icon.png",
@@ -1,14 +1,18 @@
1
1
  // TableRenderer — purpose-built renderer for the correction table.
2
2
  // Rows = speed bins (knots), columns = heel bins (degrees).
3
3
  // Each learned cell shows factor deviation (±%) and leeway (°).
4
- // Background encodes factor: green = paddlewheel reads slow, orange = reads fast.
5
- // Active cell (last updated) gets a bold border; interpolation neighbours get a faint tint.
4
+ // Background encodes factor and leeway; a bottom bar encodes fusion weight.
5
+ // Active cell (last updated) gets a thin black border.
6
6
 
7
7
  const RAD_TO_DEG = 180 / Math.PI;
8
8
  const MPS_TO_KNOTS = 1.943844;
9
9
 
10
10
  const DEFAULT_SPEED_SYMBOL = 'kn';
11
11
  const DEFAULT_HEEL_SYMBOL = '°';
12
+
13
+ const MARKER_CORRECTED_COLOR = '#0d6efd';
14
+ const MARKER_RAW_COLOR = '#dc3545';
15
+ const MARKER_DOT_RADIUS = 8;
12
16
  function fmtSpeed(mps) { return (mps * MPS_TO_KNOTS).toFixed(1); }
13
17
  function fmtHeel(rad) { return (rad * RAD_TO_DEG).toFixed(0); }
14
18
  function fmtFactor(f) { const p = (f - 1) * 100; return (p >= 0 ? '+' : '') + p.toFixed(1) + '%'; }
@@ -38,7 +42,75 @@ class TableRenderer {
38
42
  el.appendChild(this._dataRow(r, rIndex, col, table, maxDev, fmtSpeedFn));
39
43
  rIndex++;
40
44
  }
41
- return el;
45
+
46
+ const wrap = document.createElement('div');
47
+ wrap.classList.add('Table2D-wrap');
48
+ wrap.appendChild(el);
49
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
50
+ svg.classList.add('Table2D-markers');
51
+ wrap.appendChild(svg);
52
+ return wrap;
53
+ }
54
+
55
+ // Overlay a dot for the uncorrected (raw) speed and a dot for the corrected
56
+ // speed, both positioned at the raw heel column.
57
+ // wrap: the element returned by render(). row/col: the table's axis config
58
+ // ({min,max,step}). marker: { rawSpeed, heel, correctedSpeed } in raw (SI)
59
+ // units, or null/undefined to clear the overlay.
60
+ renderMarkers(wrap, row, col, marker) {
61
+ const svg = wrap.querySelector('.Table2D-markers');
62
+ const table = wrap.querySelector('.Table2D');
63
+ if (!svg || !table) return;
64
+ svg.innerHTML = '';
65
+ if (!marker) return;
66
+ const { rawSpeed, heel, correctedSpeed } = marker;
67
+ if (![rawSpeed, heel, correctedSpeed].every(Number.isFinite)) return;
68
+
69
+ const wrapRect = wrap.getBoundingClientRect();
70
+ svg.setAttribute('width', wrapRect.width);
71
+ svg.setAttribute('height', wrapRect.height);
72
+
73
+ const rowHeaders = [...table.querySelectorAll('th.TableRowHeader:not(.TableCorner)')];
74
+ const colHeaders = [...table.querySelectorAll('th.TablecolumnHeader')];
75
+ if (!rowHeaders.length || !colHeaders.length) return;
76
+
77
+ const centerY = rowHeaders.map(th => {
78
+ const r = th.getBoundingClientRect();
79
+ return (r.top + r.bottom) / 2 - wrapRect.top;
80
+ });
81
+ const centerX = colHeaders.map(th => {
82
+ const r = th.getBoundingClientRect();
83
+ return (r.left + r.right) / 2 - wrapRect.left;
84
+ });
85
+
86
+ const x = this._lerpAxis(centerX, (heel - col.min) / col.step);
87
+ const yRaw = this._lerpAxis(centerY, (rawSpeed - row.min) / row.step);
88
+ const yCorr = this._lerpAxis(centerY, (correctedSpeed - row.min) / row.step);
89
+
90
+ svg.appendChild(this._markerDot(x, yRaw, MARKER_RAW_COLOR));
91
+ svg.appendChild(this._markerDot(x, yCorr, MARKER_CORRECTED_COLOR));
92
+ }
93
+
94
+ // Interpolate a continuous axis index (may be fractional, out of range) against
95
+ // an array of known pixel centers for each axis bin.
96
+ _lerpAxis(centers, frac) {
97
+ const n = centers.length;
98
+ const f = Math.max(0, Math.min(n - 1, frac));
99
+ const i0 = Math.floor(f);
100
+ const i1 = Math.min(i0 + 1, n - 1);
101
+ const t = f - i0;
102
+ return centers[i0] + (centers[i1] - centers[i0]) * t;
103
+ }
104
+
105
+ _markerDot(x, y, color) {
106
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
107
+ circle.setAttribute('cx', x);
108
+ circle.setAttribute('cy', y);
109
+ circle.setAttribute('r', MARKER_DOT_RADIUS);
110
+ circle.setAttribute('fill', color);
111
+ circle.setAttribute('stroke', '#fff');
112
+ circle.setAttribute('stroke-width', 1);
113
+ return circle;
42
114
  }
43
115
 
44
116
  _headerRow(col, cornerText, fmtHeelFn) {
@@ -89,7 +161,7 @@ class TableRenderer {
89
161
  td.appendChild(d);
90
162
  const color = this._factorColor(factor, maxDev);
91
163
  if (leeway !== null) {
92
- const angleDeg = 90 + leeway * RAD_TO_DEG * 1;
164
+ const angleDeg = 90 + leeway * RAD_TO_DEG;
93
165
  td.style.background = `repeating-linear-gradient(
94
166
  ${angleDeg}deg,
95
167
  ${color} 0px, ${color} 9px,
@@ -107,13 +179,19 @@ class TableRenderer {
107
179
  }
108
180
 
109
181
  const attrs = cell.displayAttributes;
110
- if (attrs?.selected) td.classList.add('cell--active');
111
- else if (attrs?.normWeight > 0) td.classList.add('cell--neighbour');
182
+ if (Number.isFinite(attrs?.normWeight) && attrs.normWeight > 0) {
183
+ const d = document.createElement('div');
184
+ d.className = 'cell-weight';
185
+ const weight = Math.max(0, Math.min(1, attrs.normWeight));
186
+ d.style.width = `${weight * 100}%`;
187
+ d.title = `Fusion weight ${(weight * 100).toFixed(1)}%`;
188
+ td.appendChild(d);
189
+ }
190
+ if (attrs?.selected) td.classList.add('cell--active');
112
191
 
113
192
  return td;
114
193
  }
115
194
 
116
- // Find the largest absolute factor deviation from 1 to normalise the color scale.
117
195
  _computeMaxDev(table) {
118
196
  let maxDev = 0;
119
197
  for (const row of table) {
@@ -123,21 +201,18 @@ class TableRenderer {
123
201
  if (dev > maxDev) maxDev = dev;
124
202
  }
125
203
  }
126
- return maxDev || 0.05; // avoid a fully white table when all factors are near 1
204
+ return maxDev || 0.05;
127
205
  }
128
206
 
129
- // factor < 1: paddlewheel reads fast → white→orange
130
- // factor > 1: paddlewheel reads slow → white→green
131
207
  _factorColor(factor, maxDev) {
132
208
  if (!Number.isFinite(factor)) return '';
133
209
  const dev = factor - 1;
134
210
  if (Math.abs(dev) < 1e-6) return '';
135
- const a = Math.min(1, Math.abs(dev) / maxDev);
211
+ const intensity = Math.min(1, Math.abs(dev) / maxDev);
136
212
  if (dev < 0) {
137
- return `rgb(255,${Math.round(255 - 90 * a)},${Math.round(255 * (1 - a))})`; // white→orange
138
- } else {
139
- return `rgb(${Math.round(255 * (1 - a))},${Math.round(255 - 95 * a)},${Math.round(255 - 175 * a)})`; // white→green
213
+ return `rgb(255,${Math.round(255 - 90 * intensity)},${Math.round(255 * (1 - intensity))})`;
140
214
  }
215
+ return `rgb(${Math.round(255 * (1 - intensity))},${Math.round(255 - 95 * intensity)},${Math.round(255 - 175 * intensity)})`;
141
216
  }
142
217
  }
143
218