@sailingrotevista/rotevista-dash 2.0.16 → 2.0.17
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/app.js +260 -131
- package/index.html +35 -81
- package/package.json +1 -1
- package/style.css +76 -249
package/app.js
CHANGED
|
@@ -17,18 +17,20 @@ const SIM_SAMPLE_INTERVAL = 1000;
|
|
|
17
17
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
18
18
|
// ==========================================================================
|
|
19
19
|
let simulationMode = false;
|
|
20
|
+
let displayModeSog = 'SOG'; // 'SOG' o 'VMG'
|
|
20
21
|
let socket, renderInterval, simInterval;
|
|
21
22
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
22
23
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
24
|
+
let curBoatCompassRot = 0, curWindCompassRot = 0;
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
let
|
|
27
|
-
let
|
|
28
|
-
let
|
|
26
|
+
let smoothedLeeway = 0, rotationTrend = 0;
|
|
27
|
+
let lastShortAvgVal = null, lastInstantTwa = null;
|
|
28
|
+
let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
|
|
29
|
+
let twDirty = false;
|
|
30
|
+
let reconnectDelay = 1000;
|
|
29
31
|
let isNavigating = false;
|
|
30
32
|
|
|
31
|
-
let pressTimer, isFocusActive = false
|
|
33
|
+
let pressTimer, isFocusActive = false;
|
|
32
34
|
|
|
33
35
|
const graphModes = {
|
|
34
36
|
stw: localStorage.getItem('mode_stw') || 'standard',
|
|
@@ -41,8 +43,8 @@ const store = {
|
|
|
41
43
|
raw: {}, timestamps: {},
|
|
42
44
|
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
43
45
|
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
44
|
-
histories: { stw: [], sog: [], depth: [], tws: [] },
|
|
45
|
-
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 }
|
|
46
|
+
histories: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
|
|
47
|
+
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0 }
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
const ui = {
|
|
@@ -61,7 +63,7 @@ const ui = {
|
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
// ==========================================================================
|
|
64
|
-
// 3. MATEMATICA E
|
|
66
|
+
// 3. UTILITIES DI SISTEMA (MATEMATICA E BUFFER)
|
|
65
67
|
// ==========================================================================
|
|
66
68
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
67
69
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
@@ -69,27 +71,73 @@ function msToKts(ms) { return ms * 1.94384; }
|
|
|
69
71
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
70
72
|
function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Gestione sicura dei buffer per prevenire memory leak.
|
|
76
|
+
*/
|
|
77
|
+
function safePush(buffer, val, time, maxLen = 200) {
|
|
78
|
+
buffer.push({ val: val, time: time });
|
|
79
|
+
if (buffer.length > maxLen) { buffer.shift(); }
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
73
|
-
const now = Date.now();
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
74
85
|
if (validData.length === 0) return null;
|
|
75
|
-
let sSin = 0, sCos = 0;
|
|
86
|
+
let sSin = 0, sCos = 0;
|
|
87
|
+
validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
76
88
|
let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
|
|
77
89
|
let isStable = (validData.length > 2) && (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
78
|
-
let
|
|
79
|
-
return { val: signed ?
|
|
90
|
+
let avgRad = Math.atan2(sSin, sCos);
|
|
91
|
+
return { val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI), stable: isStable };
|
|
80
92
|
}
|
|
81
93
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Calcola il Vento Reale (Acqua e Terra) partendo dagli Apparenti.
|
|
96
|
+
*/
|
|
97
|
+
function computeTrueWind() {
|
|
98
|
+
const aws = store.raw["environment.wind.speedApparent"];
|
|
99
|
+
let awa = store.raw["environment.wind.angleApparent"];
|
|
100
|
+
const stw = store.raw["navigation.speedThroughWater"] || 0, sog = store.raw["navigation.speedOverGround"] || 0;
|
|
101
|
+
const hdg = store.raw["navigation.headingTrue"] || 0, cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
102
|
+
|
|
103
|
+
if (aws === undefined || awa === undefined) return;
|
|
104
|
+
if (awa > Math.PI) awa -= 2 * Math.PI;
|
|
105
|
+
|
|
106
|
+
// Logica Acqua (TWA/TWS)
|
|
107
|
+
const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
|
|
108
|
+
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y), twa_water = Math.atan2(tw_water_y, tw_water_x);
|
|
109
|
+
|
|
110
|
+
// Logica Terra (TWD) con gestione deriva normalizzata
|
|
111
|
+
const drift_angle = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
|
|
112
|
+
const sog_vec_x = sog * Math.cos(drift_angle), sog_vec_y = sog * Math.sin(drift_angle);
|
|
113
|
+
const tw_ground_x = aws * Math.cos(awa) - sog_vec_x, tw_ground_y = aws * Math.sin(awa) - sog_vec_y;
|
|
114
|
+
let twd_ground = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
|
|
115
|
+
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
store.raw["environment.wind.speedTrue"] = tws_water; store.raw["environment.wind.angleTrueWater"] = twa_water; store.raw["environment.wind.directionTrue"] = twd_ground;
|
|
118
|
+
|
|
119
|
+
safePush(store.smoothBuf.twa, twa_water, now); safePush(store.longBuf.twa, twa_water, now);
|
|
120
|
+
safePush(store.smoothBuf.twd, twd_ground, now); safePush(store.longBuf.twd, twd_ground, now);
|
|
121
|
+
}
|
|
85
122
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
123
|
+
function processIncomingData(path, val) {
|
|
124
|
+
const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
|
|
125
|
+
if (path === "navigation.position") store.raw["navigation.position"] = val;
|
|
126
|
+
if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
|
|
127
|
+
|
|
128
|
+
const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
|
|
129
|
+
if (twPaths.includes(path)) twDirty = true;
|
|
130
|
+
if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
|
|
131
|
+
|
|
132
|
+
if (path === "navigation.headingTrue") { safePush(store.smoothBuf.hdg, val, now); safePush(store.longBuf.hdg, val, now); }
|
|
133
|
+
if (path === "navigation.courseOverGroundTrue") { safePush(store.smoothBuf.cog, val, now); safePush(store.longBuf.cog, val, now); }
|
|
91
134
|
}
|
|
92
135
|
|
|
136
|
+
// ==========================================================================
|
|
137
|
+
// 4. AUDIO E ALLARMI
|
|
138
|
+
// ==========================================================================
|
|
139
|
+
function playBingBing() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
|
|
140
|
+
|
|
93
141
|
function playGybeAlarm() {
|
|
94
142
|
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
95
143
|
const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
|
|
@@ -97,65 +145,57 @@ function playGybeAlarm() {
|
|
|
97
145
|
for (let i = 0; i < 4; i++) { note(1800, audioCtx.currentTime + (i * 0.15), 0.1); note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1); }
|
|
98
146
|
}
|
|
99
147
|
|
|
100
|
-
function checkDepthAlarm(m) {
|
|
101
|
-
ui.depth.classList.remove('alarm-warning', 'alarm-danger');
|
|
102
|
-
if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); }
|
|
103
|
-
else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function computeTrueWind() {
|
|
107
|
-
const aws = store.raw["environment.wind.speedApparent"];
|
|
108
|
-
const awa = store.raw["environment.wind.angleApparent"];
|
|
109
|
-
const stw = store.raw["navigation.speedThroughWater"] || 0;
|
|
110
|
-
const sog = store.raw["navigation.speedOverGround"] || 0;
|
|
111
|
-
const hdg = store.raw["navigation.headingTrue"] || 0;
|
|
112
|
-
const cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
113
|
-
if (aws === undefined || awa === undefined) return;
|
|
114
|
-
const tw_water_x = aws * Math.cos(awa) - stw;
|
|
115
|
-
const tw_water_y = aws * Math.sin(awa);
|
|
116
|
-
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
117
|
-
const twa_water = Math.atan2(tw_water_y, tw_water_x);
|
|
118
|
-
const drift_angle = cog - hdg;
|
|
119
|
-
const sog_vec_x = sog * Math.cos(drift_angle);
|
|
120
|
-
const sog_vec_y = sog * Math.sin(drift_angle);
|
|
121
|
-
const tw_ground_x = aws * Math.cos(awa) - sog_vec_x;
|
|
122
|
-
const tw_ground_y = aws * Math.sin(awa) - sog_vec_y;
|
|
123
|
-
const twd_ground = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
|
|
124
|
-
const now = Date.now();
|
|
125
|
-
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
126
|
-
store.raw["environment.wind.angleTrueWater"] = twa_water;
|
|
127
|
-
store.raw["environment.wind.directionTrue"] = twd_ground;
|
|
128
|
-
store.smoothBuf.twa.push({ val: twa_water, time: now });
|
|
129
|
-
store.longBuf.twa.push({ val: twa_water, time: now });
|
|
130
|
-
store.smoothBuf.twd.push({ val: twd_ground, time: now });
|
|
131
|
-
store.longBuf.twd.push({ val: twd_ground, time: now });
|
|
132
|
-
}
|
|
148
|
+
function checkDepthAlarm(m) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
|
|
133
149
|
|
|
150
|
+
// ==========================================================================
|
|
151
|
+
// 5. TREND E TATTICA
|
|
152
|
+
// ==========================================================================
|
|
153
|
+
/**
|
|
154
|
+
* Monitoraggio Trend Vento Professionale (Tattica + Meteo + Gybe Safety)
|
|
155
|
+
*/
|
|
134
156
|
function updateWindTrend() {
|
|
157
|
+
const now = Date.now();
|
|
135
158
|
const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true);
|
|
136
159
|
const shortAvg = getCircularAverageFromBuffer(store.longBuf.twd, 5000, false);
|
|
137
160
|
const instantTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
161
|
+
|
|
138
162
|
if (!shortAvg || !twaAvgObj || instantTwaRad === undefined) return;
|
|
139
163
|
const instantTwaDeg = radToDeg(instantTwaRad);
|
|
140
|
-
|
|
141
|
-
const
|
|
164
|
+
const shortAvgDeg = radToDeg(shortAvg.val);
|
|
165
|
+
const twaAvgDeg = radToDeg(twaAvgObj.val);
|
|
166
|
+
|
|
167
|
+
if (lastShortAvgVal === null) { lastShortAvgVal = shortAvgDeg; lastInstantTwa = instantTwaDeg; return; }
|
|
168
|
+
const dt = (now - lastTrendTime) / 1000; lastTrendTime = now;
|
|
169
|
+
|
|
170
|
+
// --- GYBE SAFETY ---
|
|
171
|
+
const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
|
|
142
172
|
lastInstantTwa = instantTwaDeg;
|
|
173
|
+
if (gybeDetected && isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
|
|
174
|
+
|
|
143
175
|
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
144
176
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
145
|
-
|
|
146
|
-
|
|
177
|
+
|
|
178
|
+
if (now - lastGybeAlarmTime < 4000 && isNavigating) {
|
|
147
179
|
[compassDots.cw, compassDots.ccw, gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.add('is-gybing'); el.classList.remove('is-trending'); el.setAttribute('fill', '#ff0000'); }});
|
|
148
180
|
return;
|
|
149
181
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
|
|
183
|
+
// --- TREND CALCULATION (°/sec) ---
|
|
184
|
+
let diff = (shortAvgDeg - lastShortAvgVal + 540) % 360 - 180; lastShortAvgVal = shortAvgDeg;
|
|
185
|
+
if (dt > 0) {
|
|
186
|
+
let currentRate = Math.max(-10, Math.min(10, diff / dt)); // Clamp a 10°/sec per evitare rumore
|
|
187
|
+
const alpha = Math.min(1, dt / 5);
|
|
188
|
+
rotationTrend = rotationTrend * (1 - alpha) + currentRate * alpha;
|
|
189
|
+
}
|
|
190
|
+
|
|
153
191
|
if (Math.abs(rotationTrend) > 1.0) {
|
|
154
|
-
const pos = store.raw["navigation.position"];
|
|
155
|
-
|
|
156
|
-
let
|
|
157
|
-
|
|
192
|
+
const pos = store.raw["navigation.position"], isSouth = pos && pos.latitude < 0;
|
|
193
|
+
let meteoColor = (!isSouth) ? (rotationTrend < 0 ? "#00ff00" : "#ff0000") : (rotationTrend > 0 ? "#00ff00" : "#ff0000");
|
|
194
|
+
let isLift = (twaAvgDeg > 0) ? (rotationTrend > 0) : (rotationTrend < 0);
|
|
195
|
+
const absTwa = Math.abs(twaAvgDeg);
|
|
196
|
+
if (absTwa >= 90 && absTwa < 160) isLift = !isLift; else if (absTwa >= 160) isLift = false;
|
|
158
197
|
const tacticColor = isLift ? "#00ff00" : "#ff0000";
|
|
198
|
+
|
|
159
199
|
if (rotationTrend > 0) {
|
|
160
200
|
if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
|
|
161
201
|
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
|
|
@@ -171,60 +211,72 @@ function updateWindTrend() {
|
|
|
171
211
|
}
|
|
172
212
|
|
|
173
213
|
// ==========================================================================
|
|
174
|
-
//
|
|
175
|
-
// ==========================================================================
|
|
176
|
-
function processIncomingData(path, val) {
|
|
177
|
-
const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
|
|
178
|
-
if (path === "navigation.position") store.raw["navigation.position"] = val;
|
|
179
|
-
if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
|
|
180
|
-
if (path === "environment.wind.speedApparent" || path === "environment.wind.angleApparent" || path === "navigation.speedThroughWater") computeTrueWind();
|
|
181
|
-
if (path === "navigation.headingTrue") { store.smoothBuf.hdg.push({ val: val, time: now }); store.longBuf.hdg.push({ val: val, time: now }); }
|
|
182
|
-
if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ==========================================================================
|
|
186
|
-
// 6. MOTORE RENDERING UI
|
|
214
|
+
// 6. RENDERING E LOOP PRINCIPALE
|
|
187
215
|
// ==========================================================================
|
|
188
216
|
function updateLeewayDisplay(deg) { const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125); ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w); ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`; }
|
|
189
217
|
|
|
190
218
|
function startDisplayLoop() {
|
|
219
|
+
let tick = 0; // Contatore per i cicli di rendering
|
|
220
|
+
|
|
191
221
|
renderInterval = setInterval(() => {
|
|
192
222
|
const now = Date.now();
|
|
223
|
+
tick++;
|
|
224
|
+
|
|
225
|
+
// ==========================================================
|
|
226
|
+
// LIVE TIER (Ogni 1s) - Reattività, Numeri, Allarmi, Lancette
|
|
227
|
+
// ==========================================================
|
|
193
228
|
const pathsToWatch = { "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog, "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog, "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth, "environment.wind.speedTrue": ui.tws };
|
|
194
229
|
for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { if (pathsToWatch[p] === ui.awsSvg) ui.awsSvg.textContent = "---"; else pathsToWatch[p].innerText = "---"; delete store.raw[p]; } }
|
|
195
230
|
|
|
196
|
-
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
|
|
197
|
-
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
231
|
+
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0), sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
198
232
|
isNavigating = stwKts > CONFIG.averages.minSpeed || sogKts > CONFIG.averages.minSpeed;
|
|
199
233
|
|
|
234
|
+
// Numeri veloci
|
|
200
235
|
if (store.raw["navigation.speedThroughWater"] !== undefined) { ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts); }
|
|
201
236
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
ui.sog.style.color =
|
|
237
|
+
const twaRad = store.raw["environment.wind.angleTrueWater"];
|
|
238
|
+
const vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
|
|
239
|
+
manageHistory('vmg', vmgKts); manageHistory('sog', sogKts);
|
|
240
|
+
if (displayModeSog === 'VMG') { ui.sog.innerText = vmgKts.toFixed(1); ui.sog.style.color = "#64ffda"; document.getElementById('sog-vmg-label').textContent = 'VMG'; }
|
|
241
|
+
else { ui.sog.innerText = sogKts.toFixed(1); ui.sog.style.color = (sogKts-stwKts > 0.3) ? "#2ecc71" : (sogKts-stwKts < -0.3 ? "#e74c3c" : "#fff"); document.getElementById('sog-vmg-label').textContent = 'SOG'; }
|
|
206
242
|
}
|
|
207
243
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) { const d = store.raw["environment.depth.belowTransducer"]; ui.depth.innerText = d.toFixed(1); checkDepthAlarm(d); manageHistory('depth', d); }
|
|
208
|
-
if (store.raw["environment.wind.speedTrue"] !== undefined) {
|
|
209
|
-
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = twsKts.toFixed(1);
|
|
210
|
-
ui.tws.style.color = (twsKts >= CONFIG.graphs.reef2) ? "#e74c3c" : (twsKts >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff");
|
|
211
|
-
manageHistory('tws', twsKts);
|
|
212
|
-
}
|
|
244
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined) { const twsKts = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = twsKts.toFixed(1); ui.tws.style.color = (twsKts >= CONFIG.graphs.reef2) ? "#e74c3c" : (twsKts >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff"); manageHistory('tws', twsKts); }
|
|
213
245
|
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
214
246
|
|
|
247
|
+
// Lancette (Sempre fluide ogni 1s)
|
|
215
248
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
|
|
216
|
-
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, smAwa.val); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
249
|
+
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
217
250
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
218
|
-
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
251
|
+
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
219
252
|
|
|
253
|
+
// Leeway (Ogni 1s)
|
|
220
254
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
221
|
-
let
|
|
222
|
-
|
|
255
|
+
let driftRad = (store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI;
|
|
256
|
+
let driftDeg = radToDeg(driftRad);
|
|
257
|
+
if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
223
258
|
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
259
|
+
const isContaminated = Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7;
|
|
260
|
+
ui.leewayVal.style.color = isContaminated ? "#f39c12" : "#fff";
|
|
224
261
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
225
|
-
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
updateWindTrend(); // Trend calcolato ogni secondo
|
|
265
|
+
|
|
266
|
+
// ==========================================================
|
|
267
|
+
// HEAVY TIER (Ogni 2s) - Disegno Sparklines SVG
|
|
268
|
+
// ==========================================================
|
|
269
|
+
if (tick % 2 === 0) {
|
|
270
|
+
refreshGraph('stw');
|
|
271
|
+
refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog');
|
|
272
|
+
refreshGraph('depth');
|
|
273
|
+
refreshGraph('tws');
|
|
274
|
+
}
|
|
226
275
|
|
|
227
|
-
|
|
276
|
+
// ==========================================================
|
|
277
|
+
// SLOW TIER (Ogni 3s) - Medie MEAN e Calcoli TACK
|
|
278
|
+
// ==========================================================
|
|
279
|
+
if (tick % 3 === 0) {
|
|
228
280
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
|
|
229
281
|
cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
|
|
230
282
|
awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true),
|
|
@@ -234,26 +286,39 @@ function startDisplayLoop() {
|
|
|
234
286
|
const upUI = (el, obj, isCompass = false) => {
|
|
235
287
|
if (!obj || obj.val === null) { el.innerHTML = "---°"; el.classList.remove('unstable-data'); }
|
|
236
288
|
else {
|
|
237
|
-
let
|
|
289
|
+
let valDeg = Math.round(radToDeg(obj.val)); el.innerHTML = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
238
290
|
if (obj.stable || !isNavigating) el.classList.remove('unstable-data'); else el.classList.add('unstable-data');
|
|
239
291
|
}
|
|
240
292
|
};
|
|
241
293
|
upUI(ui.hdg, hObj, true); upUI(ui.cog, cObj, true); upUI(ui.awaAvg, awObj, false); upUI(ui.twaAvg, twObj, false); upUI(ui.twdAvg, twdObj, true);
|
|
294
|
+
|
|
242
295
|
if (hObj && twObj && hObj.val !== null) {
|
|
243
|
-
const
|
|
244
|
-
ui.tackHdg.innerHTML = `${Math.round((
|
|
245
|
-
if (cObj)
|
|
296
|
+
const tackHdgDeg = radToDeg((hObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
|
|
297
|
+
ui.tackHdg.innerHTML = `${Math.round((tackHdgDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
298
|
+
if (cObj) {
|
|
299
|
+
const tackCogDeg = radToDeg((cObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
|
|
300
|
+
ui.tackCog.innerHTML = `${Math.round((tackCogDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const smHdg = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false), smTwd = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
305
|
+
if (smHdg && smTwd) {
|
|
306
|
+
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwd.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
307
|
+
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdg.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
246
308
|
}
|
|
247
|
-
if (twdObj && hObj) { updateWindTrend(); curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`); }
|
|
248
|
-
lastAvgUIUpdate = now;
|
|
249
309
|
}
|
|
250
|
-
|
|
251
|
-
|
|
310
|
+
|
|
311
|
+
// Pulizia buffer
|
|
312
|
+
if (tick % 60 === 0) { // Ogni minuto pulisce tutto per sicurezza
|
|
313
|
+
for (let b in store.smoothBuf) { while (store.smoothBuf[b].length > 0 && (now - store.smoothBuf[b][0].time) > CONFIG.averages.smoothWindow) store.smoothBuf[b].shift(); }
|
|
314
|
+
for (let b in store.longBuf) { while (store.longBuf[b].length > 0 && (now - store.longBuf[b][0].time) > CONFIG.averages.longWindow) store.longBuf[b].shift(); }
|
|
315
|
+
tick = 0; // Reset contatore
|
|
316
|
+
}
|
|
252
317
|
}, RENDER_INTERVAL_MS);
|
|
253
318
|
}
|
|
254
319
|
|
|
255
320
|
// ==========================================================================
|
|
256
|
-
// 7.
|
|
321
|
+
// 7. CONFIGURAZIONE E GRAFICI
|
|
257
322
|
// ==========================================================================
|
|
258
323
|
async function fetchServerConfig() {
|
|
259
324
|
if (!window.location.protocol.includes("http")) return;
|
|
@@ -278,11 +343,43 @@ async function fetchServerConfig() {
|
|
|
278
343
|
}
|
|
279
344
|
}
|
|
280
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Gestisce l'archiviazione dei dati storici.
|
|
348
|
+
* Il disegno grafico viene ora gestito separatamente nel loop pesante.
|
|
349
|
+
*/
|
|
281
350
|
function manageHistory(t, v) {
|
|
282
|
-
const n = Date.now();
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
351
|
+
const n = Date.now();
|
|
352
|
+
const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
353
|
+
|
|
354
|
+
if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
|
|
355
|
+
store.histories[t].push(v);
|
|
356
|
+
if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
|
|
357
|
+
store.lastUpdates[t] = n;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Funzione dedicata al ridisegno dei grafici (chiamata dal loop HEAVY)
|
|
363
|
+
*/
|
|
364
|
+
function refreshGraph(t) {
|
|
365
|
+
const type = (t === 'vmg') ? 'sog' : t;
|
|
366
|
+
const data = store.histories[t];
|
|
367
|
+
if (!data || data.length < 2) return;
|
|
368
|
+
|
|
369
|
+
const mode = graphModes[type];
|
|
370
|
+
const cfg = calculateScale(type, data, mode);
|
|
371
|
+
|
|
372
|
+
// Aggiornamento stile box Hercules
|
|
373
|
+
const graphEl = document.getElementById(type + '-graph');
|
|
374
|
+
if (graphEl) {
|
|
375
|
+
const box = graphEl.closest('.data-box');
|
|
376
|
+
if (box) {
|
|
377
|
+
if (mode === 'hercules') box.classList.add('box-hercules');
|
|
378
|
+
else box.classList.remove('box-hercules');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
updateScaleLabels(type, cfg.min, cfg.max);
|
|
382
|
+
drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
286
383
|
}
|
|
287
384
|
|
|
288
385
|
function calculateScale(type, data, mode) {
|
|
@@ -308,14 +405,13 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
308
405
|
}
|
|
309
406
|
});
|
|
310
407
|
const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#ffffff' };
|
|
311
|
-
|
|
408
|
+
const colorKey = id === 'sog-graph' && displayModeSog === 'VMG' ? '#64ffda' : clrs[id];
|
|
409
|
+
svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(255,255,255,0.08)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${colorKey}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${colorKey}" />`;
|
|
312
410
|
}
|
|
313
411
|
|
|
314
412
|
// ==========================================================================
|
|
315
|
-
// 8. INTERAZIONI
|
|
413
|
+
// 8. INTERAZIONI E SIMULATORE
|
|
316
414
|
// ==========================================================================
|
|
317
|
-
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
318
|
-
|
|
319
415
|
function toggleFocusMode(type, element) {
|
|
320
416
|
const container = document.querySelector('.main-container'); const parentPanel = element.closest('.side-panel'); const isLeft = parentPanel.classList.contains('left-panel');
|
|
321
417
|
isFocusActive = !isFocusActive;
|
|
@@ -324,14 +420,16 @@ function toggleFocusMode(type, element) {
|
|
|
324
420
|
}
|
|
325
421
|
|
|
326
422
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
327
|
-
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
423
|
+
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
424
|
+
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
425
|
+
el.addEventListener('pointerdown', (e) => { isLongPressActive = false; pressTimer = setTimeout(() => { if (!isFocusActive) { isLongPressActive = true; toggleFocusMode(type, el); lastTapTime = 0; } }, 1000); });
|
|
426
|
+
el.addEventListener('pointerup', (e) => {
|
|
427
|
+
clearTimeout(pressTimer); if (isLongPressActive) return;
|
|
428
|
+
const currentTime = new Date().getTime(), tapDelay = currentTime - lastTapTime;
|
|
331
429
|
if (tapDelay < 300 && tapDelay > 0) { clearTimeout(tapTimeout); if (!isFocusActive) { graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); } lastTapTime = 0; }
|
|
332
|
-
else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); },
|
|
430
|
+
else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(100, 255, 218, 0.1)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
|
|
333
431
|
});
|
|
334
|
-
el.addEventListener('
|
|
432
|
+
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
335
433
|
});
|
|
336
434
|
|
|
337
435
|
if (ui.hotspot) {
|
|
@@ -340,25 +438,53 @@ if (ui.hotspot) {
|
|
|
340
438
|
ui.hotspot.addEventListener('pointerleave', () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } });
|
|
341
439
|
}
|
|
342
440
|
|
|
343
|
-
// ==========================================================================
|
|
344
|
-
// 9. CONNESSIONE E SIMULATORE
|
|
345
|
-
// ==========================================================================
|
|
346
441
|
function connect() {
|
|
347
442
|
if (simulationMode) return;
|
|
443
|
+
|
|
348
444
|
let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
|
|
445
|
+
const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws';
|
|
446
|
+
|
|
349
447
|
try {
|
|
350
|
-
socket = new WebSocket(
|
|
351
|
-
|
|
352
|
-
socket.
|
|
353
|
-
|
|
354
|
-
|
|
448
|
+
socket = new WebSocket(`${protocol}://${addr}/signalk/v1/stream?subscribe=self`);
|
|
449
|
+
|
|
450
|
+
socket.onopen = () => {
|
|
451
|
+
ui.status.className = "online";
|
|
452
|
+
ui.status.innerText = "ONLINE";
|
|
453
|
+
reconnectDelay = 1000; // Reset del ritardo dopo una connessione riuscita
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
socket.onmessage = (e) => {
|
|
457
|
+
if (simulationMode) return;
|
|
458
|
+
const d = JSON.parse(e.data);
|
|
459
|
+
if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value)));
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
socket.onclose = () => {
|
|
463
|
+
if (!simulationMode) {
|
|
464
|
+
ui.status.className = "offline";
|
|
465
|
+
ui.status.innerText = "RECONNECTING...";
|
|
466
|
+
|
|
467
|
+
// Exponential Backoff: aumenta il tempo ad ogni tentativo fallito
|
|
468
|
+
setTimeout(connect, reconnectDelay);
|
|
469
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); // Max 10 secondi
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
socket.onerror = () => {
|
|
474
|
+
socket.close(); // Forza il trigger di onclose
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
} catch (e) {
|
|
478
|
+
setTimeout(connect, reconnectDelay);
|
|
479
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
|
|
480
|
+
}
|
|
355
481
|
}
|
|
356
482
|
|
|
357
483
|
ui.depth.closest('.data-box').addEventListener('click', (function() { let dC = 0, lC = 0; return function() { const n = Date.now(); if (n - lC < 500) dC++; else dC = 1; lC = n; if (dC === 3) { simulationMode = !simulationMode; if (simulationMode) { if (socket) socket.close(); startDynamicSimulation(); } else location.reload(); dC = 0; } }; })());
|
|
358
484
|
|
|
359
485
|
function startDynamicSimulation() {
|
|
360
486
|
ui.status.innerText = "SIM ATTIVO";
|
|
361
|
-
let sim = { hdg: 45, tws: 12, twd: 45, depth: 12, stw: 5, leeway: 0, startTime: Date.now() };
|
|
487
|
+
let sim = { hdg: 45, tws: 12, twd: 45, depth: 12, stw: 5, leeway: 0, currentSpeed: 1.5, currentDir: 90, startTime: Date.now() };
|
|
362
488
|
simInterval = setInterval(() => {
|
|
363
489
|
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
364
490
|
if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
|
|
@@ -368,19 +494,22 @@ function startDynamicSimulation() {
|
|
|
368
494
|
let twaRel = (sim.twd - sim.hdg + 360) % 360; if (twaRel > 180) twaRel -= 360;
|
|
369
495
|
let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125)); sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
|
|
370
496
|
const rawLeeway = Math.sin(degToRad(twaRel)) * 4; sim.leeway += (rawLeeway - sim.leeway) * 0.05;
|
|
371
|
-
const
|
|
372
|
-
const
|
|
497
|
+
const bX = sim.stw * Math.sin(degToRad(sim.hdg + sim.leeway)), bY = sim.stw * Math.cos(degToRad(sim.hdg + sim.leeway));
|
|
498
|
+
const cX = sim.currentSpeed * Math.sin(degToRad(sim.currentDir)), cY = sim.currentSpeed * Math.cos(degToRad(sim.currentDir));
|
|
499
|
+
const sog = Math.sqrt(Math.pow(bX + cX, 2) + Math.pow(bY + cY, 2)), cog = (radToDeg(Math.atan2(bX + cX, bY + cY)) + 360) % 360;
|
|
500
|
+
const twaRad = degToRad(twaRel), aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
|
|
373
501
|
const awa = Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad));
|
|
374
502
|
processIncomingData("environment.wind.speedApparent", ktsToMs(aws)); processIncomingData("environment.wind.angleApparent", awa);
|
|
375
503
|
processIncomingData("environment.depth.belowTransducer", sim.depth); processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
376
|
-
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw)); processIncomingData("navigation.speedOverGround", ktsToMs(
|
|
504
|
+
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw)); processIncomingData("navigation.speedOverGround", ktsToMs(sog));
|
|
377
505
|
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog)); processIncomingData("navigation.position", { latitude: 45.0, longitude: 12.0 });
|
|
378
506
|
}, 1000);
|
|
379
507
|
}
|
|
380
508
|
|
|
381
509
|
// ==========================================================================
|
|
382
|
-
//
|
|
510
|
+
// 9. INIT
|
|
383
511
|
// ==========================================================================
|
|
512
|
+
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
384
513
|
(function genTicks() { const c = document.getElementById('ticks'); if (c) { for (let i = 0; i < 360; i += 10) { const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50)); l.setAttribute("stroke", m ? "#fff" : "#666"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
|
|
385
514
|
async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
|
|
386
515
|
window.addEventListener('load', init);
|