@sailingrotevista/rotevista-dash 5.0.2 → 5.0.4
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 +323 -484
- 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
|
-
|
|
493
|
-
|
|
494
|
-
const currentSin = Math.sin(instTwaRad);
|
|
495
|
-
const currentCos = Math.cos(instTwaRad);
|
|
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);
|
|
496
431
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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) {
|
|
@@ -820,10 +666,34 @@ function startDisplayLoop() {
|
|
|
820
666
|
}
|
|
821
667
|
|
|
822
668
|
// ==========================================================================
|
|
823
|
-
// 8. CONFIGURAZIONE E GRAFICI UTILS
|
|
669
|
+
// 8. CONFIGURAZIONE, AGGIORNAMENTO LIVE E GRAFICI UTILS
|
|
824
670
|
// ==========================================================================
|
|
671
|
+
|
|
672
|
+
let currentConfigString = ""; // Memoria per rilevare cambiamenti nei settings
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
|
|
676
|
+
*/
|
|
677
|
+
function applyConfigData(data) {
|
|
678
|
+
Object.assign(CONFIG.alarms, data.alarms || {});
|
|
679
|
+
Object.assign(CONFIG.graphs, data.graphs || {});
|
|
680
|
+
Object.assign(CONFIG.averaging, data.averaging || {});
|
|
681
|
+
|
|
682
|
+
// Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85).
|
|
683
|
+
// Aggiornato a 0.95 per ottimizzazione filtri.
|
|
684
|
+
if (CONFIG.averaging.stabilityThreshold <= 0.85) {
|
|
685
|
+
CONFIG.averaging.stabilityThreshold = 0.95;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (data.scales) {
|
|
689
|
+
for (let key in data.scales) {
|
|
690
|
+
if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
825
695
|
/**
|
|
826
|
-
* Recupera la configurazione
|
|
696
|
+
* Recupera la configurazione iniziale al caricamento della pagina
|
|
827
697
|
*/
|
|
828
698
|
async function fetchServerConfig() {
|
|
829
699
|
try {
|
|
@@ -831,44 +701,75 @@ async function fetchServerConfig() {
|
|
|
831
701
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
832
702
|
const data = await response.json();
|
|
833
703
|
|
|
834
|
-
//
|
|
835
|
-
|
|
704
|
+
// Salviamo l'impronta digitale della configurazione per i confronti futuri
|
|
705
|
+
currentConfigString = JSON.stringify(data);
|
|
706
|
+
|
|
707
|
+
applyConfigData(data);
|
|
708
|
+
console.log("✅ Configurazione iniziale applicata con successo.");
|
|
709
|
+
} catch (err) {
|
|
710
|
+
console.warn("⚠️ Impossibile raggiungere il server per le configurazioni. Utilizzo default locali. Motivo:", err.message);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
836
713
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
714
|
+
/**
|
|
715
|
+
* Watchdog: Controlla in background se le impostazioni sul server sono cambiate
|
|
716
|
+
*/
|
|
717
|
+
async function watchConfigChanges() {
|
|
718
|
+
try {
|
|
719
|
+
const response = await fetch('/rotevista-config');
|
|
720
|
+
if (!response.ok) return;
|
|
721
|
+
const data = await response.json();
|
|
841
722
|
|
|
842
|
-
|
|
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
|
-
if (CONFIG.averaging.stabilityThreshold <= 0.85) {
|
|
846
|
-
CONFIG.averaging.stabilityThreshold = 0.95;
|
|
847
|
-
console.log("♻️ Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85). Aggiornato a 0.95 per ottimizzazione filtri.");
|
|
848
|
-
}
|
|
723
|
+
const newConfigString = JSON.stringify(data);
|
|
849
724
|
|
|
850
|
-
//
|
|
851
|
-
if (
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
725
|
+
// Se l'impronta digitale è diversa, l'utente ha salvato nuovi settings!
|
|
726
|
+
if (newConfigString !== currentConfigString) {
|
|
727
|
+
console.log("🔄 Rilevato cambio impostazioni su Signal K! Aggiornamento in tempo reale...");
|
|
728
|
+
|
|
729
|
+
// Applichiamo i nuovi settings al volo
|
|
730
|
+
applyConfigData(data);
|
|
731
|
+
|
|
732
|
+
// Aggiorniamo l'impronta
|
|
733
|
+
currentConfigString = newConfigString;
|
|
734
|
+
|
|
735
|
+
// FEEDBACK VISIVO: Avvisiamo l'utente dell'aggiornamento
|
|
736
|
+
ui.status.innerText = "CONFIG UPDATED!";
|
|
737
|
+
ui.status.style.color = "#00C851"; // Verde Neon
|
|
738
|
+
setTimeout(() => { ui.status.style.color = ""; }, 4000);
|
|
855
739
|
}
|
|
856
|
-
|
|
857
|
-
console.log("✅ Configurazione applicata. Stabilità attiva:", CONFIG.averaging.stabilityThreshold);
|
|
858
740
|
} catch (err) {
|
|
859
|
-
console
|
|
741
|
+
// Ignoriamo gli errori di rete nel watchdog per non intasare la console in caso di disconnessione
|
|
860
742
|
}
|
|
861
743
|
}
|
|
862
744
|
|
|
745
|
+
// --------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Rielaborazione Storica Time-Based (Rimuove accumulo e gestisce il Pruning dinamicamente)
|
|
749
|
+
*/
|
|
863
750
|
function manageHistory(t, v) {
|
|
864
|
-
const n = Date.now()
|
|
751
|
+
const n = Date.now();
|
|
752
|
+
const interval = (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
753
|
+
|
|
865
754
|
if (!store.graphTempBuf[t]) store.graphTempBuf[t] = [];
|
|
866
755
|
store.graphTempBuf[t].push(v);
|
|
756
|
+
|
|
867
757
|
if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
|
|
868
758
|
const avg = store.graphTempBuf[t].reduce((a, b) => a + b, 0) / store.graphTempBuf[t].length;
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
store.
|
|
759
|
+
|
|
760
|
+
// NUOVO STORAGE TIME-BASED
|
|
761
|
+
store.histories[t].push({ time: n, val: avg });
|
|
762
|
+
|
|
763
|
+
// PRUNING DINAMICO
|
|
764
|
+
const maxViewportMinutes = CONFIG.graphs.historyMinutes * 2;
|
|
765
|
+
const maxHistoryMs = (maxViewportMinutes * 60000) + 30000;
|
|
766
|
+
|
|
767
|
+
while (store.histories[t].length > 0 && (n - store.histories[t][0].time) > maxHistoryMs) {
|
|
768
|
+
store.histories[t].shift();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
store.graphTempBuf[t] = [];
|
|
772
|
+
store.lastUpdates[t] = n;
|
|
872
773
|
}
|
|
873
774
|
}
|
|
874
775
|
|
|
@@ -888,261 +789,202 @@ function updateScaleLabels(t, min, max) {
|
|
|
888
789
|
}
|
|
889
790
|
|
|
890
791
|
/**
|
|
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à.
|
|
792
|
+
* refreshGraph: Recupero dati, switch AWS/TWS e passaggio a motore grafico.
|
|
900
793
|
*/
|
|
794
|
+
function refreshGraph(t) {
|
|
795
|
+
const boxType = (t === 'vmg') ? 'sog' : t;
|
|
796
|
+
let rawData;
|
|
797
|
+
|
|
798
|
+
if (t === 'tws' && displayModeTws === 'AWS') {
|
|
799
|
+
rawData = store.histories['aws'];
|
|
800
|
+
} else {
|
|
801
|
+
rawData = store.histories[t];
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!rawData || rawData.length < 2) return;
|
|
805
|
+
|
|
806
|
+
// ESTRAZIONE SOLO VALORI NUMERICI per calculateScale()
|
|
807
|
+
const values = rawData.map(p => p.val);
|
|
808
|
+
const mode = graphModes[boxType];
|
|
809
|
+
const cfg = calculateScale(boxType, values, mode);
|
|
810
|
+
|
|
811
|
+
const box = document.querySelector(`.box-${boxType}`);
|
|
812
|
+
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
813
|
+
|
|
814
|
+
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
815
|
+
|
|
816
|
+
// Passiamo tutto l'array (oggetti) al nuovo motore
|
|
817
|
+
drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
818
|
+
}
|
|
819
|
+
|
|
901
820
|
/**
|
|
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à.
|
|
821
|
+
* drawGraph: Motore SVG con Timeline Reale e Gestione GAP
|
|
915
822
|
*/
|
|
916
823
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
917
824
|
const svg = document.getElementById(id);
|
|
918
|
-
|
|
919
|
-
// Uscita di sicurezza se l'elemento non esiste o non ci sono abbastanza dati
|
|
920
825
|
if (!svg || d.length < 2) return;
|
|
921
826
|
|
|
922
|
-
|
|
923
|
-
const
|
|
924
|
-
const
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
const colDanger = "#ff3b30"; // Rosso Vivido (Apple/Alert style)
|
|
932
|
-
const colWarning = "#ff9800"; // Arancio Fluo (High visibility)
|
|
933
|
-
|
|
934
|
-
// Colori Base (Sotto le soglie di allarme)
|
|
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
|
|
827
|
+
const w = 200, h = 40;
|
|
828
|
+
const range = max - min || 1;
|
|
829
|
+
const isDepth = (id === 'depth-graph');
|
|
830
|
+
const now = Date.now();
|
|
831
|
+
|
|
832
|
+
// VIEWPORT TEMPORALE DINAMICO
|
|
833
|
+
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
834
|
+
const viewportMs = visibleMinutes * 60000;
|
|
835
|
+
const viewportStart = now - viewportMs;
|
|
951
836
|
|
|
952
|
-
|
|
837
|
+
// FILTRO DATI VISIBILI (Gestisce Compressione/Zoom in modo naturale)
|
|
838
|
+
const visibleData = d.filter(p => p.time >= viewportStart);
|
|
839
|
+
if (visibleData.length < 2) return;
|
|
840
|
+
|
|
841
|
+
// COLORI
|
|
842
|
+
const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
|
|
843
|
+
const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
|
|
844
|
+
|
|
845
|
+
const getColorProps = (val) => {
|
|
846
|
+
let color = colTws, opacity = "0.15", stroke = "1";
|
|
953
847
|
if (isTws) {
|
|
954
|
-
// Scegliamo il colore di base a seconda se stiamo guardando Reale o Apparente
|
|
955
848
|
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
|
-
}
|
|
849
|
+
if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke = "1.2"; }
|
|
850
|
+
else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke = "1"; }
|
|
851
|
+
else color = baseWind;
|
|
852
|
+
} else if (isDepth) {
|
|
853
|
+
if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke = "1.2"; }
|
|
854
|
+
else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke = "1"; }
|
|
855
|
+
else color = colDepth;
|
|
856
|
+
} else {
|
|
857
|
+
if (id === 'stw-graph') color = colStw;
|
|
858
|
+
else if (id === 'sog-graph') color = (displayModeSog === 'VMG') ? colVmg : colSog;
|
|
983
859
|
}
|
|
984
860
|
return { color, opacity, stroke };
|
|
985
861
|
};
|
|
986
862
|
|
|
987
|
-
//
|
|
863
|
+
// GRIGLIA ORIZZONTALE
|
|
988
864
|
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;
|
|
865
|
+
[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" />`);
|
|
866
|
+
|
|
867
|
+
// GRIGLIA VERTICALE VERA
|
|
868
|
+
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
869
|
+
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
870
|
+
const x = w - ((m / visibleMinutes) * w);
|
|
997
871
|
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
998
872
|
}
|
|
999
873
|
|
|
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
|
-
|
|
874
|
+
// RENDER TIME-BASED
|
|
875
|
+
let gradientStops = "", lines = "", areaPath = "";
|
|
876
|
+
let started = false;
|
|
877
|
+
|
|
878
|
+
for (let i = 1; i < visibleData.length; i++) {
|
|
879
|
+
const pA = visibleData[i - 1];
|
|
880
|
+
const pB = visibleData[i];
|
|
881
|
+
|
|
882
|
+
// POSIZIONE X ESATTA AL MILLISECONDO
|
|
883
|
+
const x1 = ((pA.time - viewportStart) / viewportMs) * w;
|
|
884
|
+
const x2 = ((pB.time - viewportStart) / viewportMs) * w;
|
|
885
|
+
const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
|
|
886
|
+
const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
|
|
887
|
+
|
|
888
|
+
const props = getColorProps(pB.val);
|
|
889
|
+
|
|
890
|
+
// GESTIONE GAP TEMPORALI (Network loss)
|
|
891
|
+
const deltaTime = pB.time - pA.time;
|
|
892
|
+
const expectedInterval = viewportMs / CONFIG.graphs.samples;
|
|
893
|
+
const isGap = deltaTime > (expectedInterval * 2.5);
|
|
894
|
+
|
|
895
|
+
// STOPS GRADIENTE
|
|
896
|
+
const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
|
|
897
|
+
gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
898
|
+
gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
899
|
+
|
|
900
|
+
// --- FIX: GESTIONE AREA E LINEE DURANTE I GAP ---
|
|
901
|
+
if (isGap) {
|
|
902
|
+
// Se c'è un buco nei dati e avevamo già iniziato a disegnare...
|
|
903
|
+
if (started) {
|
|
904
|
+
// Chiudiamo il pezzo di area precedente scendendo verticalmente (Z non serve qui)
|
|
905
|
+
areaPath += `L ${x1} ${h} `;
|
|
906
|
+
started = false; // Resettiamo il flag per far ripartire l'area al prossimo punto
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
// I dati sono continui, disegniamo la linea superiore
|
|
910
|
+
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" />`;
|
|
911
|
+
|
|
912
|
+
// Gestione Area
|
|
913
|
+
if (!started) {
|
|
914
|
+
// Iniziamo un nuovo pezzo di area dal fondo, saliamo a y1
|
|
915
|
+
areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
|
|
916
|
+
started = true;
|
|
917
|
+
}
|
|
918
|
+
// Aggiungiamo il punto attuale
|
|
919
|
+
areaPath += `L ${x2} ${y2} `;
|
|
1037
920
|
}
|
|
1038
921
|
}
|
|
1039
922
|
|
|
1040
|
-
//
|
|
1041
|
-
|
|
923
|
+
// CHIUSURA FINALE DELL'AREA (Solo se non siamo finiti dentro un gap)
|
|
924
|
+
if (started) {
|
|
925
|
+
const last = visibleData[visibleData.length - 1];
|
|
926
|
+
const lastX = ((last.time - viewportStart) / viewportMs) * w;
|
|
927
|
+
areaPath += `L ${lastX} ${h} Z`; // Z chiude automaticamente il path tornando al punto 'M' iniziale
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// GRADIENTE
|
|
931
|
+
const gradId = `grad-${id}`;
|
|
932
|
+
const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
|
|
1042
933
|
|
|
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)
|
|
934
|
+
// RENDER FINALE
|
|
1058
935
|
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
1059
936
|
}
|
|
1060
937
|
|
|
1061
938
|
// ==========================================================================
|
|
1062
939
|
// 9. INTERAZIONI E GESTI
|
|
1063
940
|
// ==========================================================================
|
|
1064
|
-
/**
|
|
1065
|
-
* toggleFocusMode: Gestisce l'attivazione della modalità Split-Screen (Focus).
|
|
1066
|
-
* Ingrandisce un box specifico e mantiene visibile la bussola centrale.
|
|
1067
|
-
*/
|
|
1068
941
|
function toggleFocusMode(type, element) {
|
|
1069
942
|
const container = document.querySelector('.main-container');
|
|
1070
|
-
|
|
1071
|
-
// Identifica se il box appartiene alla colonna sinistra (per il layout CSS)
|
|
1072
943
|
const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
|
|
1073
|
-
|
|
1074
|
-
// Inverte lo stato del Focus
|
|
1075
944
|
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
|
-
}
|
|
945
|
+
if (isFocusActive) { container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right'); element.classList.add('is-focused'); }
|
|
946
|
+
else { container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right'); document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused')); }
|
|
1086
947
|
}
|
|
1087
948
|
|
|
1088
|
-
/**
|
|
1089
|
-
* Inizializzazione Gesti e Interazioni sui box con grafico.
|
|
1090
|
-
* Gestisce: Singolo Tap (Switch), Doppio Tap (Zoom), Long Press (Focus).
|
|
1091
|
-
*/
|
|
1092
949
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
1093
950
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
1094
951
|
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
1095
952
|
|
|
1096
|
-
// --- GESTIONE PRESSIONE (Inizio) ---
|
|
1097
953
|
el.addEventListener('pointerdown', (e) => {
|
|
1098
954
|
isLongPressActive = false;
|
|
1099
|
-
// Timer per attivare il Focus Mode dopo 1 secondo di pressione continua
|
|
1100
955
|
pressTimer = setTimeout(() => {
|
|
1101
956
|
if (!isFocusActive) {
|
|
1102
957
|
isLongPressActive = true;
|
|
1103
958
|
toggleFocusMode(type, el);
|
|
1104
|
-
lastTapTime = 0;
|
|
959
|
+
lastTapTime = 0;
|
|
1105
960
|
}
|
|
1106
961
|
}, 1000);
|
|
1107
962
|
});
|
|
1108
963
|
|
|
1109
|
-
// --- GESTIONE RILASCIO (Fine Gesto) ---
|
|
1110
964
|
el.addEventListener('pointerup', (e) => {
|
|
1111
|
-
clearTimeout(pressTimer);
|
|
1112
|
-
if (isLongPressActive) return;
|
|
965
|
+
clearTimeout(pressTimer);
|
|
966
|
+
if (isLongPressActive) return;
|
|
1113
967
|
|
|
1114
968
|
const currentTime = new Date().getTime();
|
|
1115
969
|
const tapDelay = currentTime - lastTapTime;
|
|
1116
970
|
|
|
1117
|
-
// 1. GESTIONE DOPPIO CLICK (Zoom Hercules)
|
|
1118
971
|
if (tapDelay < 300 && tapDelay > 0) {
|
|
1119
972
|
clearTimeout(tapTimeout);
|
|
1120
|
-
// Switch tra modalità scala Standard e Zoom Hercules
|
|
1121
973
|
graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard';
|
|
1122
974
|
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
1123
975
|
refreshGraph(type);
|
|
1124
976
|
lastTapTime = 0;
|
|
1125
|
-
}
|
|
1126
|
-
// 2. GESTIONE SINGOLO CLICK (Switch Dati o Esci dal Focus)
|
|
1127
|
-
else {
|
|
977
|
+
} else {
|
|
1128
978
|
lastTapTime = currentTime;
|
|
1129
979
|
tapTimeout = setTimeout(() => {
|
|
1130
|
-
// Se il box è in focus, il singolo click lo chiude
|
|
1131
980
|
if (isFocusActive && el.classList.contains('is-focused')) {
|
|
1132
981
|
toggleFocusMode(type, el);
|
|
1133
|
-
}
|
|
1134
|
-
// Se siamo in visualizzazione standard, gestiamo gli switch dati
|
|
1135
|
-
else if (!isFocusActive) {
|
|
1136
|
-
// Switch SOG <-> VMG
|
|
982
|
+
} else if (!isFocusActive) {
|
|
1137
983
|
if (type === 'sog') {
|
|
1138
984
|
displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
|
|
1139
|
-
}
|
|
1140
|
-
// Switch TWS <-> AWS (Nuova Logica)
|
|
1141
|
-
else if (type === 'tws') {
|
|
985
|
+
} else if (type === 'tws') {
|
|
1142
986
|
displayModeTws = (displayModeTws === 'TWS') ? 'AWS' : 'TWS';
|
|
1143
987
|
}
|
|
1144
|
-
|
|
1145
|
-
// Feedback visivo (lampeggio leggero) dell'avvenuto switch
|
|
1146
988
|
el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
|
1147
989
|
setTimeout(() => el.style.backgroundColor = "", 150);
|
|
1148
990
|
}
|
|
@@ -1150,7 +992,6 @@ function toggleFocusMode(type, element) {
|
|
|
1150
992
|
}
|
|
1151
993
|
});
|
|
1152
994
|
|
|
1153
|
-
// --- GESTIONE USCITA (Se l'utente trascina il dito fuori) ---
|
|
1154
995
|
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
1155
996
|
});
|
|
1156
997
|
|
|
@@ -1194,10 +1035,7 @@ function connect() {
|
|
|
1194
1035
|
const d = JSON.parse(e.data);
|
|
1195
1036
|
if (d.updates) {
|
|
1196
1037
|
d.updates.forEach(u => {
|
|
1197
|
-
// 1. Estraiamo il nome del sensore/sorgente (es. "yacht_device" o "Unknown")
|
|
1198
1038
|
const sourceLabel = u.source ? (u.source.label || u.source.talker || "Unknown") : "Unknown";
|
|
1199
|
-
|
|
1200
|
-
// 2. Passiamo il nome della sorgente come TERZO parametro
|
|
1201
1039
|
if (u.values) {
|
|
1202
1040
|
u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
|
|
1203
1041
|
}
|
|
@@ -1231,6 +1069,7 @@ async function init() {
|
|
|
1231
1069
|
await fetchServerConfig();
|
|
1232
1070
|
startDisplayLoop();
|
|
1233
1071
|
connect();
|
|
1072
|
+
setInterval(watchConfigChanges, 10000);
|
|
1234
1073
|
}
|
|
1235
1074
|
|
|
1236
1075
|
window.addEventListener('load', init);
|