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/README.md +74 -45
- package/correctionTable.js +296 -66
- package/docs/correction-table-learning.md +33 -0
- package/docs/correction-table-v1.schema.json +219 -0
- package/docs/correction-table-v2.schema.json +134 -0
- package/docs/correction-table.schema.json +219 -0
- package/index.js +299 -132
- package/package.json +2 -2
- package/public/TableRenderer.js +89 -14
- package/public/app.js +230 -166
- package/public/index.html +14 -6
- package/public/main.css +19 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speedandcurrent",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
44
|
+
"signalkutilities": "^3.1.6"
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
47
|
"icon.png",
|
package/public/TableRenderer.js
CHANGED
|
@@ -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
|
|
5
|
-
// Active cell (last updated) gets a
|
|
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
|
-
|
|
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
|
|
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?.
|
|
111
|
-
|
|
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;
|
|
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
|
|
211
|
+
const intensity = Math.min(1, Math.abs(dev) / maxDev);
|
|
136
212
|
if (dev < 0) {
|
|
137
|
-
return `rgb(255,${Math.round(255 - 90 *
|
|
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
|
|