@sailingrotevista/rotevista-dash 5.0.2 → 5.0.3
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 +261 -474
- package/package.json +1 -1
package/app.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ==========================================================================
|
|
3
|
-
* Signal K Wind Dashboard - Pro Version
|
|
3
|
+
* Signal K Wind Dashboard - Pro Version 3.5 (Time-Based Architecture)
|
|
4
4
|
* ==========================================================================
|
|
5
5
|
* Autore: Sailing Rotevista
|
|
6
6
|
* Motore di calcolo tattico per navigazione e crociera.
|
|
7
|
-
* Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico
|
|
8
|
-
* Memoria UI persistente, Modalità Hercules
|
|
7
|
+
* Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
|
|
8
|
+
* Memoria UI persistente, Modalità Hercules, Focus Split Screen e
|
|
9
|
+
* Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
// ==========================================================================
|
|
@@ -18,13 +19,13 @@ let CONFIG = {
|
|
|
18
19
|
longWindow: 30000,
|
|
19
20
|
stabilityTolerance: 2000,
|
|
20
21
|
stabilityThreshold: 0.99,
|
|
21
|
-
minSpeed: 0,
|
|
22
|
+
minSpeed: 0.5,
|
|
22
23
|
stabilityBreakout: 15
|
|
23
24
|
},
|
|
24
25
|
graphs: { reef1: 10, reef2: 15, historyMinutes: 10, samples: 60 },
|
|
25
26
|
scales: {
|
|
26
|
-
stw: { stdMax:
|
|
27
|
-
sog: { stdMax:
|
|
27
|
+
stw: { stdMax: 4, hercSpan: 4, step: 2 },
|
|
28
|
+
sog: { stdMax: 4, hercSpan: 4, step: 2 },
|
|
28
29
|
tws: { stdMax: 15, hercSpan: 10, step: 5 },
|
|
29
30
|
depth: { stdMax: 8, hercSpan: 5, step: 5 }
|
|
30
31
|
},
|
|
@@ -34,10 +35,9 @@ let CONFIG = {
|
|
|
34
35
|
const RENDER_INTERVAL_MS = 1000;
|
|
35
36
|
const TIMEOUT_MS = 5000;
|
|
36
37
|
const SIM_SAMPLE_INTERVAL = 1000;
|
|
37
|
-
const DASH_VERSION = "3.
|
|
38
|
+
const DASH_VERSION = "3.5"; // Major Update: Time-Based storage and GAP handling
|
|
38
39
|
const sourceLocks = {};
|
|
39
40
|
|
|
40
|
-
|
|
41
41
|
// ==========================================================================
|
|
42
42
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
43
43
|
// ==========================================================================
|
|
@@ -60,7 +60,6 @@ let emaTwaSin = 0;
|
|
|
60
60
|
let emaTwaCos = 0;
|
|
61
61
|
let firstEmaRun = true;
|
|
62
62
|
|
|
63
|
-
// Stato dei singoli grafici (Standard vs Hercules Zoom)
|
|
64
63
|
const graphModes = {
|
|
65
64
|
stw: 'standard',
|
|
66
65
|
sog: 'standard',
|
|
@@ -74,8 +73,8 @@ const store = {
|
|
|
74
73
|
timestamps: {},
|
|
75
74
|
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
76
75
|
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
76
|
+
// histories ora conterrà array di oggetti: { time: Date.now(), val: numero }
|
|
77
77
|
histories: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
|
|
78
|
-
// Buffer temporaneo per il calcolo della media dell'intervallo del grafico
|
|
79
78
|
graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
|
|
80
79
|
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 }
|
|
81
80
|
};
|
|
@@ -97,16 +96,13 @@ const ui = {
|
|
|
97
96
|
};
|
|
98
97
|
|
|
99
98
|
// ==========================================================================
|
|
100
|
-
// 3. UTILITIES (MATEMATICA, BUFFER
|
|
99
|
+
// 3. UTILITIES (MATEMATICA, BUFFER E MEMORIA)
|
|
101
100
|
// ==========================================================================
|
|
102
101
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
103
102
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
104
103
|
function msToKts(ms) { return ms * 1.94384; }
|
|
105
104
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
106
105
|
|
|
107
|
-
/**
|
|
108
|
-
* Calcola il percorso più breve per la rotazione di un puntatore (evita giri di 360°)
|
|
109
|
-
*/
|
|
110
106
|
function getShortestRotation(curr, target) {
|
|
111
107
|
let diff = (target - curr) % 360;
|
|
112
108
|
if (diff > 180) diff -= 360;
|
|
@@ -114,15 +110,10 @@ function getShortestRotation(curr, target) {
|
|
|
114
110
|
return curr + diff;
|
|
115
111
|
}
|
|
116
112
|
|
|
117
|
-
/**
|
|
118
|
-
* Inserisce un dato nel buffer circolare limitandolo a 2000 campioni (30 min)
|
|
119
|
-
*/
|
|
120
113
|
/**
|
|
121
114
|
* Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
|
|
122
115
|
*/
|
|
123
116
|
function safePush(buffer, val, time) {
|
|
124
|
-
// --- PROTEZIONE ANTI-NaN (FONDAMENTALE ALL'AVVIO) ---
|
|
125
|
-
// Se il valore è nullo, non definito o non è un numero, ignoriamo l'inserimento
|
|
126
117
|
if (val === null || val === undefined || isNaN(val)) return;
|
|
127
118
|
|
|
128
119
|
buffer.push({
|
|
@@ -132,21 +123,15 @@ function safePush(buffer, val, time) {
|
|
|
132
123
|
cos: Math.cos(val)
|
|
133
124
|
});
|
|
134
125
|
|
|
135
|
-
// Teniamo sempre in memoria il DOPPIO della storia impostata (per la modalità ancoraggio)
|
|
136
|
-
// + 1 minuto di margine
|
|
137
126
|
const maxHistoryMs = (CONFIG.graphs.historyMinutes * 60000 * 2) + 60000;
|
|
138
|
-
|
|
139
127
|
while (buffer.length > 0 && (time - buffer[0].time) > maxHistoryMs) {
|
|
140
128
|
buffer.shift();
|
|
141
129
|
}
|
|
142
|
-
|
|
143
|
-
// Tetto massimo di campioni per sicurezza (circa 2 ore a 5Hz)
|
|
144
130
|
if (buffer.length > 36000) buffer.shift();
|
|
145
131
|
}
|
|
146
132
|
|
|
147
133
|
/**
|
|
148
134
|
* Media Circolare Vettoriale - Versione "Soft Outlier Rejection"
|
|
149
|
-
* Riduce l'impatto degli sbalzi limitando il loro angolo massimo di discostamento.
|
|
150
135
|
*/
|
|
151
136
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
|
|
152
137
|
now = now || Date.now();
|
|
@@ -156,7 +141,6 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
|
|
|
156
141
|
let sSin = 0, sCos = 0, count = 0;
|
|
157
142
|
let newestTime = 0, oldestTime = 0;
|
|
158
143
|
|
|
159
|
-
// 1. MEDIA PILOTA: Guardiamo l'ultima frazione di dati (es. max 15 campioni) per sapere dove punta "ora"
|
|
160
144
|
let pilotSin = 0, pilotCos = 0;
|
|
161
145
|
const pilotSamples = Math.min(len, 15);
|
|
162
146
|
for (let i = len - 1; i >= len - pilotSamples; i--) {
|
|
@@ -164,34 +148,21 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
|
|
|
164
148
|
pilotCos += bufferArray[i].cos;
|
|
165
149
|
}
|
|
166
150
|
const pilotRad = Math.atan2(pilotSin, pilotCos);
|
|
167
|
-
|
|
168
|
-
// Il limite elastico in radianti (basato sul tuo stabilityBreakout in gradi)
|
|
169
151
|
const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
|
|
170
152
|
|
|
171
|
-
// 2. CALCOLO AMMORTIZZATO
|
|
172
153
|
for (let i = len - 1; i >= 0; i--) {
|
|
173
154
|
const item = bufferArray[i];
|
|
174
155
|
if ((now - item.time) > windowMs) break;
|
|
175
156
|
|
|
176
|
-
|
|
177
|
-
let diffRad = Math.atan2(
|
|
178
|
-
Math.sin(item.val - pilotRad),
|
|
179
|
-
Math.cos(item.val - pilotRad)
|
|
180
|
-
);
|
|
181
|
-
|
|
157
|
+
let diffRad = Math.atan2(Math.sin(item.val - pilotRad), Math.cos(item.val - pilotRad));
|
|
182
158
|
let finalSin, finalCos;
|
|
183
159
|
|
|
184
|
-
// Se lo scarto è maggiore del limite, "Pattiniamo" (Ammortizzazione)
|
|
185
160
|
if (Math.abs(diffRad) > limitRad) {
|
|
186
|
-
// Tronchiamo la differenza al limite massimo consentito (mantenendo il segno)
|
|
187
161
|
const clampedDiff = Math.sign(diffRad) * limitRad;
|
|
188
|
-
// Ricalcoliamo l'angolo ammortizzato
|
|
189
162
|
const clampedRad = pilotRad + clampedDiff;
|
|
190
|
-
|
|
191
163
|
finalSin = Math.sin(clampedRad);
|
|
192
164
|
finalCos = Math.cos(clampedRad);
|
|
193
165
|
} else {
|
|
194
|
-
// Il dato è buono, usiamo i valori precalcolati
|
|
195
166
|
finalSin = item.sin;
|
|
196
167
|
finalCos = item.cos;
|
|
197
168
|
}
|
|
@@ -220,9 +191,6 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
|
|
|
220
191
|
};
|
|
221
192
|
}
|
|
222
193
|
|
|
223
|
-
/**
|
|
224
|
-
* Salva lo stato attuale della dashboard (dati e preferenze UI) nel browser
|
|
225
|
-
*/
|
|
226
194
|
function saveDashboardState() {
|
|
227
195
|
try {
|
|
228
196
|
const focusedBox = document.querySelector('.data-box.is-focused');
|
|
@@ -248,9 +216,6 @@ function saveDashboardState() {
|
|
|
248
216
|
} catch (e) { console.error("Save error:", e); }
|
|
249
217
|
}
|
|
250
218
|
|
|
251
|
-
/**
|
|
252
|
-
* Carica e ripristina lo stato salvato (entro i 20 minuti di vecchiaia)
|
|
253
|
-
*/
|
|
254
219
|
function loadDashboardState() {
|
|
255
220
|
const saved = localStorage.getItem('rotevista_dash_state');
|
|
256
221
|
if (!saved) return;
|
|
@@ -263,31 +228,24 @@ function loadDashboardState() {
|
|
|
263
228
|
if (state.longBuf) Object.assign(store.longBuf, state.longBuf);
|
|
264
229
|
if (state.graphModes) Object.assign(graphModes, state.graphModes);
|
|
265
230
|
|
|
266
|
-
// Ripristino SOG/VMG
|
|
267
231
|
if (state.displayModeSog) {
|
|
268
232
|
displayModeSog = state.displayModeSog;
|
|
269
233
|
const labelEl = document.getElementById('sog-vmg-label');
|
|
270
234
|
if (labelEl) labelEl.textContent = displayModeSog;
|
|
271
235
|
}
|
|
272
|
-
|
|
273
|
-
// Ripristino TWS/AWS ---
|
|
274
236
|
if (state.displayModeTws) {
|
|
275
237
|
displayModeTws = state.displayModeTws;
|
|
276
238
|
const labelEl = document.getElementById('tws-aws-label');
|
|
277
239
|
if (labelEl) labelEl.textContent = displayModeTws;
|
|
278
240
|
}
|
|
279
|
-
|
|
280
|
-
// Ripristino Tema Notte
|
|
281
241
|
if (state.isNightMode) document.body.classList.add('night-mode');
|
|
282
242
|
|
|
283
|
-
// Ripristino Focus (Dual Screen)
|
|
284
243
|
if (state.isFocusActive && state.focusedBoxType) {
|
|
285
244
|
setTimeout(() => {
|
|
286
245
|
const el = document.querySelector(`.box-${state.focusedBoxType}`);
|
|
287
246
|
if (el) { isFocusActive = false; toggleFocusMode(state.focusedBoxType, el); }
|
|
288
247
|
}, 200);
|
|
289
248
|
}
|
|
290
|
-
console.log("Stato ripristinato con successo dalla cache.");
|
|
291
249
|
}
|
|
292
250
|
} catch (e) { localStorage.removeItem('rotevista_dash_state'); }
|
|
293
251
|
}
|
|
@@ -323,13 +281,10 @@ function playGybeAlarm() {
|
|
|
323
281
|
}
|
|
324
282
|
|
|
325
283
|
function checkDepthAlarm(m) {
|
|
326
|
-
// Rimuoviamo sempre le classi prima di riapplicarle
|
|
327
284
|
ui.depth.classList.remove('alarm-warning', 'alarm-danger', 'blink-alarm');
|
|
328
|
-
|
|
329
|
-
// Logica di confronto dinamica
|
|
330
285
|
if (m < CONFIG.alarms.depthDanger) {
|
|
331
286
|
ui.depth.classList.add('alarm-danger', 'blink-alarm');
|
|
332
|
-
playBingBing();
|
|
287
|
+
playBingBing();
|
|
333
288
|
} else if (m < CONFIG.alarms.depthWarning) {
|
|
334
289
|
ui.depth.classList.add('alarm-warning');
|
|
335
290
|
}
|
|
@@ -350,11 +305,9 @@ function computeTrueWind() {
|
|
|
350
305
|
const hdg = store.raw["navigation.headingTrue"] || 0, cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
351
306
|
if (aws === undefined || awa === undefined) return;
|
|
352
307
|
|
|
353
|
-
// Vento Reale Rispetto all'acqua (TWA/TWS Water)
|
|
354
308
|
const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
|
|
355
309
|
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
356
310
|
|
|
357
|
-
// Vento Reale Rispetto al fondo (TWD Ground)
|
|
358
311
|
const drift = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
|
|
359
312
|
const tw_ground_x = aws * Math.cos(awa) - sog * Math.cos(drift), tw_ground_y = aws * Math.sin(awa) - sog * Math.sin(drift);
|
|
360
313
|
const tws_ground = Math.sqrt(tw_ground_x * tw_ground_x + tw_ground_y * tw_ground_y);
|
|
@@ -376,49 +329,37 @@ function computeTrueWind() {
|
|
|
376
329
|
function processIncomingData(path, val, source) {
|
|
377
330
|
const now = Date.now();
|
|
378
331
|
|
|
379
|
-
//
|
|
332
|
+
// FILTRO SORGENTE STICKY
|
|
380
333
|
if (!sourceLocks[path] || sourceLocks[path].label === source || (now - sourceLocks[path].lastSeen > 2000)) {
|
|
381
334
|
sourceLocks[path] = { label: source, lastSeen: now };
|
|
382
335
|
} else {
|
|
383
|
-
// Se arriva un dato da un'altra sorgente mentre il lock è attivo, lo scartiamo
|
|
384
336
|
return;
|
|
385
337
|
}
|
|
386
338
|
|
|
387
|
-
// --- 2. AGGIORNAMENTO DATI (Solo per la sorgente eletta) ---
|
|
388
339
|
store.timestamps[path] = now;
|
|
389
340
|
store.raw[path] = val;
|
|
390
341
|
|
|
391
|
-
// Buffer per AWA (Vento Apparente)
|
|
392
342
|
if (path === "environment.wind.angleApparent") {
|
|
393
343
|
safePush(store.smoothBuf.awa, val, now);
|
|
394
344
|
safePush(store.longBuf.awa, val, now);
|
|
395
345
|
}
|
|
396
|
-
|
|
397
|
-
// Buffer per HDG (Prua)
|
|
398
346
|
if (path === "navigation.headingTrue") {
|
|
399
347
|
safePush(store.smoothBuf.hdg, val, now);
|
|
400
348
|
safePush(store.longBuf.hdg, val, now);
|
|
401
349
|
}
|
|
402
|
-
|
|
403
|
-
// Buffer per COG (Rotta Fondo)
|
|
404
350
|
if (path === "navigation.courseOverGroundTrue") {
|
|
405
351
|
safePush(store.smoothBuf.cog, val, now);
|
|
406
352
|
safePush(store.longBuf.cog, val, now);
|
|
407
353
|
}
|
|
408
354
|
|
|
409
|
-
// --- 3. TRIGGER CALCOLO VENTO REALE (TWA/TWS/TWD) ---
|
|
410
355
|
const twPaths = [
|
|
411
|
-
"environment.wind.speedApparent",
|
|
412
|
-
"
|
|
413
|
-
"navigation.
|
|
414
|
-
"navigation.speedOverGround",
|
|
415
|
-
"navigation.headingTrue",
|
|
416
|
-
"navigation.courseOverGroundTrue"
|
|
356
|
+
"environment.wind.speedApparent", "environment.wind.angleApparent",
|
|
357
|
+
"navigation.speedThroughWater", "navigation.speedOverGround",
|
|
358
|
+
"navigation.headingTrue", "navigation.courseOverGroundTrue"
|
|
417
359
|
];
|
|
418
360
|
|
|
419
361
|
if (twPaths.includes(path)) {
|
|
420
362
|
twDirty = true;
|
|
421
|
-
// Calcolo limitato a 10Hz per non pesare sulla CPU
|
|
422
363
|
if (twDirty && (now - lastTWCompute > 100)) {
|
|
423
364
|
computeTrueWind();
|
|
424
365
|
lastTWCompute = now;
|
|
@@ -432,20 +373,20 @@ function processIncomingData(path, val, source) {
|
|
|
432
373
|
// ==========================================================================
|
|
433
374
|
function updateWindTrend() {
|
|
434
375
|
const now = Date.now();
|
|
435
|
-
// TATTICA (Lancetta): 2s vs 10s (reazione rapida per trim)
|
|
436
376
|
const twaNow = getCircularAverageFromBuffer(store.longBuf.twa, 2000, true);
|
|
437
377
|
const twaRef = getCircularAverageFromBuffer(store.longBuf.twa, 10000, true);
|
|
438
378
|
|
|
439
|
-
// STRATEGIA (Bussola TWD): 1 min vs 30 minuti (tendenza meteo profonda)
|
|
440
379
|
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
441
380
|
const multiplier = isNavigating ? 1 : 2;
|
|
442
381
|
const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
|
|
443
382
|
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
383
|
+
|
|
444
384
|
if (!twaNow || !twaRef || !twdNow || !twdRef) return;
|
|
385
|
+
|
|
445
386
|
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
446
387
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
447
388
|
|
|
448
|
-
// TREND METEO
|
|
389
|
+
// TREND METEO
|
|
449
390
|
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
450
391
|
if (Math.abs(deltaMeteo) > 6.0) {
|
|
451
392
|
const isSouth = store.raw["navigation.position"]?.latitude < 0;
|
|
@@ -459,20 +400,17 @@ function updateWindTrend() {
|
|
|
459
400
|
}
|
|
460
401
|
} else { [compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
461
402
|
|
|
462
|
-
// TREND TATTICO
|
|
403
|
+
// TREND TATTICO
|
|
463
404
|
let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
464
405
|
const curTwaDeg = radToDeg(twaNow.val);
|
|
465
406
|
if (Math.abs(deltaTac) > 3.0) {
|
|
466
|
-
// Calcolo logica tattica: LIFT (Verde), HEADER (Rosso) o NEUTRO (Grigio)
|
|
467
407
|
let absTwa = Math.abs(curTwaDeg);
|
|
468
408
|
let tacticColor;
|
|
469
|
-
|
|
470
|
-
// Se siamo tra 75° e 105° (Traverso), il cambio è considerato neutro
|
|
471
409
|
if (absTwa > 75 && absTwa < 105) {
|
|
472
|
-
tacticColor = "#bbb";
|
|
410
|
+
tacticColor = "#bbb";
|
|
473
411
|
} else {
|
|
474
412
|
let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
|
|
475
|
-
if (absTwa >= 90) isLift = !isLift;
|
|
413
|
+
if (absTwa >= 90) isLift = !isLift;
|
|
476
414
|
tacticColor = isLift ? "#27ae60" : "#c0392b";
|
|
477
415
|
}
|
|
478
416
|
if (deltaTac > 0) {
|
|
@@ -484,100 +422,45 @@ function updateWindTrend() {
|
|
|
484
422
|
}
|
|
485
423
|
} else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
486
424
|
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
425
|
+
// ALLARME STRAMBATA (EMA FILTERED)
|
|
426
|
+
const instTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
427
|
+
if (instTwaRad !== undefined) {
|
|
428
|
+
const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
|
|
429
|
+
const currentSin = Math.sin(instTwaRad);
|
|
430
|
+
const currentCos = Math.cos(instTwaRad);
|
|
493
431
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
firstEmaRun = false;
|
|
501
|
-
} else {
|
|
502
|
-
// Media Esponenziale Vettoriale
|
|
503
|
-
emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
|
|
504
|
-
emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
|
|
505
|
-
}
|
|
432
|
+
if (firstEmaRun) {
|
|
433
|
+
emaTwaSin = currentSin; emaTwaCos = currentCos; firstEmaRun = false;
|
|
434
|
+
} else {
|
|
435
|
+
emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
|
|
436
|
+
emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
|
|
437
|
+
}
|
|
506
438
|
|
|
507
|
-
|
|
508
|
-
const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
|
|
439
|
+
const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
|
|
509
440
|
|
|
510
|
-
|
|
511
|
-
if (Math.
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
|
|
517
|
-
}
|
|
441
|
+
if (Math.abs(smoothedTwaDeg) > 155) {
|
|
442
|
+
if (Math.sign(smoothedTwaDeg) !== Math.sign(lastInstantTwa) && lastInstantTwa !== null) {
|
|
443
|
+
if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
|
|
444
|
+
lastGybeAlarmTime = now;
|
|
445
|
+
playGybeAlarm();
|
|
446
|
+
console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
|
|
518
447
|
}
|
|
519
448
|
}
|
|
520
|
-
lastInstantTwa = smoothedTwaDeg;
|
|
521
449
|
}
|
|
450
|
+
lastInstantTwa = smoothedTwaDeg;
|
|
451
|
+
}
|
|
522
452
|
}
|
|
523
453
|
|
|
524
454
|
// ==========================================================================
|
|
525
455
|
// 7. RENDERING ENGINE E AGGIORNAMENTO UI
|
|
526
456
|
// ==========================================================================
|
|
527
|
-
/**
|
|
528
|
-
* refreshGraph: Recupera i dati corretti dallo store e coordina il disegno del grafico.
|
|
529
|
-
* Gestisce lo switch tra TWS/AWS e la mappatura VMG -> SOG.
|
|
530
|
-
*
|
|
531
|
-
* @param {string} t - Il tipo di dato da aggiornare ('stw', 'sog', 'depth', 'tws', 'vmg')
|
|
532
|
-
*/
|
|
533
|
-
function refreshGraph(t) {
|
|
534
|
-
// 1. Mappatura del box UI: il VMG condivide il riquadro fisico del SOG
|
|
535
|
-
const boxType = (t === 'vmg') ? 'sog' : t;
|
|
536
|
-
|
|
537
|
-
// 2. Selezione della sorgente dati corretta
|
|
538
|
-
let data;
|
|
539
|
-
if (t === 'tws' && displayModeTws === 'AWS') {
|
|
540
|
-
// Se siamo nel box vento e la modalità è AWS, carichiamo la storia dell'apparente
|
|
541
|
-
data = store.histories['aws'];
|
|
542
|
-
} else {
|
|
543
|
-
// Altrimenti carichiamo la storia standard (TWS, STW, SOG, Depth, VMG)
|
|
544
|
-
data = store.histories[t];
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// 3. Controllo integrità: se non ci sono dati sufficienti, non disegniamo nulla
|
|
548
|
-
if (!data || data.length < 2) return;
|
|
549
|
-
|
|
550
|
-
// 4. Configurazione Scala: recupera la modalità (standard/hercules) e calcola min/max
|
|
551
|
-
const mode = graphModes[boxType];
|
|
552
|
-
const cfg = calculateScale(boxType, data, mode);
|
|
553
|
-
|
|
554
|
-
// 5. Aggiornamento estetico del Box: aggiunge lo sfondo speciale se in modalità Hercules
|
|
555
|
-
const box = document.querySelector(`.box-${boxType}`);
|
|
556
|
-
if (box) {
|
|
557
|
-
box.classList.toggle('box-hercules', mode === 'hercules');
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// 6. Aggiornamento etichette numeriche della scala (Y-axis)
|
|
561
|
-
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
562
|
-
|
|
563
|
-
// 7. Render finale del grafico SVG
|
|
564
|
-
// Il parametro 't === tws' indica a drawGraph di attivare la logica dei colori Reef (Rosso/Arancio)
|
|
565
|
-
drawGraph(
|
|
566
|
-
data,
|
|
567
|
-
boxType + '-graph',
|
|
568
|
-
cfg.min,
|
|
569
|
-
cfg.max,
|
|
570
|
-
t === 'tws',
|
|
571
|
-
mode === 'hercules'
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
457
|
|
|
575
458
|
/**
|
|
576
|
-
* upUI: Aggiornamento valori digitali
|
|
459
|
+
* upUI: Aggiornamento valori digitali
|
|
577
460
|
*/
|
|
578
461
|
const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
579
462
|
if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
|
|
580
|
-
el.innerHTML = "---°";
|
|
463
|
+
el.innerHTML = "---°";
|
|
581
464
|
el.classList.remove('unstable-data');
|
|
582
465
|
} else {
|
|
583
466
|
let valDeg = Math.round(radToDeg(obj.val));
|
|
@@ -586,7 +469,6 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
586
469
|
el.innerHTML = mainVal + dev;
|
|
587
470
|
|
|
588
471
|
let diff = Math.abs((radToDeg(instantRaw) - radToDeg(obj.val) + 540) % 360 - 180);
|
|
589
|
-
// Allarme lampeggio solo se in navigazione E (R bassa O deviazione alta O salto istantaneo brusco)
|
|
590
472
|
if (isNavigating && (!obj.stable || obj.dev > CONFIG.averaging.stabilityBreakout || diff > CONFIG.averaging.stabilityBreakout)) el.classList.add('unstable-data');
|
|
591
473
|
else el.classList.remove('unstable-data');
|
|
592
474
|
}
|
|
@@ -594,29 +476,40 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
594
476
|
|
|
595
477
|
/**
|
|
596
478
|
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
597
|
-
* Gestisce la gerarchia di aggiornamento Live (1s), Heavy (2s) e Slow (3s).
|
|
598
479
|
*/
|
|
599
480
|
function startDisplayLoop() {
|
|
600
481
|
renderInterval = setInterval(() => {
|
|
601
482
|
const now = Date.now();
|
|
602
483
|
const isNight = document.body.classList.contains('night-mode');
|
|
603
484
|
|
|
604
|
-
// Conversione velocità da m/s a Nodi
|
|
605
485
|
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
|
|
606
486
|
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
607
487
|
|
|
608
|
-
// Verifica stato navigazione basato su soglia impostata (minSpeed)
|
|
609
488
|
isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
|
|
610
489
|
|
|
611
|
-
// ---
|
|
612
|
-
|
|
490
|
+
// --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
|
|
491
|
+
const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
492
|
+
const requiredMs = viewportMinutes * 60000;
|
|
493
|
+
const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
|
|
494
|
+
|
|
495
|
+
if (oldestStw) {
|
|
496
|
+
const availableMs = now - oldestStw.time;
|
|
497
|
+
if (availableMs >= requiredMs) {
|
|
498
|
+
ui.status.innerText = `ONLINE ${viewportMinutes}min`;
|
|
499
|
+
} else {
|
|
500
|
+
const availableMin = Math.max(1, Math.floor(availableMs / 60000));
|
|
501
|
+
ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
ui.status.innerText = `ONLINE`;
|
|
505
|
+
}
|
|
506
|
+
ui.status.className = (socket && socket.readyState === WebSocket.OPEN) ? "online" : "offline";
|
|
507
|
+
|
|
508
|
+
// --- WATCHDOG: CONTROLLO TIMEOUT ---
|
|
613
509
|
const watch = {
|
|
614
|
-
"navigation.speedThroughWater": ui.stw,
|
|
615
|
-
"navigation.
|
|
616
|
-
"
|
|
617
|
-
"navigation.courseOverGroundTrue": ui.cog,
|
|
618
|
-
"environment.wind.speedApparent": ui.awsSvg,
|
|
619
|
-
"environment.depth.belowTransducer": ui.depth,
|
|
510
|
+
"navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
|
|
511
|
+
"navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
|
|
512
|
+
"environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
|
|
620
513
|
"environment.wind.speedTrue": ui.tws
|
|
621
514
|
};
|
|
622
515
|
for (let p in watch) {
|
|
@@ -628,51 +521,34 @@ function startDisplayLoop() {
|
|
|
628
521
|
// --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
|
|
629
522
|
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
630
523
|
ui.stw.innerText = stwKts.toFixed(1);
|
|
631
|
-
|
|
632
|
-
ui.stw.style.color = "";
|
|
524
|
+
ui.stw.style.color = ""; // Neutro
|
|
633
525
|
manageHistory('stw', stwKts);
|
|
634
526
|
}
|
|
635
527
|
|
|
636
|
-
// --- LOGICA SOG / VMG
|
|
528
|
+
// --- LOGICA SOG / VMG ---
|
|
637
529
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
// Registriamo i dati nelle storie (per i grafici)
|
|
642
|
-
manageHistory('vmg', vmg);
|
|
530
|
+
const vmgVal = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
531
|
+
manageHistory('vmg', vmgVal);
|
|
643
532
|
manageHistory('sog', sogKts);
|
|
644
533
|
|
|
645
|
-
const
|
|
646
|
-
|
|
534
|
+
const labelSogVmg = document.getElementById('sog-vmg-label');
|
|
647
535
|
if (displayModeSog === 'VMG') {
|
|
648
|
-
|
|
649
|
-
ui.sog.
|
|
650
|
-
|
|
651
|
-
if (labelEl) labelEl.textContent = 'VMG';
|
|
536
|
+
ui.sog.innerText = vmgVal.toFixed(1);
|
|
537
|
+
ui.sog.style.setProperty('color', '#00b8d4', 'important'); // Cyan
|
|
538
|
+
if (labelSogVmg) labelSogVmg.textContent = 'VMG';
|
|
652
539
|
} else {
|
|
653
|
-
// MODALITÀ SOG: Mostra valore istantaneo
|
|
654
540
|
ui.sog.innerText = sogKts.toFixed(1);
|
|
655
|
-
if (
|
|
541
|
+
if (labelSogVmg) labelSogVmg.textContent = 'SOG';
|
|
656
542
|
|
|
657
|
-
// --- LOGICA COLORE DINAMICO (Solo se in navigazione reale) ---
|
|
658
543
|
if (isNavigating) {
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
ui.sog.style.setProperty('color', '#ff3b30', 'important');
|
|
667
|
-
} else if (deltaCurrent > 0.3) {
|
|
668
|
-
// CORRENTE A FAVORE: Verde Neon (Feedback Positivo)
|
|
669
|
-
ui.sog.style.setProperty('color', '#00C851', 'important');
|
|
670
|
-
} else {
|
|
671
|
-
// NEUTRO: Amber (Colore base della linea SOG)
|
|
672
|
-
ui.sog.style.setProperty('color', '#ffbb33', 'important');
|
|
673
|
-
}
|
|
544
|
+
const lastSog = store.histories.sog.length > 0 ? store.histories.sog[store.histories.sog.length - 1].val : sogKts;
|
|
545
|
+
const lastStw = store.histories.stw.length > 0 ? store.histories.stw[store.histories.stw.length - 1].val : stwKts;
|
|
546
|
+
const drift = lastSog - lastStw;
|
|
547
|
+
|
|
548
|
+
if (drift < -0.3) ui.sog.style.setProperty('color', '#ff3b30', 'important'); // Contro
|
|
549
|
+
else if (drift > 0.3) ui.sog.style.setProperty('color', '#00C851', 'important'); // Favore
|
|
550
|
+
else ui.sog.style.setProperty('color', '#ffbb33', 'important'); // Neutro SOG
|
|
674
551
|
} else {
|
|
675
|
-
// NON IN NAVIGAZIONE: Riportiamo il colore al default neutro
|
|
676
552
|
ui.sog.style.color = "";
|
|
677
553
|
}
|
|
678
554
|
}
|
|
@@ -681,65 +557,50 @@ function startDisplayLoop() {
|
|
|
681
557
|
// --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
|
|
682
558
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
683
559
|
ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
|
|
684
|
-
// Il colore neutro/allarme è gestito internamente dalla funzione checkDepthAlarm
|
|
685
560
|
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
686
561
|
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
687
562
|
}
|
|
688
563
|
|
|
689
|
-
// --- GESTIONE VENTO (TWS / AWS SWITCH
|
|
690
|
-
const
|
|
691
|
-
const
|
|
564
|
+
// --- GESTIONE VENTO (TWS / AWS SWITCH) ---
|
|
565
|
+
const twsVal = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
566
|
+
const awsVal = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
|
|
692
567
|
|
|
693
|
-
|
|
694
|
-
if (store.raw["environment.wind.
|
|
695
|
-
if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsKts);
|
|
568
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsVal);
|
|
569
|
+
if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsVal);
|
|
696
570
|
|
|
697
571
|
if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
698
|
-
const
|
|
699
|
-
const
|
|
572
|
+
const labelWind = document.getElementById('tws-aws-label');
|
|
573
|
+
const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
|
|
700
574
|
|
|
701
|
-
ui.tws.innerText =
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
ui.tws.style.setProperty('color', '#ff9800', 'important'); // Arancio Reef 1
|
|
575
|
+
ui.tws.innerText = currentWind.toFixed(1);
|
|
576
|
+
if (labelWind) labelWind.textContent = displayModeTws;
|
|
577
|
+
|
|
578
|
+
if (currentWind >= CONFIG.graphs.reef2) {
|
|
579
|
+
ui.tws.style.setProperty('color', '#ff3b30', 'important');
|
|
580
|
+
} else if (currentWind >= CONFIG.graphs.reef1) {
|
|
581
|
+
ui.tws.style.setProperty('color', '#ff9800', 'important');
|
|
709
582
|
} else {
|
|
710
|
-
// Colore di base differenziato per modalità (Indaco per AWS, Navy per TWS)
|
|
711
583
|
if (displayModeTws === 'AWS') {
|
|
712
|
-
ui.tws.style.setProperty('color', '#5c6bc0', 'important');
|
|
584
|
+
ui.tws.style.setProperty('color', '#5c6bc0', 'important');
|
|
713
585
|
} else {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
ui.tws.style.setProperty('color', twsColor, 'important');
|
|
586
|
+
const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
|
|
587
|
+
ui.tws.style.setProperty('color', navyNight, 'important');
|
|
717
588
|
}
|
|
718
589
|
}
|
|
719
590
|
}
|
|
720
591
|
|
|
721
|
-
// --- AGGIORNAMENTO NUMERO AWS CENTRALE (Bussola) ---
|
|
722
592
|
if (store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
723
|
-
const awsVal = msToKts(store.raw["environment.wind.speedApparent"]);
|
|
724
593
|
ui.awsSvg.textContent = awsVal.toFixed(1);
|
|
725
594
|
}
|
|
726
595
|
|
|
727
|
-
// --- PUNTATORI ANALOGICI
|
|
596
|
+
// --- PUNTATORI ANALOGICI ---
|
|
728
597
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
|
|
729
598
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
|
|
730
|
-
if (smAwa) {
|
|
731
|
-
|
|
732
|
-
ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`);
|
|
733
|
-
}
|
|
734
|
-
if (smTwa) {
|
|
735
|
-
curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val));
|
|
736
|
-
ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`);
|
|
737
|
-
}
|
|
599
|
+
if (smAwa) ui.awa.setAttribute('transform', `rotate(${curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val))}, 200, 200)`);
|
|
600
|
+
if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
|
|
738
601
|
|
|
739
|
-
// --- CALCOLO LEEWAY E TRACK POINTER ---
|
|
740
602
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
741
603
|
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
742
|
-
// Filtraggio scarroccio: azzeramento se barca ferma, altrimenti smoothing
|
|
743
604
|
smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
744
605
|
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
745
606
|
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
@@ -747,16 +608,12 @@ function startDisplayLoop() {
|
|
|
747
608
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
748
609
|
}
|
|
749
610
|
|
|
750
|
-
//
|
|
751
|
-
updateWindTrend();
|
|
752
|
-
|
|
753
|
-
// TIER HEAVY (2s) - Aggiornamento Grafici e Salvataggio Stato
|
|
611
|
+
// --- HEAVY TIER (2s) E SLOW TIER (3s) ---
|
|
754
612
|
if (lastAvgUIUpdate++ % 2 === 0) {
|
|
755
613
|
['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
|
|
756
614
|
saveDashboardState();
|
|
757
615
|
}
|
|
758
616
|
|
|
759
|
-
// TIER SLOW (3s) - Calcolo Medie Lunghe e Tack Strategico
|
|
760
617
|
if (lastAvgUIUpdate % 3 === 0) {
|
|
761
618
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
|
|
762
619
|
let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
|
|
@@ -764,41 +621,31 @@ function startDisplayLoop() {
|
|
|
764
621
|
let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
|
|
765
622
|
let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
|
|
766
623
|
|
|
767
|
-
// Aggiornamento interfaccia per i valori mediati (Heading, Cog, Awa, Twa, Twd)
|
|
768
624
|
upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
|
|
769
625
|
upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
|
|
770
626
|
upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
|
|
771
627
|
upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
|
|
772
628
|
upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
|
|
773
629
|
|
|
774
|
-
// --- LOGICA TACK STRATEGICA (VETTORIALE) ---
|
|
775
630
|
if (hObj && twdObj) {
|
|
776
631
|
const reflectAngle = (targetRad, axisRad) => {
|
|
777
632
|
const dS = Math.sin(axisRad - targetRad);
|
|
778
633
|
const dC = Math.cos(axisRad - targetRad);
|
|
779
|
-
return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS,
|
|
780
|
-
Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
|
|
634
|
+
return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
|
|
781
635
|
};
|
|
782
|
-
|
|
783
636
|
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
784
|
-
|
|
785
|
-
if (
|
|
786
|
-
|
|
787
|
-
} else if (unstableH) {
|
|
788
|
-
ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data');
|
|
789
|
-
} else {
|
|
637
|
+
if (!isNavigating) ui.tackHdg.innerHTML = "---°";
|
|
638
|
+
else if (unstableH) { ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data'); }
|
|
639
|
+
else {
|
|
790
640
|
const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
|
|
791
641
|
ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}°`;
|
|
792
642
|
ui.tackHdg.classList.remove('unstable-data');
|
|
793
643
|
}
|
|
794
|
-
|
|
795
644
|
if (cObj) {
|
|
796
645
|
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
797
|
-
if (!isNavigating)
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data');
|
|
801
|
-
} else {
|
|
646
|
+
if (!isNavigating) ui.tackCog.innerHTML = "---°";
|
|
647
|
+
else if (unstableC) { ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data'); }
|
|
648
|
+
else {
|
|
802
649
|
const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
|
|
803
650
|
ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}°`;
|
|
804
651
|
ui.tackCog.classList.remove('unstable-data');
|
|
@@ -806,7 +653,6 @@ function startDisplayLoop() {
|
|
|
806
653
|
}
|
|
807
654
|
}
|
|
808
655
|
|
|
809
|
-
// Rotazione Mini-Icone nella bussola TWD (Mini-Bussole)
|
|
810
656
|
const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
|
|
811
657
|
const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
812
658
|
if (smHdgIcons && smTwdIcons) {
|
|
@@ -822,53 +668,57 @@ function startDisplayLoop() {
|
|
|
822
668
|
// ==========================================================================
|
|
823
669
|
// 8. CONFIGURAZIONE E GRAFICI UTILS
|
|
824
670
|
// ==========================================================================
|
|
825
|
-
/**
|
|
826
|
-
* Recupera la configurazione dal server e applica migrazioni automatiche per le vecchie versioni
|
|
827
|
-
*/
|
|
828
671
|
async function fetchServerConfig() {
|
|
829
672
|
try {
|
|
830
673
|
const response = await fetch('/rotevista-config');
|
|
831
674
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
832
675
|
const data = await response.json();
|
|
833
676
|
|
|
834
|
-
// Stampa di debug per verificare cosa riceve il client
|
|
835
|
-
console.log("🔍 Configurazione ricevuta dal Server:", data);
|
|
836
|
-
|
|
837
|
-
// Merge intelligente dei dati ricevuti
|
|
838
677
|
Object.assign(CONFIG.alarms, data.alarms || {});
|
|
839
678
|
Object.assign(CONFIG.graphs, data.graphs || {});
|
|
840
679
|
Object.assign(CONFIG.averaging, data.averaging || {});
|
|
841
680
|
|
|
842
|
-
// --- LOGICA DI MIGRAZIONE SILENZIOSA ---
|
|
843
|
-
// Se il valore ricevuto è il vecchio default (0.85) o inferiore, lo portiamo al nuovo standard 0.95.
|
|
844
|
-
// Questo è necessario perché con i nuovi filtri "Soft" lo 0.85 non farebbe quasi mai lampeggiare gli allarmi.
|
|
845
681
|
if (CONFIG.averaging.stabilityThreshold <= 0.85) {
|
|
846
682
|
CONFIG.averaging.stabilityThreshold = 0.95;
|
|
847
683
|
console.log("♻️ Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85). Aggiornato a 0.95 per ottimizzazione filtri.");
|
|
848
684
|
}
|
|
849
685
|
|
|
850
|
-
// Per le scale, siccome sono nidificate, facciamo un loop di merge profondo
|
|
851
686
|
if (data.scales) {
|
|
852
687
|
for (let key in data.scales) {
|
|
853
688
|
if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
|
|
854
689
|
}
|
|
855
690
|
}
|
|
856
|
-
|
|
857
|
-
console.log("✅ Configurazione applicata. Stabilità attiva:", CONFIG.averaging.stabilityThreshold);
|
|
858
691
|
} catch (err) {
|
|
859
692
|
console.warn("⚠️ Utilizzo default locali. Motivo:", err.message);
|
|
860
693
|
}
|
|
861
694
|
}
|
|
862
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Rielaborazione Storica Time-Based (Rimuove accumulo e gestisce il Pruning dinamicamente)
|
|
698
|
+
*/
|
|
863
699
|
function manageHistory(t, v) {
|
|
864
|
-
const n = Date.now()
|
|
700
|
+
const n = Date.now();
|
|
701
|
+
const interval = (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
702
|
+
|
|
865
703
|
if (!store.graphTempBuf[t]) store.graphTempBuf[t] = [];
|
|
866
704
|
store.graphTempBuf[t].push(v);
|
|
705
|
+
|
|
867
706
|
if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
|
|
868
707
|
const avg = store.graphTempBuf[t].reduce((a, b) => a + b, 0) / store.graphTempBuf[t].length;
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
store.
|
|
708
|
+
|
|
709
|
+
// NUOVO STORAGE TIME-BASED
|
|
710
|
+
store.histories[t].push({ time: n, val: avg });
|
|
711
|
+
|
|
712
|
+
// PRUNING DINAMICO
|
|
713
|
+
const maxViewportMinutes = CONFIG.graphs.historyMinutes * 2;
|
|
714
|
+
const maxHistoryMs = (maxViewportMinutes * 60000) + 30000;
|
|
715
|
+
|
|
716
|
+
while (store.histories[t].length > 0 && (n - store.histories[t][0].time) > maxHistoryMs) {
|
|
717
|
+
store.histories[t].shift();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
store.graphTempBuf[t] = [];
|
|
721
|
+
store.lastUpdates[t] = n;
|
|
872
722
|
}
|
|
873
723
|
}
|
|
874
724
|
|
|
@@ -888,261 +738,202 @@ function updateScaleLabels(t, min, max) {
|
|
|
888
738
|
}
|
|
889
739
|
|
|
890
740
|
/**
|
|
891
|
-
*
|
|
892
|
-
* drawGraph: Motore di Rendering SVG "Tactical Precision" (Integrale)
|
|
893
|
-
* ==========================================================================
|
|
894
|
-
* Caratteristiche:
|
|
895
|
-
* - Linea dinamica: 1.0px (base) / 1.5px (allerta).
|
|
896
|
-
* - Area segmentata: colore preciso sotto i picchi (0.15 / 0.45 / 0.85).
|
|
897
|
-
* - Fix Colore Bleeding: Usa 'userSpaceOnUse' per mantenere i colori al loro posto.
|
|
898
|
-
* - Fix Diagonale: Chiusura verticale sull'ultimo punto reale.
|
|
899
|
-
* - Palette Vivid Glass: Colori distinti per ogni modalità.
|
|
741
|
+
* refreshGraph: Recupero dati, switch AWS/TWS e passaggio a motore grafico.
|
|
900
742
|
*/
|
|
743
|
+
function refreshGraph(t) {
|
|
744
|
+
const boxType = (t === 'vmg') ? 'sog' : t;
|
|
745
|
+
let rawData;
|
|
746
|
+
|
|
747
|
+
if (t === 'tws' && displayModeTws === 'AWS') {
|
|
748
|
+
rawData = store.histories['aws'];
|
|
749
|
+
} else {
|
|
750
|
+
rawData = store.histories[t];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (!rawData || rawData.length < 2) return;
|
|
754
|
+
|
|
755
|
+
// ESTRAZIONE SOLO VALORI NUMERICI per calculateScale()
|
|
756
|
+
const values = rawData.map(p => p.val);
|
|
757
|
+
const mode = graphModes[boxType];
|
|
758
|
+
const cfg = calculateScale(boxType, values, mode);
|
|
759
|
+
|
|
760
|
+
const box = document.querySelector(`.box-${boxType}`);
|
|
761
|
+
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
762
|
+
|
|
763
|
+
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
764
|
+
|
|
765
|
+
// Passiamo tutto l'array (oggetti) al nuovo motore
|
|
766
|
+
drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
767
|
+
}
|
|
768
|
+
|
|
901
769
|
/**
|
|
902
|
-
*
|
|
903
|
-
* drawGraph: Motore di Rendering Grafico SVG "Tactical Glass" (Versione Pro)
|
|
904
|
-
* ==========================================================================
|
|
905
|
-
* Questa funzione trasforma un array di dati in una sparkline SVG dinamica.
|
|
906
|
-
*
|
|
907
|
-
* Caratteristiche principali:
|
|
908
|
-
* 1. LINEA CHIRURGICA: Mantiene uno spessore di 1px costante per un look tecnico.
|
|
909
|
-
* 2. AREA DINAMICA: L'area sottesa cambia colore solo sotto i picchi di allerta.
|
|
910
|
-
* 3. ZERO ARTEFATTI:
|
|
911
|
-
* - Usa LinearGradients con 'userSpaceOnUse' per evitare trascinamenti di colore.
|
|
912
|
-
* - Elimina le righe verticali di giunzione tra i segmenti.
|
|
913
|
-
* - Chiude il tracciato verticalmente eliminando la "coda" diagonale finale.
|
|
914
|
-
* 4. MULTI-MODALITÀ: Gestisce switch AWS/TWS, SOG/VMG e allarmi Profondità.
|
|
770
|
+
* drawGraph: Motore SVG con Timeline Reale e Gestione GAP
|
|
915
771
|
*/
|
|
916
772
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
917
773
|
const svg = document.getElementById(id);
|
|
918
|
-
|
|
919
|
-
// Uscita di sicurezza se l'elemento non esiste o non ci sono abbastanza dati
|
|
920
774
|
if (!svg || d.length < 2) return;
|
|
921
775
|
|
|
922
|
-
|
|
923
|
-
const
|
|
924
|
-
const
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const colTws = "#2c3e50"; // Navy Slate (Vento Reale)
|
|
936
|
-
const colAws = "#5c6bc0"; // Electric Indigo (Vento Apparente)
|
|
937
|
-
const colDepth = "#0088cc"; // Ocean Blue (Profondità)
|
|
938
|
-
const colStw = "#00C851"; // Emerald Neon (Velocità Acqua)
|
|
939
|
-
const colSog = "#ffbb33"; // Amber (Velocità Fondo)
|
|
940
|
-
const colVmg = "#00b8d4"; // Cyan Vibrant (VMG)
|
|
941
|
-
|
|
942
|
-
/**
|
|
943
|
-
* getColorProps: Funzione interna per determinare lo stile di ogni punto.
|
|
944
|
-
* Restituisce un oggetto con {colore, opacità area, spessore linea}.
|
|
945
|
-
*/
|
|
946
|
-
const getColorProps = (val) => {
|
|
947
|
-
// Inizializziamo con i valori di default (Dati Normali)
|
|
948
|
-
let color = colTws;
|
|
949
|
-
let opacity = "0.15";
|
|
950
|
-
let stroke = "1"; // Spessore fisso a 1px richiesto
|
|
776
|
+
const w = 200, h = 40;
|
|
777
|
+
const range = max - min || 1;
|
|
778
|
+
const isDepth = (id === 'depth-graph');
|
|
779
|
+
const now = Date.now();
|
|
780
|
+
|
|
781
|
+
// VIEWPORT TEMPORALE DINAMICO
|
|
782
|
+
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
783
|
+
const viewportMs = visibleMinutes * 60000;
|
|
784
|
+
const viewportStart = now - viewportMs;
|
|
785
|
+
|
|
786
|
+
// FILTRO DATI VISIBILI (Gestisce Compressione/Zoom in modo naturale)
|
|
787
|
+
const visibleData = d.filter(p => p.time >= viewportStart);
|
|
788
|
+
if (visibleData.length < 2) return;
|
|
951
789
|
|
|
952
|
-
|
|
790
|
+
// COLORI
|
|
791
|
+
const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
|
|
792
|
+
const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
|
|
793
|
+
|
|
794
|
+
const getColorProps = (val) => {
|
|
795
|
+
let color = colTws, opacity = "0.15", stroke = "1";
|
|
953
796
|
if (isTws) {
|
|
954
|
-
// Scegliamo il colore di base a seconda se stiamo guardando Reale o Apparente
|
|
955
797
|
const baseWind = (displayModeTws === 'AWS') ? colAws : colTws;
|
|
956
|
-
|
|
957
|
-
if (val >= CONFIG.graphs.
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
else if (isDepth) {
|
|
967
|
-
if (val < CONFIG.alarms.depthDanger) {
|
|
968
|
-
color = colDanger; opacity = "0.55";
|
|
969
|
-
} else if (val < CONFIG.alarms.depthWarning) {
|
|
970
|
-
color = colWarning; opacity = "0.45";
|
|
971
|
-
} else {
|
|
972
|
-
color = colDepth;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
// C. LOGICA PER LE VELOCITÀ (STW, SOG, VMG)
|
|
976
|
-
else {
|
|
977
|
-
if (id === 'stw-graph') {
|
|
978
|
-
color = colStw;
|
|
979
|
-
} else if (id === 'sog-graph') {
|
|
980
|
-
// Il box SOG cambia colore se l'utente switcha in modalità VMG
|
|
981
|
-
color = (displayModeSog === 'VMG') ? colVmg : colSog;
|
|
982
|
-
}
|
|
798
|
+
if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke = "1.2"; }
|
|
799
|
+
else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke = "1"; }
|
|
800
|
+
else color = baseWind;
|
|
801
|
+
} else if (isDepth) {
|
|
802
|
+
if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke = "1.2"; }
|
|
803
|
+
else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke = "1"; }
|
|
804
|
+
else color = colDepth;
|
|
805
|
+
} else {
|
|
806
|
+
if (id === 'stw-graph') color = colStw;
|
|
807
|
+
else if (id === 'sog-graph') color = (displayModeSog === 'VMG') ? colVmg : colSog;
|
|
983
808
|
}
|
|
984
809
|
return { color, opacity, stroke };
|
|
985
810
|
};
|
|
986
811
|
|
|
987
|
-
//
|
|
812
|
+
// GRIGLIA ORIZZONTALE
|
|
988
813
|
let grids = "";
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
for (let m = gridInterval; m < CONFIG.graphs.historyMinutes; m += gridInterval) {
|
|
996
|
-
const x = w - (m / CONFIG.graphs.historyMinutes) * w;
|
|
814
|
+
[0.25, 0.5, 0.75].forEach(p => grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(0,0,0,0.12)" stroke-width="0.5" />`);
|
|
815
|
+
|
|
816
|
+
// GRIGLIA VERTICALE VERA
|
|
817
|
+
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
818
|
+
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
819
|
+
const x = w - ((m / visibleMinutes) * w);
|
|
997
820
|
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
998
821
|
}
|
|
999
822
|
|
|
1000
|
-
//
|
|
1001
|
-
let gradientStops = ""
|
|
1002
|
-
let
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const x1 = ((
|
|
1010
|
-
const
|
|
1011
|
-
const
|
|
1012
|
-
const y2 = h - (Math.max(0, Math.min(1, (
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
823
|
+
// RENDER TIME-BASED
|
|
824
|
+
let gradientStops = "", lines = "", areaPath = "";
|
|
825
|
+
let started = false;
|
|
826
|
+
|
|
827
|
+
for (let i = 1; i < visibleData.length; i++) {
|
|
828
|
+
const pA = visibleData[i - 1];
|
|
829
|
+
const pB = visibleData[i];
|
|
830
|
+
|
|
831
|
+
// POSIZIONE X ESATTA AL MILLISECONDO
|
|
832
|
+
const x1 = ((pA.time - viewportStart) / viewportMs) * w;
|
|
833
|
+
const x2 = ((pB.time - viewportStart) / viewportMs) * w;
|
|
834
|
+
const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
|
|
835
|
+
const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
|
|
836
|
+
|
|
837
|
+
const props = getColorProps(pB.val);
|
|
838
|
+
|
|
839
|
+
// GESTIONE GAP TEMPORALI (Network loss)
|
|
840
|
+
const deltaTime = pB.time - pA.time;
|
|
841
|
+
const expectedInterval = viewportMs / CONFIG.graphs.samples;
|
|
842
|
+
const isGap = deltaTime > (expectedInterval * 2.5);
|
|
843
|
+
|
|
844
|
+
// STOPS GRADIENTE
|
|
845
|
+
const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
|
|
846
|
+
gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
847
|
+
gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
848
|
+
|
|
849
|
+
// --- FIX: GESTIONE AREA E LINEE DURANTE I GAP ---
|
|
850
|
+
if (isGap) {
|
|
851
|
+
// Se c'è un buco nei dati e avevamo già iniziato a disegnare...
|
|
852
|
+
if (started) {
|
|
853
|
+
// Chiudiamo il pezzo di area precedente scendendo verticalmente (Z non serve qui)
|
|
854
|
+
areaPath += `L ${x1} ${h} `;
|
|
855
|
+
started = false; // Resettiamo il flag per far ripartire l'area al prossimo punto
|
|
856
|
+
}
|
|
857
|
+
} else {
|
|
858
|
+
// I dati sono continui, disegniamo la linea superiore
|
|
859
|
+
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" />`;
|
|
860
|
+
|
|
861
|
+
// Gestione Area
|
|
862
|
+
if (!started) {
|
|
863
|
+
// Iniziamo un nuovo pezzo di area dal fondo, saliamo a y1
|
|
864
|
+
areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
|
|
865
|
+
started = true;
|
|
866
|
+
}
|
|
867
|
+
// Aggiungiamo il punto attuale
|
|
868
|
+
areaPath += `L ${x2} ${y2} `;
|
|
1037
869
|
}
|
|
1038
870
|
}
|
|
1039
871
|
|
|
1040
|
-
//
|
|
1041
|
-
|
|
872
|
+
// CHIUSURA FINALE DELL'AREA (Solo se non siamo finiti dentro un gap)
|
|
873
|
+
if (started) {
|
|
874
|
+
const last = visibleData[visibleData.length - 1];
|
|
875
|
+
const lastX = ((last.time - viewportStart) / viewportMs) * w;
|
|
876
|
+
areaPath += `L ${lastX} ${h} Z`; // Z chiude automaticamente il path tornando al punto 'M' iniziale
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// GRADIENTE
|
|
880
|
+
const gradId = `grad-${id}`;
|
|
881
|
+
const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
|
|
1042
882
|
|
|
1043
|
-
|
|
1044
|
-
* IMPORTANTE: gradientUnits="userSpaceOnUse" fissa il gradiente alle coordinate
|
|
1045
|
-
* assolute (0-200px). Questo impedisce al colore di allarme di "allungarsi"
|
|
1046
|
-
* erroneamente se il grafico è solo a metà schermo.
|
|
1047
|
-
*/
|
|
1048
|
-
const defs = `
|
|
1049
|
-
<defs>
|
|
1050
|
-
<linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">
|
|
1051
|
-
${gradientStops}
|
|
1052
|
-
</linearGradient>
|
|
1053
|
-
</defs>
|
|
1054
|
-
`;
|
|
1055
|
-
|
|
1056
|
-
// --- 6. AGGIORNAMENTO FINALE DEL DOM ---
|
|
1057
|
-
// Inseriamo tutto nell'elemento SVG: Defs (Gradienti) -> Griglie -> Area (Fill) -> Linee (Stroke)
|
|
883
|
+
// RENDER FINALE
|
|
1058
884
|
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
1059
885
|
}
|
|
1060
886
|
|
|
1061
887
|
// ==========================================================================
|
|
1062
888
|
// 9. INTERAZIONI E GESTI
|
|
1063
889
|
// ==========================================================================
|
|
1064
|
-
/**
|
|
1065
|
-
* toggleFocusMode: Gestisce l'attivazione della modalità Split-Screen (Focus).
|
|
1066
|
-
* Ingrandisce un box specifico e mantiene visibile la bussola centrale.
|
|
1067
|
-
*/
|
|
1068
890
|
function toggleFocusMode(type, element) {
|
|
1069
891
|
const container = document.querySelector('.main-container');
|
|
1070
|
-
|
|
1071
|
-
// Identifica se il box appartiene alla colonna sinistra (per il layout CSS)
|
|
1072
892
|
const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
|
|
1073
|
-
|
|
1074
|
-
// Inverte lo stato del Focus
|
|
1075
893
|
isFocusActive = !isFocusActive;
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
// Applica le classi per dividere lo schermo e mettere in risalto il box
|
|
1079
|
-
container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right');
|
|
1080
|
-
element.classList.add('is-focused');
|
|
1081
|
-
} else {
|
|
1082
|
-
// Rimuove tutte le classi di focus e torna alla griglia standard
|
|
1083
|
-
container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
|
|
1084
|
-
document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
|
|
1085
|
-
}
|
|
894
|
+
if (isFocusActive) { container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right'); element.classList.add('is-focused'); }
|
|
895
|
+
else { container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right'); document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused')); }
|
|
1086
896
|
}
|
|
1087
897
|
|
|
1088
|
-
/**
|
|
1089
|
-
* Inizializzazione Gesti e Interazioni sui box con grafico.
|
|
1090
|
-
* Gestisce: Singolo Tap (Switch), Doppio Tap (Zoom), Long Press (Focus).
|
|
1091
|
-
*/
|
|
1092
898
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
1093
899
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
1094
900
|
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
1095
901
|
|
|
1096
|
-
// --- GESTIONE PRESSIONE (Inizio) ---
|
|
1097
902
|
el.addEventListener('pointerdown', (e) => {
|
|
1098
903
|
isLongPressActive = false;
|
|
1099
|
-
// Timer per attivare il Focus Mode dopo 1 secondo di pressione continua
|
|
1100
904
|
pressTimer = setTimeout(() => {
|
|
1101
905
|
if (!isFocusActive) {
|
|
1102
906
|
isLongPressActive = true;
|
|
1103
907
|
toggleFocusMode(type, el);
|
|
1104
|
-
lastTapTime = 0;
|
|
908
|
+
lastTapTime = 0;
|
|
1105
909
|
}
|
|
1106
910
|
}, 1000);
|
|
1107
911
|
});
|
|
1108
912
|
|
|
1109
|
-
// --- GESTIONE RILASCIO (Fine Gesto) ---
|
|
1110
913
|
el.addEventListener('pointerup', (e) => {
|
|
1111
|
-
clearTimeout(pressTimer);
|
|
1112
|
-
if (isLongPressActive) return;
|
|
914
|
+
clearTimeout(pressTimer);
|
|
915
|
+
if (isLongPressActive) return;
|
|
1113
916
|
|
|
1114
917
|
const currentTime = new Date().getTime();
|
|
1115
918
|
const tapDelay = currentTime - lastTapTime;
|
|
1116
919
|
|
|
1117
|
-
// 1. GESTIONE DOPPIO CLICK (Zoom Hercules)
|
|
1118
920
|
if (tapDelay < 300 && tapDelay > 0) {
|
|
1119
921
|
clearTimeout(tapTimeout);
|
|
1120
|
-
// Switch tra modalità scala Standard e Zoom Hercules
|
|
1121
922
|
graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard';
|
|
1122
923
|
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
1123
924
|
refreshGraph(type);
|
|
1124
925
|
lastTapTime = 0;
|
|
1125
|
-
}
|
|
1126
|
-
// 2. GESTIONE SINGOLO CLICK (Switch Dati o Esci dal Focus)
|
|
1127
|
-
else {
|
|
926
|
+
} else {
|
|
1128
927
|
lastTapTime = currentTime;
|
|
1129
928
|
tapTimeout = setTimeout(() => {
|
|
1130
|
-
// Se il box è in focus, il singolo click lo chiude
|
|
1131
929
|
if (isFocusActive && el.classList.contains('is-focused')) {
|
|
1132
930
|
toggleFocusMode(type, el);
|
|
1133
|
-
}
|
|
1134
|
-
// Se siamo in visualizzazione standard, gestiamo gli switch dati
|
|
1135
|
-
else if (!isFocusActive) {
|
|
1136
|
-
// Switch SOG <-> VMG
|
|
931
|
+
} else if (!isFocusActive) {
|
|
1137
932
|
if (type === 'sog') {
|
|
1138
933
|
displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
|
|
1139
|
-
}
|
|
1140
|
-
// Switch TWS <-> AWS (Nuova Logica)
|
|
1141
|
-
else if (type === 'tws') {
|
|
934
|
+
} else if (type === 'tws') {
|
|
1142
935
|
displayModeTws = (displayModeTws === 'TWS') ? 'AWS' : 'TWS';
|
|
1143
936
|
}
|
|
1144
|
-
|
|
1145
|
-
// Feedback visivo (lampeggio leggero) dell'avvenuto switch
|
|
1146
937
|
el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
|
1147
938
|
setTimeout(() => el.style.backgroundColor = "", 150);
|
|
1148
939
|
}
|
|
@@ -1150,7 +941,6 @@ function toggleFocusMode(type, element) {
|
|
|
1150
941
|
}
|
|
1151
942
|
});
|
|
1152
943
|
|
|
1153
|
-
// --- GESTIONE USCITA (Se l'utente trascina il dito fuori) ---
|
|
1154
944
|
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
1155
945
|
});
|
|
1156
946
|
|
|
@@ -1194,10 +984,7 @@ function connect() {
|
|
|
1194
984
|
const d = JSON.parse(e.data);
|
|
1195
985
|
if (d.updates) {
|
|
1196
986
|
d.updates.forEach(u => {
|
|
1197
|
-
// 1. Estraiamo il nome del sensore/sorgente (es. "yacht_device" o "Unknown")
|
|
1198
987
|
const sourceLabel = u.source ? (u.source.label || u.source.talker || "Unknown") : "Unknown";
|
|
1199
|
-
|
|
1200
|
-
// 2. Passiamo il nome della sorgente come TERZO parametro
|
|
1201
988
|
if (u.values) {
|
|
1202
989
|
u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
|
|
1203
990
|
}
|