@sailingrotevista/rotevista-dash 6.2.8 → 7.0.2

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 CHANGED
@@ -7,6 +7,7 @@
7
7
  * Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
8
8
  * Memoria UI persistente, Modalità Hercules, Focus Split Screen e
9
9
  * Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
10
+ *file app.js
10
11
  */
11
12
 
12
13
  // ==========================================================================
@@ -43,12 +44,11 @@ const DASH_VERSION = "6.0"; // Major Update: Server-Side History RAM Logging (Pr
43
44
  let simulationMode = false;
44
45
  let displayModeSog = 'SOG';
45
46
  let displayModeTws = 'TWS';
47
+ let activeInstrument = 'gauge'; // Modalità di default all'avvio: 'gauge' (analogico) o 'radar' (storico)
46
48
  let socket, renderInterval, simInterval;
47
- let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
48
- let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
49
- let curBoatCompassRot = 0, curWindCompassRot = 0;
49
+ let lastAvgUIUpdate = 0; // Chirurgico: Rimosse audioCtx e lastAlarmTime poiché sono già dichiarate in utils.js
50
50
 
51
- let smoothedLeeway = 0, rotationTrend = 0, meteoTrend = 0;
51
+ let rotationTrend = 0, meteoTrend = 0;
52
52
  let lastShortAvgVal = null, lastInstantTwa = null;
53
53
  let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
54
54
  let twDirty = false, isNavigating = false, reconnectDelay = 1000;
@@ -101,34 +101,8 @@ const ui = {
101
101
  };
102
102
 
103
103
  // ==========================================================================
104
- // 3. UTILITIES (MATEMATICA, BUFFER E MEMORIA)
104
+ // 3. UTILITIES (MATEMATICA, BUFFER E MEMORIA) - [Esportate in utils.js]
105
105
  // ==========================================================================
106
- function radToDeg(rad) { return rad * (180 / Math.PI); }
107
- function degToRad(deg) { return deg * (Math.PI / 180); }
108
- function msToKts(ms) { return ms * 1.94384; }
109
- function ktsToMs(kts) { return kts / 1.94384; }
110
-
111
- function getShortestRotation(curr, target) {
112
- let diff = (target - curr) % 360;
113
- if (diff > 180) diff -= 360;
114
- else if (diff < -180) diff += 360;
115
- return curr + diff;
116
- }
117
-
118
- /**
119
- * Scrive il testo nel DOM/SVG solo se il valore è realmente cambiato.
120
- * Utilizza innerHTML perché è l'unico metodo sicuro al 100% per forzare il ridisegno dei testi negli SVG.
121
- */
122
- function safeSetText(el, text) {
123
- if (!el) return;
124
- // Se l'elemento è parte di un SVG, usiamo textContent, altrimenti innerHTML
125
- const isSVG = el instanceof SVGElement;
126
- if (isSVG) {
127
- if (el.textContent !== text) el.textContent = text;
128
- } else {
129
- if (el.innerHTML !== text) el.innerHTML = text;
130
- }
131
- }
132
106
 
133
107
  /**
134
108
  * Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
@@ -155,68 +129,6 @@ function safePush(buffer, val, time) {
155
129
  if (buffer.length > 36000) buffer.shift();
156
130
  }
157
131
 
158
- /**
159
- * Media Circolare Vettoriale - Versione "Soft Outlier Rejection"
160
- */
161
- function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
162
- now = now || Date.now();
163
- const len = bufferArray.length;
164
- if (len === 0) return null;
165
-
166
- let sSin = 0, sCos = 0, count = 0;
167
- let newestTime = 0, oldestTime = 0;
168
-
169
- let pilotSin = 0, pilotCos = 0;
170
- const pilotSamples = Math.min(len, 15);
171
- for (let i = len - 1; i >= len - pilotSamples; i--) {
172
- pilotSin += bufferArray[i].sin;
173
- pilotCos += bufferArray[i].cos;
174
- }
175
- const pilotRad = Math.atan2(pilotSin, pilotCos);
176
- const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
177
-
178
- for (let i = len - 1; i >= 0; i--) {
179
- const item = bufferArray[i];
180
- if ((now - item.time) > windowMs) break;
181
-
182
- let diffRad = Math.atan2(Math.sin(item.val - pilotRad), Math.cos(item.val - pilotRad));
183
- let finalSin, finalCos;
184
-
185
- if (Math.abs(diffRad) > limitRad) {
186
- const clampedDiff = Math.sign(diffRad) * limitRad;
187
- const clampedRad = pilotRad + clampedDiff;
188
- finalSin = Math.sin(clampedRad);
189
- const relativeCos = Math.cos(clampedRad);
190
- finalCos = relativeCos;
191
- } else {
192
- finalSin = item.sin;
193
- finalCos = item.cos;
194
- }
195
-
196
- sSin += finalSin;
197
- sCos += finalCos;
198
-
199
- if (count === 0) newestTime = item.time;
200
- oldestTime = item.time;
201
- count++;
202
- }
203
-
204
- if (count === 0) return null;
205
-
206
- const R = Math.hypot(sSin, sCos) / count;
207
- const avgRad = Math.atan2(sSin, sCos);
208
- const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
209
- const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
210
- const safeR = Math.max(R, 1e-9);
211
-
212
- return {
213
- val: finalVal,
214
- stable: historyDuration > 10000 && R > CONFIG.averaging.stabilityThreshold,
215
- dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
216
- samples: count
217
- };
218
- }
219
-
220
132
  function saveDashboardState() {
221
133
  try {
222
134
  const focusedBox = document.querySelector('.data-box.is-focused');
@@ -277,34 +189,8 @@ function loadDashboardState() {
277
189
  }
278
190
 
279
191
  // ==========================================================================
280
- // 4. AUDIO E ALLARMI
192
+ // 4. AUDIO E ALLARMI - [Esportati in utils.js]
281
193
  // ==========================================================================
282
- function playBingBing() {
283
- if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
284
- const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n;
285
- function b(f, s) {
286
- const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
287
- o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f;
288
- g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4);
289
- o.start(s); o.stop(s + 0.5);
290
- }
291
- b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6);
292
- }
293
-
294
- function playGybeAlarm() {
295
- if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
296
- const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
297
- function note(f, s, d) {
298
- const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
299
- o.connect(g); g.connect(audioCtx.destination); o.type = 'square';
300
- o.frequency.value = f; g.gain.setValueAtTime(0.05, s);
301
- g.gain.exponentialRampToValueAtTime(0.001, s + d); o.start(s); o.stop(s + d);
302
- }
303
- for (let i = 0; i < 4; i++) {
304
- note(1800, audioCtx.currentTime + (i * 0.15), 0.1);
305
- note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1);
306
- }
307
- }
308
194
 
309
195
  function checkDepthAlarm(m) {
310
196
  ui.depth.classList.remove('alarm-warning', 'alarm-danger', 'blink-alarm');
@@ -316,12 +202,6 @@ function checkDepthAlarm(m) {
316
202
  }
317
203
  }
318
204
 
319
- function updateLeewayDisplay(deg) {
320
- const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125);
321
- ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w);
322
- ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`;
323
- }
324
-
325
205
  /**
326
206
  * computeTrueWind: Calcola TWS, TWA e TWD strategico.
327
207
  * Gestisce il fallback separato (Split-Fallback) in caso di dati parzialmente nativi di bordo.
@@ -670,22 +550,29 @@ function startDisplayLoop() {
670
550
  updateWindTrend();
671
551
 
672
552
  // --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
673
- const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
674
- const requiredMs = viewportMinutes * 60000;
675
- const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
676
-
677
- if (oldestStw) {
678
- const availableMs = now - oldestStw.time;
679
- if (availableMs >= requiredMs) {
680
- ui.status.innerText = `ONLINE ${viewportMinutes}min`;
681
- } else {
682
- const availableMin = Math.max(1, Math.floor(availableMs / 60000));
683
- ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
684
- }
685
- } else {
686
- ui.status.innerText = `ONLINE`;
687
- }
688
- ui.status.className = (socket && socket.readyState === WebSocket.OPEN) ? "online" : "offline";
553
+ const isSocketOpen = socket && socket.readyState === WebSocket.OPEN;
554
+
555
+ if (isSocketOpen) {
556
+ ui.status.className = "online"; // Colore Verde
557
+ const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
558
+ const requiredMs = viewportMinutes * 60000;
559
+ const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
560
+
561
+ if (oldestStw) {
562
+ const availableMs = now - oldestStw.time;
563
+ if (availableMs >= requiredMs) {
564
+ ui.status.innerText = `ONLINE ${viewportMinutes}min`;
565
+ } else {
566
+ const availableMin = Math.max(1, Math.floor(availableMs / 60000));
567
+ ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
568
+ }
569
+ } else {
570
+ ui.status.innerText = `ONLINE`;
571
+ }
572
+ } else {
573
+ ui.status.className = "offline"; // Colore Rosso
574
+ ui.status.innerText = "OFFLINE"; // Chirurgico: Forza il testo a OFFLINE se il socket è chiuso, evitando scritte verdi in rosso
575
+ }
689
576
 
690
577
  // --- WATCHDOG: CONTROLLO TIMEOUT ---
691
578
  const watch = {
@@ -738,131 +625,95 @@ function startDisplayLoop() {
738
625
  }
739
626
 
740
627
  // --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
741
- if (store.raw["environment.depth.belowTransducer"] !== undefined) {
742
- safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
743
- checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
744
- manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
745
- }
746
-
747
- // --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
748
-
749
- // Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
750
- const rawTws = store.raw["environment.wind.speedTrue"];
751
- const rawAws = store.raw["environment.wind.speedApparent"];
752
-
753
- const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
754
- const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
755
-
756
- if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
757
- if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
758
-
759
- // Disegno testo casella destra (TWS o AWS)
760
- if (rawTws !== undefined || rawAws !== undefined) {
761
- const labelWind = document.getElementById('tws-aws-label');
762
- const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
763
-
764
- safeSetText(ui.tws, currentWind.toFixed(1));
765
- if (labelWind) labelWind.textContent = displayModeTws;
766
-
767
- if (currentWind >= CONFIG.graphs.reef2) {
768
- ui.tws.style.setProperty('color', '#ff3b30', 'important');
769
- } else if (currentWind >= CONFIG.graphs.reef1) {
770
- ui.tws.style.setProperty('color', '#ff9800', 'important');
771
- } else {
772
- if (displayModeTws === 'AWS') {
773
- ui.tws.style.setProperty('color', '#5c6bc0', 'important');
774
- } else {
775
- const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
776
- ui.tws.style.setProperty('color', navyNight, 'important');
628
+ if (store.raw["environment.depth.belowTransducer"] !== undefined) {
629
+ safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
630
+ checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
631
+ manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
777
632
  }
778
- }
779
- }
780
633
 
781
- // --- SBLOCCO FORZATO TESTO BUSSOLA (AWS) ---
782
- if (rawAws !== undefined && rawAws !== null && !isNaN(awsVal)) {
783
- const strVal = awsVal.toFixed(1);
784
- // Recupero robusto dell'elemento
785
- let awsSvgEl = document.getElementById('aws-val-svg');
786
-
787
- if (awsSvgEl) {
788
- // Trucco anti-congelamento SVG: se il testo non combacia, lo forziamo usando
789
- // textContent (standard SVG) invece di innerHTML (standard HTML).
790
- if (awsSvgEl.textContent !== strVal) {
791
- awsSvgEl.textContent = strVal;
634
+ // --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
635
+
636
+ // Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
637
+ const rawTws = store.raw["environment.wind.speedTrue"];
638
+ const rawAws = store.raw["environment.wind.speedApparent"];
639
+
640
+ const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
641
+ const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
642
+
643
+ if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
644
+ if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
645
+
646
+ // Disegno testo casella destra (TWS o AWS)
647
+ if (rawTws !== undefined || rawAws !== undefined) {
648
+ const labelWind = document.getElementById('tws-aws-label');
649
+ const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
650
+
651
+ safeSetText(ui.tws, currentWind.toFixed(1));
652
+ if (labelWind) labelWind.textContent = displayModeTws;
653
+
654
+ if (currentWind >= CONFIG.graphs.reef2) {
655
+ ui.tws.style.setProperty('color', '#ff3b30', 'important');
656
+ } else if (currentWind >= CONFIG.graphs.reef1) {
657
+ ui.tws.style.setProperty('color', '#ff9800', 'important');
658
+ } else {
659
+ if (displayModeTws === 'AWS') {
660
+ ui.tws.style.setProperty('color', '#5c6bc0', 'important');
661
+ } else {
662
+ const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
663
+ ui.tws.style.setProperty('color', navyNight, 'important');
664
+ }
665
+ }
792
666
  }
793
- } else {
794
- console.error("ERRORE: Elemento SVG bussola ('aws-val-svg') non trovato nel documento!");
795
- }
796
- }
797
667
 
798
- // --- PUNTATORI ANALOGICI ---
799
- const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
800
- const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
801
- if (smAwa) ui.awa.setAttribute('transform', `rotate(${curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val))}, 200, 200)`);
802
- if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
803
-
804
- if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
805
- let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (2 * Math.PI) - Math.PI);
806
- smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
807
- curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
808
- ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
809
- ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#ff9800" : "";
810
- updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
811
- }
812
-
813
- // --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
814
- if (lastAvgUIUpdate++ % 10 === 0) {
815
- saveDashboardState();
816
- }
668
+ // --- AGGIORNAMENTO DELLA BUSSOLA CENTRALE (BATTERY SAVER A 1Hz) ---
669
+ if (activeInstrument === 'gauge') {
670
+ updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
671
+ }
672
+
673
+ // --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
674
+ if (lastAvgUIUpdate++ % 10 === 0) {
675
+ saveDashboardState();
676
+ }
817
677
 
818
- if (lastAvgUIUpdate % 3 === 0) {
819
- let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
820
- let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
821
- let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true);
822
- let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
823
- let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
824
-
825
- upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
826
- upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
827
- upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
828
- upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
829
- upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
830
-
831
- if (hObj && twdObj) {
832
- const reflectAngle = (targetRad, axisRad) => {
833
- const dS = Math.sin(axisRad - targetRad);
834
- const dC = Math.cos(axisRad - targetRad);
835
- return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
836
- };
837
- const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
838
- if (!isNavigating) ui.tackHdg.innerHTML = "---&deg;";
839
- else if (unstableH) { ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data'); }
840
- else {
841
- const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
842
- ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
843
- ui.tackHdg.classList.remove('unstable-data');
844
- }
845
- if (cObj) {
846
- const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
847
- if (!isNavigating) ui.tackCog.innerHTML = "---&deg;";
848
- else if (unstableC) { ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data'); }
849
- else {
850
- const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
851
- ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
852
- ui.tackCog.classList.remove('unstable-data');
678
+ if (lastAvgUIUpdate % 3 === 0) {
679
+ let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
680
+ let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
681
+ let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true);
682
+ let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
683
+ let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
684
+
685
+ upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
686
+ upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
687
+ upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
688
+ upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
689
+ upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
690
+
691
+ if (hObj && twdObj) {
692
+ const reflectAngle = (targetRad, axisRad) => {
693
+ const dS = Math.sin(axisRad - targetRad);
694
+ const dC = Math.cos(axisRad - targetRad);
695
+ return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
696
+ };
697
+ const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
698
+ if (!isNavigating) ui.tackHdg.innerHTML = "---&deg;";
699
+ else if (unstableH) { ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data'); }
700
+ else {
701
+ const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
702
+ ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
703
+ ui.tackHdg.classList.remove('unstable-data');
704
+ }
705
+ if (cObj) {
706
+ const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
707
+ if (!isNavigating) ui.tackCog.innerHTML = "---&deg;";
708
+ else if (unstableC) { ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data'); }
709
+ else {
710
+ const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
711
+ ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
712
+ ui.tackCog.classList.remove('unstable-data');
713
+ }
714
+ }
853
715
  }
854
716
  }
855
- }
856
-
857
- const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
858
- const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
859
- if (smHdgIcons && smTwdIcons) {
860
- curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
861
- ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
862
- curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val));
863
- ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
864
- }
865
- }
866
717
  }, RENDER_INTERVAL_MS);
867
718
  }
868
719
 
@@ -954,7 +805,7 @@ async function watchConfigChanges() {
954
805
  }
955
806
 
956
807
  /**
957
- * Recupera lo storico dei grafici pre-popolato dal server Signal K (Pro v6.0)
808
+ * Recupera lo storico dei grafici e dei radar pre-popolato dal server Signal K (Pro v6.0)
958
809
  */
959
810
  async function fetchServerHistory() {
960
811
  try {
@@ -968,6 +819,12 @@ async function fetchServerHistory() {
968
819
  store.histories[key] = data[key];
969
820
  }
970
821
  }
822
+ // Sincronizza i dati specifici del radar storici, previsionali e i buffer minuto per minuto
823
+ if (data.windRadarSlots) store.windRadarSlots = data.windRadarSlots;
824
+ if (data.futureForecast) store.futureForecast = data.futureForecast;
825
+ if (data.twd) store.twdMinuteBuffer = data.twd;
826
+ if (data.tws) store.twsMinuteBuffer = data.tws;
827
+
971
828
  // --- SILLABAZIONE STRATEGICA DELLA BUSSOLA METEO (TWD) ---
972
829
  // Se il server ci invia lo storico del TWD, lo inseriamo calcolando i seni e coseni per i vettori
973
830
  if (data.twd && data.twd.length > 0) {
@@ -978,7 +835,7 @@ async function fetchServerHistory() {
978
835
  cos: Math.cos(p.val)
979
836
  }));
980
837
  console.log(`📈 Memoria strategica TWD sincronizzata dal server (${data.twd.length} punti).`);
981
- }
838
+ }
982
839
  console.log("📈 Storico dei grafici pre-popolato caricato dal server.");
983
840
  }
984
841
  } catch (err) {
@@ -1085,321 +942,6 @@ function manageHistory(type, value) {
1085
942
  store.lastUpdates[type] = Math.floor(now / bucketIntervalMs) * bucketIntervalMs;
1086
943
  }
1087
944
 
1088
- /**
1089
- * Gestione dinamica delle scale dei grafici (Involucro Elastico Snapped e Safety Zoom)
1090
- * Implementa lo "Snap a Griglia" basato sull'hercSpan senza padding per tutti i sensori.
1091
- */
1092
- function calculateScale(type, data, mode) {
1093
- const s = CONFIG.scales[type];
1094
- const currentVal = data[data.length - 1];
1095
-
1096
- // Fallback di emergenza se il buffer è momentaneamente vuoto
1097
- if (currentVal === undefined || currentVal === null) {
1098
- return { min: 0, max: s ? s.stdMax : 10 };
1099
- }
1100
-
1101
- // Inizializzazione della memoria delle scale nello store se non esiste
1102
- if (!store.herculesScales) store.herculesScales = {};
1103
- if (!store.herculesScales[type]) {
1104
- store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
1105
- }
1106
- let currentScale = store.herculesScales[type];
1107
-
1108
- // ==========================================================================
1109
- // 1. SEZIONE PROFONDITÀ (DEDICATA A 2 MINUTI DI SICUREZZA, MINIMO FISSO A 0)
1110
- // ==========================================================================
1111
- if (type === 'depth') {
1112
- const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
1113
- if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
1114
-
1115
- const now = Date.now();
1116
- const depthSafetyWindowMs = 120000; // 2 minuti fissi per la sicurezza
1117
-
1118
- // Estrazione dati reali degli ultimi 2 minuti con timestamp
1119
- const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
1120
- const recentVals = recentPoints.map(p => p.val);
1121
-
1122
- const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
1123
- const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
1124
-
1125
- // --- 1.1 NORMALE PROFONDITÀ (CON SOGLIA STANDARD E FILTRO 2 MINUTI) ---
1126
- if (mode !== 'hercules') {
1127
- if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
1128
- store.depthProtectedActive = true;
1129
- }
1130
- if (store.depthProtectedActive) {
1131
- if (localMin > shallowThreshold) {
1132
- store.depthProtectedActive = false;
1133
- }
1134
- }
1135
- if (store.depthProtectedActive) {
1136
- return { min: 0, max: shallowThreshold };
1137
- }
1138
- const maxHistorico = Math.max(...data);
1139
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
1140
- }
1141
-
1142
- // --- 1.2 HERCULES PROFONDITÀ (BASE 0 IN ACQUE BASSE, FLUTTUANTE AL LARGO) ---
1143
- if (mode === 'hercules') {
1144
- const roundStep = s.hercSpan; // Passo di griglia selezionato dall'utente
1145
- const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
1146
-
1147
- let targetMin = 0;
1148
- // Se siamo in acque profonde, lasciamo che il minimo fluttui per mostrare i micro-dettagli
1149
- if (localMin > shallowThreshold) {
1150
- targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
1151
- }
1152
-
1153
- let targetMax = Math.ceil(localMax / roundStep) * roundStep;
1154
-
1155
- // Impediamo uno span inferiore a 4 metri per evitare grafici piatti
1156
- if (targetMax - targetMin < 4) {
1157
- targetMax = targetMin + 4;
1158
- }
1159
-
1160
- // Regola asimmetrica per il MIN e il MAX (Espansione istantanea, contrazione a 2 minuti)
1161
- if (currentVal < currentScale.min || currentVal > currentScale.max) {
1162
- currentScale.min = Math.min(currentScale.min, targetMin);
1163
- currentScale.max = Math.max(currentScale.max, targetMax);
1164
- } else {
1165
- const allStableInTarget = recentVals.every(val => val >= targetMin && val <= targetMax);
1166
- if (allStableInTarget) {
1167
- currentScale.min = targetMin;
1168
- currentScale.max = targetMax;
1169
- }
1170
- }
1171
-
1172
- return { min: currentScale.min, max: currentScale.max };
1173
- }
1174
- }
1175
-
1176
- // ==========================================================================
1177
- // 2. ALTRI GRAFICI (STW, SOG, TWS): COMPORTAMENTO ADATTIVO SU TERZO DEL GRAFICO
1178
- // ==========================================================================
1179
-
1180
- // --- 2.1 MODALITÀ NORMALE ---
1181
- if (mode !== 'hercules') {
1182
- const maxHistorico = Math.max(...data);
1183
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
1184
- }
1185
-
1186
- // --- 2.2 MODALITÀ HERCULES AD ALTO CONTRASTO (SNAP SENZA PADDING) ---
1187
- if (mode === 'hercules') {
1188
- const roundStep = s.hercSpan; // Passo di griglia (ex hercSpan)
1189
-
1190
- const oneThirdCount = Math.max(1, Math.floor(data.length / 3));
1191
- const recentThirdData = data.slice(-oneThirdCount);
1192
-
1193
- const localMin = Math.min(...recentThirdData);
1194
- const localMax = Math.max(...recentThirdData);
1195
-
1196
- // Applichiamo lo snap diretto ai multipli della griglia
1197
- let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
1198
- let targetMax = Math.ceil(localMax / roundStep) * roundStep;
1199
-
1200
- // Se lo span calcolato è nullo (es. velocità costante), forziamo lo span minimo
1201
- if (targetMax - targetMin === 0) {
1202
- targetMax = targetMin + roundStep;
1203
- }
1204
-
1205
- // APPLICAZIONE REGOLE ASIMMETRICHE ANTI-CLIPPING
1206
- // A. Espansione ISTANTANEA in alto o in basso (sicurezza)
1207
- if (currentVal < currentScale.min || currentVal > currentScale.max) {
1208
- currentScale.min = Math.min(currentScale.min, targetMin);
1209
- currentScale.max = Math.max(currentScale.max, targetMax);
1210
- }
1211
- // B. Contrazione RITARDATA (solo se tutto il terzo recente si è assestato nel target)
1212
- else {
1213
- const allStableInTarget = recentThirdData.every(val => val >= targetMin && val <= targetMax);
1214
- if (allStableInTarget) {
1215
- currentScale.min = targetMin;
1216
- currentScale.max = targetMax;
1217
- }
1218
- }
1219
-
1220
- return { min: currentScale.min, max: currentScale.max };
1221
- }
1222
- }
1223
-
1224
- function updateScaleLabels(t, min, max) {
1225
- const el = document.getElementById(t + '-scale');
1226
- if (el) el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`;
1227
- }
1228
-
1229
- /**
1230
- * refreshGraph: Recupero dati, switch AWS/TWS/VMG e passaggio a motore grafico.
1231
- * Redirige e protegge i canali secondari (AWS/VMG) evitando ridisegni inutili.
1232
- */
1233
- function refreshGraph(t) {
1234
- // Redirezione di sicurezza dei canali secondari (AWS / VMG) verso i contenitori principali
1235
- if (t === 'aws') {
1236
- if (displayModeTws !== 'AWS') return; // Se non stiamo visualizzando AWS a schermo, ignora il rinfresco
1237
- t = 'tws';
1238
- }
1239
- if (t === 'vmg') {
1240
- if (displayModeSog !== 'VMG') return; // Se non stiamo visualizzando VMG a schermo, ignora il rinfresco
1241
- t = 'sog';
1242
- }
1243
-
1244
- const boxType = t;
1245
- let rawData;
1246
-
1247
- if (t === 'tws' && displayModeTws === 'AWS') {
1248
- rawData = store.histories['aws'];
1249
- } else {
1250
- rawData = store.histories[t];
1251
- }
1252
-
1253
- if (!rawData || rawData.length < 2) return;
1254
-
1255
- const values = rawData.map(p => p.val);
1256
- const mode = graphModes[boxType];
1257
- const cfg = calculateScale(boxType, values, mode);
1258
-
1259
- const box = document.querySelector(`.box-${boxType}`);
1260
- if (box) box.classList.toggle('box-hercules', mode === 'hercules');
1261
-
1262
- updateScaleLabels(boxType, cfg.min, cfg.max);
1263
- drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
1264
- }
1265
-
1266
- /**
1267
- * drawGraph: Motore SVG con Timeline Reale e Gestione GAP
1268
- * Risolve i conflitti di orologio (Clock Drift) tra Cerbo GX e Tablet.
1269
- * Integra la reattività dello spessore della curva in modalità Dual Screen (Focus).
1270
- */
1271
- function drawGraph(d, id, min, max, isTws, isHercules) {
1272
- const svg = document.getElementById(id);
1273
- if (!svg || d.length < 2) return;
1274
-
1275
- const w = 200, h = 40;
1276
- const range = max - min || 1;
1277
- const isDepth = (id === 'depth-graph');
1278
-
1279
- // --- RISOLUZIONE DISALLINEAMENTO ORARIO ---
1280
- // Usiamo il tempo del sensore (l'ultimo dato ricevuto) invece dell'orologio del tablet
1281
- const latestPoint = d[d.length - 1];
1282
- const now = latestPoint ? latestPoint.time : Date.now();
1283
-
1284
- // --- RILEVAMENTO DINAMICO DUAL SCREEN BLINDATO ---
1285
- const box = svg.closest('.data-box');
1286
- const isFocused = isFocusActive && box && box.classList.contains('is-focused');
1287
-
1288
- const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
1289
- const viewportMs = visibleMinutes * 60000;
1290
- const viewportStart = now - viewportMs;
1291
-
1292
- const visibleData = d.filter(p => p.time >= viewportStart);
1293
- if (visibleData.length < 2) return;
1294
-
1295
- const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
1296
- const colDepth = "#0088cc", colStw = "#00C851", colStwBorder = "#007a3d", colSog = "#ffbb33", colVmg = "#00b8d4";
1297
-
1298
- const getColorProps = (val) => {
1299
- // Se siamo in Dual Screen (focus), aumentiamo lo spessore base della curva di un filino (da 1.6 a 2.2)
1300
- const baseStroke = isFocused ? "4.2" : "1.6";
1301
- const alertStroke = isFocused ? "4.8" : "2.2";
1302
- const warnStroke = isFocused ? "4.4" : "1.8";
1303
-
1304
- let color = colTws, opacity = "0.15", stroke = baseStroke;
1305
- if (isTws) {
1306
- const baseWind = (displayModeTws === 'AWS') ? colAws : colTws;
1307
- if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
1308
- else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
1309
- else color = baseWind;
1310
- } else if (isDepth) {
1311
- if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
1312
- else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
1313
- else color = colDepth;
1314
- } else {
1315
- if (id === 'stw-graph') color = colStw;
1316
- else if (id === 'sog-graph') color = (displayModeSog === 'VMG') ? colVmg : colSog;
1317
- }
1318
- return { color, opacity, stroke };
1319
- };
1320
-
1321
- let grids = "";
1322
- // --- GRIGLIE ORIZZONTALI (Spessore fisso a 0.5px, non deformabile) ---
1323
- [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" vector-effect="non-scaling-stroke" />`);
1324
-
1325
- // --- GRIGLIE VERTICALI (Spessore fisso a 0.4px, non deformabile) ---
1326
- const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
1327
- for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
1328
- const x = w - ((m / visibleMinutes) * w);
1329
- grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.4" vector-effect="non-scaling-stroke" />`;
1330
- }
1331
-
1332
- // --- DISEGNO LINEE DI SICUREZZA ALLARME PROFONDITÀ (DANGER & WARNING) ---
1333
- if (isDepth) {
1334
- const dangerVal = CONFIG.alarms.depthDanger; // Es. 2.5m
1335
- const warningVal = CONFIG.alarms.depthWarning; // Es. 3.5m
1336
- const marginX = 4; // Margine per non toccare i bordi esterni
1337
-
1338
- // Spessore dinamico per gli allarmi: 4.8px in Dual Screen, 1.8px nella griglia normale
1339
- const alarmStrokeWidth = isFocused ? "4.8" : "1.8";
1340
-
1341
- // Linea Rossa Viva (Spessore dinamico, con il tuo tratteggio alternato personalizzato)
1342
- if (dangerVal >= min && dangerVal <= max) {
1343
- const p = (dangerVal - min) / range;
1344
- const y = h - (p * h);
1345
- grids += `<line x1="${marginX}" y1="${y}" x2="${w - marginX}" y2="${y}" stroke="rgba(255, 59, 48, 0.95)" stroke-width="${alarmStrokeWidth}" stroke-dasharray="12, 6, 2, 6" vector-effect="non-scaling-stroke" />`;
1346
- }
1347
- // Linea Giallo Oro Viva (Spessore dinamico, con il tuo tratteggio alternato personalizzato)
1348
- if (warningVal >= min && warningVal <= max) {
1349
- const p = (warningVal - min) / range;
1350
- const y = h - (p * h);
1351
- grids += `<line x1="${marginX}" y1="${y}" x2="${w - marginX}" y2="${y}" stroke="rgba(255, 204, 0, 0.95)" stroke-width="${alarmStrokeWidth}" stroke-dasharray="6 , 2, 6, 12" vector-effect="non-scaling-stroke" />`;
1352
- }
1353
- }
1354
-
1355
- let gradientStops = "", lines = "", areaPath = "";
1356
- let started = false;
1357
-
1358
- for (let i = 1; i < visibleData.length; i++) {
1359
- const pA = visibleData[i - 1];
1360
- const pB = visibleData[i];
1361
-
1362
- const x1 = ((pA.time - viewportStart) / viewportMs) * w;
1363
- const x2 = ((pB.time - viewportStart) / viewportMs) * w;
1364
- const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
1365
- const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
1366
-
1367
- const props = getColorProps(pB.val);
1368
- const deltaTime = pB.time - pA.time;
1369
- const expectedInterval = viewportMs / CONFIG.graphs.samples;
1370
- const isGap = deltaTime > (expectedInterval * 2.5);
1371
-
1372
- const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
1373
- gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
1374
- gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
1375
-
1376
- if (isGap) {
1377
- if (started) {
1378
- areaPath += `L ${x1} ${h} `;
1379
- started = false;
1380
- }
1381
- } else {
1382
- // Curva del grafico: spessore reattivo al focus e protezione da deformazione (non-scaling)
1383
- lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" vector-effect="non-scaling-stroke" />`;
1384
- if (!started) {
1385
- areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
1386
- started = true;
1387
- }
1388
- areaPath += `L ${x2} ${y2} `;
1389
- }
1390
- }
1391
-
1392
- if (started) {
1393
- const last = visibleData[visibleData.length - 1];
1394
- const lastX = ((last.time - viewportStart) / viewportMs) * w;
1395
- areaPath += `L ${lastX} ${h} Z`;
1396
- }
1397
-
1398
- const gradId = `grad-${id}`;
1399
- const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
1400
- svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
1401
- }
1402
-
1403
945
  // ==========================================================================
1404
946
  // 9. INTERAZIONI E GESTI
1405
947
  // ==========================================================================
@@ -1565,43 +1107,104 @@ function connect() {
1565
1107
  // 11. INIT E CICLO DI VITA
1566
1108
  // ==========================================================================
1567
1109
  window.addEventListener('contextmenu', e => e.preventDefault(), true);
1568
- (function genTicks() {
1569
- const c = document.getElementById('ticks');
1570
- if (c) {
1571
- for (let i = 0; i < 360; i += 10) {
1572
- const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
1573
- const m = i % 30 === 0;
1574
- l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50));
1575
- l.setAttribute("stroke", m ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1");
1576
- l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l);
1577
- }
1578
- }
1579
- })();
1580
1110
 
1581
1111
  async function init() {
1582
1112
  loadDashboardState();
1583
1113
 
1114
+ // Disegna la grafica statica delle tacche di calibrazione di entrambi gli strumenti
1115
+ initCompassTicks(); // Tacche del Wind Gauge analogico (gauge.js)
1116
+ initRadarTicks(); // Tacche del Wind Radar storico (weather-radar.js)
1117
+
1118
+ // 1. COMANDO TATTICO: Gestore Box TWD (Pressione prolungata -> Radar | Tocco rapido in modalità Radar -> Torna a Gauge)
1119
+ const twdBox = document.querySelector('.box-twd');
1120
+ if (twdBox) {
1121
+ let twdPressTimer = null;
1122
+ let longPressTriggered = false; // Flag di controllo della pressione prolungata
1123
+
1124
+ twdBox.addEventListener('pointerdown', (e) => {
1125
+ longPressTriggered = false;
1126
+ if (activeInstrument === 'gauge') {
1127
+ twdPressTimer = setTimeout(() => {
1128
+ activeInstrument = 'radar';
1129
+ document.getElementById('wind-gauge').style.display = 'none';
1130
+ document.getElementById('wind-radar').style.display = 'block';
1131
+ renderRadar(); // Disegna immediatamente il radar all'attivazione
1132
+ twdPressTimer = null;
1133
+ longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
1134
+ }, 1000);
1135
+ }
1136
+ });
1137
+ twdBox.addEventListener('pointerup', () => {
1138
+ if (activeInstrument === 'gauge') {
1139
+ if (twdPressTimer) {
1140
+ clearTimeout(twdPressTimer);
1141
+ twdPressTimer = null;
1142
+ }
1143
+ } else if (activeInstrument === 'radar') {
1144
+ if (longPressTriggered) {
1145
+ // Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
1146
+ longPressTriggered = false;
1147
+ } else {
1148
+ // Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
1149
+ activeInstrument = 'gauge';
1150
+ document.getElementById('wind-radar').style.display = 'none';
1151
+ document.getElementById('wind-gauge').style.display = 'block';
1152
+ }
1153
+ }
1154
+ });
1155
+ twdBox.addEventListener('pointerleave', () => {
1156
+ if (twdPressTimer) {
1157
+ clearTimeout(twdPressTimer);
1158
+ twdPressTimer = null;
1159
+ }
1160
+ longPressTriggered = false;
1161
+ });
1162
+ }
1163
+
1164
+ // 2. COMANDO TATTICO: Click in qualsiasi punto del radar per tornare all'analogico
1165
+ const windRadarSvg = document.getElementById('wind-radar');
1166
+ if (windRadarSvg) {
1167
+ windRadarSvg.addEventListener('pointerup', () => {
1168
+ if (activeInstrument === 'radar') {
1169
+ activeInstrument = 'gauge';
1170
+ windRadarSvg.style.display = 'none';
1171
+ document.getElementById('wind-gauge').style.display = 'block';
1172
+ }
1173
+ });
1174
+ }
1175
+
1584
1176
  // Rileviamo se siamo sul Mac tramite file:// (Ambiente di sviluppo locale)
1585
1177
  const isLocalFile = (window.location.protocol === 'file:');
1586
1178
 
1587
- // 1. CARICAMENTO STORICO GRAFICI REALI DAL CERBO GX
1179
+ // 3. CARICAMENTO STORICO GRAFICI E RADAR REALI DAL CERBO GX
1588
1180
  try {
1589
1181
  await fetchServerHistory();
1590
1182
  } catch (err) {
1591
1183
  console.warn("⚠️ Impossibile caricare lo storico reale dal server.");
1592
1184
  }
1593
1185
 
1594
- // 2. CARICAMENTO CONFIGURAZIONI REALI (Bypassato su Mac per preservare i tuoi test!)
1186
+ // 4. CARICAMENTO CONFIGURAZIONI REALI (Bypassato su Mac per preservare i tuoi test!)
1595
1187
  if (!isLocalFile) {
1596
1188
  await fetchServerConfig();
1597
1189
  } else {
1598
- // Mantiene la CONFIG locale di app.js per farti fare le prove delle scale sul Mac
1599
1190
  console.log("🎮 Esecuzione locale file://: utilizzo delle calibrazioni di CONFIG locali di debug.");
1600
1191
  }
1601
1192
 
1602
1193
  startDisplayLoop();
1603
1194
  connect(); // Si collegherà in tempo reale al WebSocket reale della barca
1604
1195
 
1196
+ // 5. POLL LENTO (15 secondi): aggiorna i dati radar in background e, se attivo, li ridisegna
1197
+ setInterval(async () => {
1198
+ try {
1199
+ await fetchServerHistory();
1200
+ if (activeInstrument === 'radar') {
1201
+ renderRadar();
1202
+ }
1203
+ } catch (err) {
1204
+ console.warn("⚠️ Errore aggiornamento periodico storico:", err);
1205
+ }
1206
+ }, 15000);
1207
+
1605
1208
  // Controlla le modifiche di configurazione sul Cerbo solo se non siamo sul Mac via file://
1606
1209
  if (!isLocalFile) {
1607
1210
  setInterval(watchConfigChanges, 10000);