speedandcurrent 2.1.1 → 2.2.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.
package/package.json CHANGED
@@ -1,49 +1,50 @@
1
- {
2
- "name": "speedandcurrent",
3
- "version": "2.1.1",
4
- "description": "A plugin that automatically corrects boat speed and calculates current and leeway using sensor fusion.",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "node --test test/plugin.test.js"
8
- },
9
- "author": "aswin.bouwmeester@gmail.com",
10
- "license": "MIT",
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/Asw1n/speedandcurrent.git"
14
- },
15
- "bugs": {
16
- "url": "https://github.com/Asw1n/speedandcurrent/issues"
17
- },
18
- "homepage": "https://github.com/Asw1n/speedandcurrent#readme",
19
- "engines": {
20
- "node": ">=22.0.0",
21
- "signalk-server": ">=2.28.0"
22
- },
23
- "keywords": [
24
- "signalk-webapp",
25
- "signalk-node-server-plugin",
26
- "signalk-category-utility",
27
- "signalk-category-instruments"
28
- ],
29
- "signalk-plugin-enabled-by-default": false,
30
- "signalk": {
31
- "appIcon": "./icon.png",
32
- "displayName": "Speed and current",
33
- "screenshots": [
34
- "./docs/correctionTable.png",
35
- "./docs/STW_estimation.png",
36
- "./docs/learning.png"
37
- ]
38
- },
39
- "dependencies": {
40
- "kalman-filter": "^2.3.0",
41
- "signalkutilities": "^3.0.0"
42
- },
43
- "files": [
44
- "index.js",
45
- "correctionTable.js",
46
- "public/",
47
- "docs/"
48
- ]
49
- }
1
+ {
2
+ "name": "speedandcurrent",
3
+ "version": "2.2.1",
4
+ "description": "A plugin that automatically corrects boat speed and calculates current and leeway using sensor fusion.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node --test test/plugin.test.js"
8
+ },
9
+ "author": "aswin.bouwmeester@gmail.com",
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/Asw1n/speedandcurrent.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/Asw1n/speedandcurrent/issues"
17
+ },
18
+ "homepage": "https://github.com/Asw1n/speedandcurrent#readme",
19
+ "engines": {
20
+ "node": ">=22.0.0",
21
+ "signalk-server": ">=2.28.0"
22
+ },
23
+ "keywords": [
24
+ "signalk-webapp",
25
+ "signalk-node-server-plugin",
26
+ "signalk-category-utility",
27
+ "signalk-category-instruments"
28
+ ],
29
+ "signalk-plugin-enabled-by-default": false,
30
+ "signalk": {
31
+ "appIcon": "./icon.png",
32
+ "displayName": "Speed and current",
33
+ "screenshots": [
34
+ "./docs/correctionTable.png",
35
+ "./docs/STW_estimation.png",
36
+ "./docs/learning.png"
37
+ ]
38
+ },
39
+ "dependencies": {
40
+ "kalman-filter": "^2.3.0",
41
+ "signalkutilities": "^3.1.0"
42
+ },
43
+ "files": [
44
+ "icon.png",
45
+ "index.js",
46
+ "correctionTable.js",
47
+ "public/",
48
+ "docs/"
49
+ ]
50
+ }
@@ -1,144 +1,144 @@
1
- // TableRenderer — purpose-built renderer for the correction table.
2
- // Rows = speed bins (knots), columns = heel bins (degrees).
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.
6
-
7
- const RAD_TO_DEG = 180 / Math.PI;
8
- const MPS_TO_KNOTS = 1.943844;
9
-
10
- const DEFAULT_SPEED_SYMBOL = 'kn';
11
- const DEFAULT_HEEL_SYMBOL = '°';
12
- function fmtSpeed(mps) { return (mps * MPS_TO_KNOTS).toFixed(1); }
13
- function fmtHeel(rad) { return (rad * RAD_TO_DEG).toFixed(0); }
14
- function fmtFactor(f) { const p = (f - 1) * 100; return (p >= 0 ? '+' : '') + p.toFixed(1) + '%'; }
15
- function fmtLeeway(rad) { const d = rad * RAD_TO_DEG; return (d >= 0 ? '+' : '') + d.toFixed(0) + '°'; }
16
-
17
- class TableRenderer {
18
-
19
- // opts.fmtSpeed / opts.fmtHeel: optional unit-aware formatter functions.
20
- // Fall back to the module-level fmtSpeed / fmtHeel when not provided.
21
- render(data, opts = {}) {
22
- const { id, row, col, table, displayAttributes } = data;
23
- const label = displayAttributes?.label ?? '';
24
- const maxDev = this._computeMaxDev(table);
25
- const fmtSpeedFn = opts.fmtSpeed || fmtSpeed;
26
- const fmtHeelFn = opts.fmtHeel || fmtHeel;
27
- const speedSymbol = opts.speedSymbol || DEFAULT_SPEED_SYMBOL;
28
- const heelSymbol = opts.heelSymbol || DEFAULT_HEEL_SYMBOL;
29
- const cornerText = `${speedSymbol} / ${heelSymbol}`;
30
-
31
- const el = document.createElement('table');
32
- el.id = id;
33
- el.classList.add('Table2D');
34
- el.appendChild(this._headerRow(col, cornerText, fmtHeelFn));
35
-
36
- let rIndex = 0;
37
- for (let r = row.min; r <= row.max + 0.01; r += row.step) {
38
- el.appendChild(this._dataRow(r, rIndex, col, table, maxDev, fmtSpeedFn));
39
- rIndex++;
40
- }
41
- return el;
42
- }
43
-
44
- _headerRow(col, cornerText, fmtHeelFn) {
45
- const tr = document.createElement('tr');
46
- const th0 = document.createElement('th');
47
- th0.textContent = cornerText;
48
- th0.classList.add('TableRowHeader', 'TableCorner');
49
- tr.appendChild(th0);
50
- for (let c = col.min; c <= col.max + 0.01; c += col.step) {
51
- const th = document.createElement('th');
52
- th.textContent = fmtHeelFn(c);
53
- th.classList.add('TablecolumnHeader');
54
- tr.appendChild(th);
55
- }
56
- return tr;
57
- }
58
-
59
- _dataRow(r, rIndex, col, table, maxDev, fmtSpeedFn) {
60
- const tr = document.createElement('tr');
61
- const th = document.createElement('th');
62
- th.textContent = fmtSpeedFn(r);
63
- th.classList.add('TableRowHeader');
64
- tr.appendChild(th);
65
- let cIndex = 0;
66
- for (let c = col.min; c <= col.max + 0.01; c += col.step) {
67
- tr.appendChild(this._cellElement(table[rIndex][cIndex], maxDev));
68
- cIndex++;
69
- }
70
- return tr;
71
- }
72
-
73
- _cellElement(cell, maxDev) {
74
- const td = document.createElement('td');
75
- td.classList.add('TableCell');
76
-
77
- if (!cell || cell.N === 0) {
78
- td.classList.add('cell--empty');
79
- return td;
80
- }
81
-
82
- const factor = Number.isFinite(cell.factor) ? cell.factor : null;
83
- const leeway = Number.isFinite(cell.leeway) ? cell.leeway : null;
84
-
85
- if (factor !== null) {
86
- const d = document.createElement('div');
87
- d.className = 'cell-factor';
88
- d.textContent = fmtFactor(factor);
89
- td.appendChild(d);
90
- const color = this._factorColor(factor, maxDev);
91
- if (leeway !== null) {
92
- const angleDeg = 90 + leeway * RAD_TO_DEG * 1;
93
- td.style.background = `repeating-linear-gradient(
94
- ${angleDeg}deg,
95
- ${color} 0px, ${color} 9px,
96
- #f0f0f0 9px, #f0f0f0 10px
97
- )`;
98
- } else {
99
- td.style.backgroundColor = color;
100
- }
101
- }
102
- if (leeway !== null) {
103
- const d = document.createElement('div');
104
- d.className = 'cell-leeway';
105
- d.textContent = fmtLeeway(leeway);
106
- td.appendChild(d);
107
- }
108
-
109
- const attrs = cell.displayAttributes;
110
- if (attrs?.selected) td.classList.add('cell--active');
111
- else if (attrs?.normWeight > 0) td.classList.add('cell--neighbour');
112
-
113
- return td;
114
- }
115
-
116
- // Find the largest absolute factor deviation from 1 to normalise the color scale.
117
- _computeMaxDev(table) {
118
- let maxDev = 0;
119
- for (const row of table) {
120
- for (const cell of row) {
121
- if (!cell || cell.N === 0 || !Number.isFinite(cell.factor)) continue;
122
- const dev = Math.abs(cell.factor - 1);
123
- if (dev > maxDev) maxDev = dev;
124
- }
125
- }
126
- return maxDev || 0.05; // avoid a fully white table when all factors are near 1
127
- }
128
-
129
- // factor < 1: paddlewheel reads fast → white→orange
130
- // factor > 1: paddlewheel reads slow → white→green
131
- _factorColor(factor, maxDev) {
132
- if (!Number.isFinite(factor)) return '';
133
- const dev = factor - 1;
134
- if (Math.abs(dev) < 1e-6) return '';
135
- const a = Math.min(1, Math.abs(dev) / maxDev);
136
- 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
140
- }
141
- }
142
- }
143
-
1
+ // TableRenderer — purpose-built renderer for the correction table.
2
+ // Rows = speed bins (knots), columns = heel bins (degrees).
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.
6
+
7
+ const RAD_TO_DEG = 180 / Math.PI;
8
+ const MPS_TO_KNOTS = 1.943844;
9
+
10
+ const DEFAULT_SPEED_SYMBOL = 'kn';
11
+ const DEFAULT_HEEL_SYMBOL = '°';
12
+ function fmtSpeed(mps) { return (mps * MPS_TO_KNOTS).toFixed(1); }
13
+ function fmtHeel(rad) { return (rad * RAD_TO_DEG).toFixed(0); }
14
+ function fmtFactor(f) { const p = (f - 1) * 100; return (p >= 0 ? '+' : '') + p.toFixed(1) + '%'; }
15
+ function fmtLeeway(rad) { const d = rad * RAD_TO_DEG; return (d >= 0 ? '+' : '') + d.toFixed(0) + '°'; }
16
+
17
+ class TableRenderer {
18
+
19
+ // opts.fmtSpeed / opts.fmtHeel: optional unit-aware formatter functions.
20
+ // Fall back to the module-level fmtSpeed / fmtHeel when not provided.
21
+ render(data, opts = {}) {
22
+ const { id, row, col, table, displayAttributes } = data;
23
+ const label = displayAttributes?.label ?? '';
24
+ const maxDev = this._computeMaxDev(table);
25
+ const fmtSpeedFn = opts.fmtSpeed || fmtSpeed;
26
+ const fmtHeelFn = opts.fmtHeel || fmtHeel;
27
+ const speedSymbol = opts.speedSymbol || DEFAULT_SPEED_SYMBOL;
28
+ const heelSymbol = opts.heelSymbol || DEFAULT_HEEL_SYMBOL;
29
+ const cornerText = `${speedSymbol} / ${heelSymbol}`;
30
+
31
+ const el = document.createElement('table');
32
+ el.id = id;
33
+ el.classList.add('Table2D');
34
+ el.appendChild(this._headerRow(col, cornerText, fmtHeelFn));
35
+
36
+ let rIndex = 0;
37
+ for (let r = row.min; r <= row.max + 0.01; r += row.step) {
38
+ el.appendChild(this._dataRow(r, rIndex, col, table, maxDev, fmtSpeedFn));
39
+ rIndex++;
40
+ }
41
+ return el;
42
+ }
43
+
44
+ _headerRow(col, cornerText, fmtHeelFn) {
45
+ const tr = document.createElement('tr');
46
+ const th0 = document.createElement('th');
47
+ th0.textContent = cornerText;
48
+ th0.classList.add('TableRowHeader', 'TableCorner');
49
+ tr.appendChild(th0);
50
+ for (let c = col.min; c <= col.max + 0.01; c += col.step) {
51
+ const th = document.createElement('th');
52
+ th.textContent = fmtHeelFn(c);
53
+ th.classList.add('TablecolumnHeader');
54
+ tr.appendChild(th);
55
+ }
56
+ return tr;
57
+ }
58
+
59
+ _dataRow(r, rIndex, col, table, maxDev, fmtSpeedFn) {
60
+ const tr = document.createElement('tr');
61
+ const th = document.createElement('th');
62
+ th.textContent = fmtSpeedFn(r);
63
+ th.classList.add('TableRowHeader');
64
+ tr.appendChild(th);
65
+ let cIndex = 0;
66
+ for (let c = col.min; c <= col.max + 0.01; c += col.step) {
67
+ tr.appendChild(this._cellElement(table[rIndex][cIndex], maxDev));
68
+ cIndex++;
69
+ }
70
+ return tr;
71
+ }
72
+
73
+ _cellElement(cell, maxDev) {
74
+ const td = document.createElement('td');
75
+ td.classList.add('TableCell');
76
+
77
+ if (!cell || cell.N === 0) {
78
+ td.classList.add('cell--empty');
79
+ return td;
80
+ }
81
+
82
+ const factor = Number.isFinite(cell.factor) ? cell.factor : null;
83
+ const leeway = Number.isFinite(cell.leeway) ? cell.leeway : null;
84
+
85
+ if (factor !== null) {
86
+ const d = document.createElement('div');
87
+ d.className = 'cell-factor';
88
+ d.textContent = fmtFactor(factor);
89
+ td.appendChild(d);
90
+ const color = this._factorColor(factor, maxDev);
91
+ if (leeway !== null) {
92
+ const angleDeg = 90 + leeway * RAD_TO_DEG * 1;
93
+ td.style.background = `repeating-linear-gradient(
94
+ ${angleDeg}deg,
95
+ ${color} 0px, ${color} 9px,
96
+ #f0f0f0 9px, #f0f0f0 10px
97
+ )`;
98
+ } else {
99
+ td.style.backgroundColor = color;
100
+ }
101
+ }
102
+ if (leeway !== null) {
103
+ const d = document.createElement('div');
104
+ d.className = 'cell-leeway';
105
+ d.textContent = fmtLeeway(leeway);
106
+ td.appendChild(d);
107
+ }
108
+
109
+ const attrs = cell.displayAttributes;
110
+ if (attrs?.selected) td.classList.add('cell--active');
111
+ else if (attrs?.normWeight > 0) td.classList.add('cell--neighbour');
112
+
113
+ return td;
114
+ }
115
+
116
+ // Find the largest absolute factor deviation from 1 to normalise the color scale.
117
+ _computeMaxDev(table) {
118
+ let maxDev = 0;
119
+ for (const row of table) {
120
+ for (const cell of row) {
121
+ if (!cell || cell.N === 0 || !Number.isFinite(cell.factor)) continue;
122
+ const dev = Math.abs(cell.factor - 1);
123
+ if (dev > maxDev) maxDev = dev;
124
+ }
125
+ }
126
+ return maxDev || 0.05; // avoid a fully white table when all factors are near 1
127
+ }
128
+
129
+ // factor < 1: paddlewheel reads fast → white→orange
130
+ // factor > 1: paddlewheel reads slow → white→green
131
+ _factorColor(factor, maxDev) {
132
+ if (!Number.isFinite(factor)) return '';
133
+ const dev = factor - 1;
134
+ if (Math.abs(dev) < 1e-6) return '';
135
+ const a = Math.min(1, Math.abs(dev) / maxDev);
136
+ 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
140
+ }
141
+ }
142
+ }
143
+
144
144
  export default TableRenderer;