@sailingrotevista/rotevista-dash 4.0.20 → 5.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
@@ -17,11 +17,11 @@ let CONFIG = {
17
17
  smoothWindow: 2000,
18
18
  longWindow: 30000,
19
19
  stabilityTolerance: 2000,
20
- stabilityThreshold: 0.9,
20
+ stabilityThreshold: 0.99,
21
21
  minSpeed: 0,
22
22
  stabilityBreakout: 15
23
23
  },
24
- graphs: { reef1: 8, reef2: 10.0, historyMinutes: 30, samples: 60 },
24
+ graphs: { reef1: 10, reef2: 15, historyMinutes: 10, samples: 60 },
25
25
  scales: {
26
26
  stw: { stdMax: 8, hercSpan: 4, step: 2 },
27
27
  sog: { stdMax: 8, hercSpan: 4, step: 2 },
@@ -34,7 +34,7 @@ let CONFIG = {
34
34
  const RENDER_INTERVAL_MS = 1000;
35
35
  const TIMEOUT_MS = 5000;
36
36
  const SIM_SAMPLE_INTERVAL = 1000;
37
- const DASH_VERSION = "2.4"; // Versione per la gestione della memoria locale
37
+ const DASH_VERSION = "3.0"; // Versione della memoria locale
38
38
  const sourceLocks = {};
39
39
 
40
40
 
@@ -42,7 +42,8 @@ const sourceLocks = {};
42
42
  // 2. STATO GLOBALE E RIFERIMENTI UI
43
43
  // ==========================================================================
44
44
  let simulationMode = false;
45
- let displayModeSog = 'SOG'; // Può essere 'SOG' o 'VMG'
45
+ let displayModeSog = 'SOG';
46
+ let displayModeTws = 'TWS';
46
47
  let socket, renderInterval, simInterval;
47
48
  let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
48
49
  let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
@@ -73,10 +74,10 @@ const store = {
73
74
  timestamps: {},
74
75
  smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
75
76
  longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
76
- histories: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
77
+ histories: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
77
78
  // Buffer temporaneo per il calcolo della media dell'intervallo del grafico
78
- graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
79
- lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0 }
79
+ graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
80
+ lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 }
80
81
  };
81
82
 
82
83
  // Riferimenti agli elementi DOM mappati all'avvio
@@ -120,6 +121,10 @@ function getShortestRotation(curr, target) {
120
121
  * Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
121
122
  */
122
123
  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
+ if (val === null || val === undefined || isNaN(val)) return;
127
+
123
128
  buffer.push({
124
129
  val: val,
125
130
  time: time,
@@ -139,63 +144,78 @@ function safePush(buffer, val, time) {
139
144
  if (buffer.length > 36000) buffer.shift();
140
145
  }
141
146
 
147
+ /**
148
+ * Media Circolare Vettoriale - Versione "Soft Outlier Rejection"
149
+ * Riduce l'impatto degli sbalzi limitando il loro angolo massimo di discostamento.
150
+ */
142
151
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
143
152
  now = now || Date.now();
144
-
145
153
  const len = bufferArray.length;
146
154
  if (len === 0) return null;
147
155
 
148
- let sSin = 0;
149
- let sCos = 0;
150
- let count = 0;
156
+ let sSin = 0, sCos = 0, count = 0;
157
+ let newestTime = 0, oldestTime = 0;
151
158
 
152
- let newestTime = 0;
153
- let oldestTime = 0;
159
+ // 1. MEDIA PILOTA: Guardiamo l'ultima frazione di dati (es. max 15 campioni) per sapere dove punta "ora"
160
+ let pilotSin = 0, pilotCos = 0;
161
+ const pilotSamples = Math.min(len, 15);
162
+ for (let i = len - 1; i >= len - pilotSamples; i--) {
163
+ pilotSin += bufferArray[i].sin;
164
+ pilotCos += bufferArray[i].cos;
165
+ }
166
+ const pilotRad = Math.atan2(pilotSin, pilotCos);
167
+
168
+ // Il limite elastico in radianti (basato sul tuo stabilityBreakout in gradi)
169
+ const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
154
170
 
171
+ // 2. CALCOLO AMMORTIZZATO
155
172
  for (let i = len - 1; i >= 0; i--) {
156
173
  const item = bufferArray[i];
157
-
158
174
  if ((now - item.time) > windowMs) break;
159
175
 
160
- sSin += item.sin;
161
- sCos += item.cos;
176
+ // Troviamo la differenza angolare (da -Pi a +Pi) tra il dato e la Media Pilota
177
+ let diffRad = Math.atan2(
178
+ Math.sin(item.val - pilotRad),
179
+ Math.cos(item.val - pilotRad)
180
+ );
181
+
182
+ let finalSin, finalCos;
183
+
184
+ // Se lo scarto è maggiore del limite, "Pattiniamo" (Ammortizzazione)
185
+ if (Math.abs(diffRad) > limitRad) {
186
+ // Tronchiamo la differenza al limite massimo consentito (mantenendo il segno)
187
+ const clampedDiff = Math.sign(diffRad) * limitRad;
188
+ // Ricalcoliamo l'angolo ammortizzato
189
+ const clampedRad = pilotRad + clampedDiff;
190
+
191
+ finalSin = Math.sin(clampedRad);
192
+ finalCos = Math.cos(clampedRad);
193
+ } else {
194
+ // Il dato è buono, usiamo i valori precalcolati
195
+ finalSin = item.sin;
196
+ finalCos = item.cos;
197
+ }
198
+
199
+ sSin += finalSin;
200
+ sCos += finalCos;
162
201
 
163
202
  if (count === 0) newestTime = item.time;
164
203
  oldestTime = item.time;
165
-
166
204
  count++;
167
205
  }
168
206
 
169
207
  if (count === 0) return null;
170
208
 
171
209
  const R = Math.hypot(sSin, sCos) / count;
172
-
173
210
  const avgRad = Math.atan2(sSin, sCos);
174
-
175
- const finalVal = signed
176
- ? avgRad
177
- : (avgRad + Math.PI * 2) % (Math.PI * 2);
178
-
179
- const historyDuration =
180
- (count > 2)
181
- ? (newestTime - oldestTime)
182
- : 0;
183
-
184
- const isStable =
185
- historyDuration > 10000 &&
186
- R > CONFIG.averaging.stabilityThreshold;
187
-
211
+ const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
212
+ const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
188
213
  const safeR = Math.max(R, 1e-9);
189
214
 
190
215
  return {
191
216
  val: finalVal,
192
- stable: isStable,
193
- dev: (R < 1)
194
- ? Math.round(
195
- Math.sqrt(-2 * Math.log(safeR)) *
196
- (180 / Math.PI)
197
- )
198
- : 0,
217
+ stable: historyDuration > 10000 && R > CONFIG.averaging.stabilityThreshold,
218
+ dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
199
219
  samples: count
200
220
  };
201
221
  }
@@ -217,6 +237,7 @@ function saveDashboardState() {
217
237
  histories: store.histories,
218
238
  longBuf: store.longBuf,
219
239
  displayModeSog: displayModeSog,
240
+ displayModeTws: displayModeTws,
220
241
  graphModes: graphModes,
221
242
  isNightMode: document.body.classList.contains('night-mode'),
222
243
  isFocusActive: isFocusActive,
@@ -248,6 +269,13 @@ function loadDashboardState() {
248
269
  const labelEl = document.getElementById('sog-vmg-label');
249
270
  if (labelEl) labelEl.textContent = displayModeSog;
250
271
  }
272
+
273
+ // Ripristino TWS/AWS ---
274
+ if (state.displayModeTws) {
275
+ displayModeTws = state.displayModeTws;
276
+ const labelEl = document.getElementById('tws-aws-label');
277
+ if (labelEl) labelEl.textContent = displayModeTws;
278
+ }
251
279
 
252
280
  // Ripristino Tema Notte
253
281
  if (state.isNightMode) document.body.classList.add('night-mode');
@@ -496,25 +524,61 @@ function updateWindTrend() {
496
524
  // ==========================================================================
497
525
  // 7. RENDERING ENGINE E AGGIORNAMENTO UI
498
526
  // ==========================================================================
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
+ */
499
533
  function refreshGraph(t) {
500
- const type = (t === 'vmg') ? 'sog' : t;
501
- const data = store.histories[t]; if (!data || data.length < 2) return;
502
- const mode = graphModes[type], cfg = calculateScale(type, data, mode);
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);
503
553
 
504
- // Gestione visualizzazione Hercules Zoom (sfondo rosso)
505
- const box = document.querySelector(`.box-${type}`);
506
- if (box) box.classList.toggle('box-hercules', mode === 'hercules');
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
+ }
507
559
 
508
- updateScaleLabels(type, cfg.min, cfg.max);
509
- drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
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
+ );
510
573
  }
511
574
 
512
575
  /**
513
576
  * upUI: Aggiornamento valori digitali con logica anti-ritardo (Istantaneo vs Media)
514
577
  */
515
578
  const upUI = (el, obj, instantRaw, isCompass = false) => {
516
- if (!obj || obj.val === null || instantRaw === undefined) {
517
- el.innerHTML = "---&deg;"; el.classList.remove('unstable-data');
579
+ if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
580
+ el.innerHTML = "---&deg;";
581
+ el.classList.remove('unstable-data');
518
582
  } else {
519
583
  let valDeg = Math.round(radToDeg(obj.val));
520
584
  let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "&deg;";
@@ -528,9 +592,6 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
528
592
  }
529
593
  };
530
594
 
531
- /**
532
- * Loop principale di aggiornamento interfaccia (1Hz)
533
- */
534
595
  /**
535
596
  * Loop principale di aggiornamento interfaccia (1Hz)
536
597
  * Gestisce la gerarchia di aggiornamento Live (1s), Heavy (2s) e Slow (3s).
@@ -538,95 +599,164 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
538
599
  function startDisplayLoop() {
539
600
  renderInterval = setInterval(() => {
540
601
  const now = Date.now();
602
+ const isNight = document.body.classList.contains('night-mode');
603
+
604
+ // Conversione velocità da m/s a Nodi
541
605
  const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
542
606
  const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
543
607
 
544
- // Verifica stato navigazione basato su soglia impostata
608
+ // Verifica stato navigazione basato su soglia impostata (minSpeed)
545
609
  isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
546
610
 
547
611
  // --- TIER LIVE (1s): CONTROLLO TIMEOUT DATI ---
548
- const watch = { "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog, "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog, "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth, "environment.wind.speedTrue": ui.tws };
612
+ // Se un dato non arriva da più di 5 secondi, mostra i trattini
613
+ const watch = {
614
+ "navigation.speedThroughWater": ui.stw,
615
+ "navigation.speedOverGround": ui.sog,
616
+ "navigation.headingTrue": ui.hdg,
617
+ "navigation.courseOverGroundTrue": ui.cog,
618
+ "environment.wind.speedApparent": ui.awsSvg,
619
+ "environment.depth.belowTransducer": ui.depth,
620
+ "environment.wind.speedTrue": ui.tws
621
+ };
549
622
  for (let p in watch) {
550
623
  if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
551
624
  watch[p].innerText = "---"; delete store.raw[p];
552
625
  }
553
626
  }
554
627
 
555
- // --- AGGIORNAMENTO DATI ISTANTANEI ---
628
+ // --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
556
629
  if (store.raw["navigation.speedThroughWater"] !== undefined) {
557
- ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts);
630
+ ui.stw.innerText = stwKts.toFixed(1);
631
+ // Colore neutro (ereditato dal tema) perché non ha switch di modalità
632
+ ui.stw.style.color = "";
633
+ manageHistory('stw', stwKts);
558
634
  }
559
635
 
560
- // --- LOGICA SOG / VMG E COLORI DINAMICI ---
636
+ // --- LOGICA SOG / VMG (COLORI DINAMICI E FILTRO NAVIGAZIONE) ---
561
637
  if (store.raw["navigation.speedOverGround"] !== undefined) {
638
+ // Calcolo VMG istantanea per il display numerico
562
639
  const vmg = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
563
- manageHistory('vmg', vmg); manageHistory('sog', sogKts);
640
+
641
+ // Registriamo i dati nelle storie (per i grafici)
642
+ manageHistory('vmg', vmg);
643
+ manageHistory('sog', sogKts);
564
644
 
565
645
  const labelEl = document.getElementById('sog-vmg-label');
646
+
566
647
  if (displayModeSog === 'VMG') {
648
+ // MODALITÀ VMG: Mostra valore istantaneo, colore Cyan fisso per identificare la modalità
567
649
  ui.sog.innerText = vmg.toFixed(1);
568
- // AGGIORNATO AL NUOVO COLORE CYAN VIBRANTE (#00b8d4)
569
650
  ui.sog.style.setProperty('color', '#00b8d4', 'important');
570
651
  if (labelEl) labelEl.textContent = 'VMG';
571
652
  } else {
653
+ // MODALITÀ SOG: Mostra valore istantaneo
572
654
  ui.sog.innerText = sogKts.toFixed(1);
573
655
  if (labelEl) labelEl.textContent = 'SOG';
574
656
 
575
- // Colore Corrente: Giallo Caldo per corrente a favore, Rosso per corrente contraria
576
- // (Allineiamo il colore della corrente a favore con il #ffbb33 usato nei grafici)
577
- if (sogKts - stwKts > 0.3) ui.sog.style.setProperty('color', '#ffbb33', 'important');
578
- else if (sogKts - stwKts < -0.3) ui.sog.style.setProperty('color', '#ff3b30', 'important'); // Rosso vivido
579
- else ui.sog.style.color = ""; // Torna normale
657
+ // --- LOGICA COLORE DINAMICO (Solo se in navigazione reale) ---
658
+ if (isNavigating) {
659
+ // Usiamo la media dell'ultimo punto del grafico per stabilizzare il colore (anti-onda)
660
+ const lastAvgSog = store.histories.sog.length > 0 ? store.histories.sog[store.histories.sog.length - 1] : sogKts;
661
+ const lastAvgStw = store.histories.stw.length > 0 ? store.histories.stw[store.histories.stw.length - 1] : stwKts;
662
+ const deltaCurrent = lastAvgSog - lastAvgStw;
663
+
664
+ if (deltaCurrent < -0.3) {
665
+ // CORRENTE CONTRO: Rosso Vivido
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
+ }
674
+ } else {
675
+ // NON IN NAVIGAZIONE: Riportiamo il colore al default neutro
676
+ ui.sog.style.color = "";
677
+ }
580
678
  }
581
679
  }
582
680
 
681
+ // --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
583
682
  if (store.raw["environment.depth.belowTransducer"] !== undefined) {
584
683
  ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
684
+ // Il colore neutro/allarme è gestito internamente dalla funzione checkDepthAlarm
585
685
  checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
586
686
  manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
587
687
  }
588
688
 
589
- if (store.raw["environment.wind.speedTrue"] !== undefined) {
590
- const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
591
- ui.tws.innerText = twsKts.toFixed(1);
592
-
593
- // Colore Reef: se normale usiamo "", il CSS metterà Nero (giorno) o Rosso (notte)
594
- if (twsKts >= CONFIG.graphs.reef2) ui.tws.style.setProperty('color', '#e74c3c', 'important');
595
- else if (twsKts >= CONFIG.graphs.reef1) ui.tws.style.setProperty('color', '#e67e22', 'important');
596
- else ui.tws.style.color = "";
689
+ // --- GESTIONE VENTO (TWS / AWS SWITCH CON COLORI COORDINATI) ---
690
+ const twsKts = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
691
+ const awsKts = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
692
+
693
+ // Registriamo sempre entrambe le storie per permettere lo switch fluido dei grafici
694
+ if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsKts);
695
+ if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsKts);
597
696
 
598
- manageHistory('tws', twsKts);
697
+ if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
698
+ const labelEl = document.getElementById('tws-aws-label');
699
+ const currentWindValue = (displayModeTws === 'AWS') ? awsKts : twsKts;
700
+
701
+ ui.tws.innerText = currentWindValue.toFixed(1);
702
+ if (labelEl) labelEl.textContent = displayModeTws;
703
+
704
+ // Logica Colore Testo Vento (Priorità ai Reef, poi colore di base modalità)
705
+ if (currentWindValue >= CONFIG.graphs.reef2) {
706
+ ui.tws.style.setProperty('color', '#ff3b30', 'important'); // Rosso Reef 2
707
+ } else if (currentWindValue >= CONFIG.graphs.reef1) {
708
+ ui.tws.style.setProperty('color', '#ff9800', 'important'); // Arancio Reef 1
709
+ } else {
710
+ // Colore di base differenziato per modalità (Indaco per AWS, Navy per TWS)
711
+ if (displayModeTws === 'AWS') {
712
+ ui.tws.style.setProperty('color', '#5c6bc0', 'important'); // Indigo
713
+ } else {
714
+ // Per il TWS, schiariamo il Navy in modalità notte per renderlo leggibile
715
+ const twsColor = isNight ? '#6c8ea0' : '#2c3e50';
716
+ ui.tws.style.setProperty('color', twsColor, 'important');
717
+ }
718
+ }
599
719
  }
600
720
 
721
+ // --- AGGIORNAMENTO NUMERO AWS CENTRALE (Bussola) ---
601
722
  if (store.raw["environment.wind.speedApparent"] !== undefined) {
602
- ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
723
+ const awsVal = msToKts(store.raw["environment.wind.speedApparent"]);
724
+ ui.awsSvg.textContent = awsVal.toFixed(1);
603
725
  }
604
726
 
605
727
  // --- PUNTATORI ANALOGICI (Smoothing 2s) ---
606
728
  const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
607
729
  const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
608
- if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
609
- if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
730
+ if (smAwa) {
731
+ curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val));
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
+ }
610
738
 
611
739
  // --- CALCOLO LEEWAY E TRACK POINTER ---
612
740
  if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
613
741
  let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
614
- // Azzeramento sotto soglia minima impostata
742
+ // Filtraggio scarroccio: azzeramento se barca ferma, altrimenti smoothing
615
743
  smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
616
- curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
617
- ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#e67e22" : "";
744
+ curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
745
+ ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
746
+ ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#ff9800" : "";
618
747
  updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
619
748
  }
620
749
 
750
+ // Aggiorna i trend meteo/tattici (pallini) e l'allarme strambata
621
751
  updateWindTrend();
622
752
 
623
- // TIER HEAVY (2s) - Grafici e Persistenza Browser
753
+ // TIER HEAVY (2s) - Aggiornamento Grafici e Salvataggio Stato
624
754
  if (lastAvgUIUpdate++ % 2 === 0) {
625
755
  ['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
626
756
  saveDashboardState();
627
757
  }
628
758
 
629
- // TIER SLOW (3s) - Medie Lunghe e Calcolo TACK
759
+ // TIER SLOW (3s) - Calcolo Medie Lunghe e Tack Strategico
630
760
  if (lastAvgUIUpdate % 3 === 0) {
631
761
  let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
632
762
  let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
@@ -634,6 +764,7 @@ function startDisplayLoop() {
634
764
  let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
635
765
  let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
636
766
 
767
+ // Aggiornamento interfaccia per i valori mediati (Heading, Cog, Awa, Twa, Twd)
637
768
  upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
638
769
  upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
639
770
  upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
@@ -642,27 +773,25 @@ function startDisplayLoop() {
642
773
 
643
774
  // --- LOGICA TACK STRATEGICA (VETTORIALE) ---
644
775
  if (hObj && twdObj) {
645
- // Funzione interna per riflettere un angolo rispetto all'asse del vento (TWD)
646
776
  const reflectAngle = (targetRad, axisRad) => {
647
- const diffSin = Math.sin(axisRad - targetRad);
648
- const diffCos = Math.cos(axisRad - targetRad);
649
- return Math.atan2(Math.sin(axisRad) * diffCos + Math.cos(axisRad) * diffSin,
650
- Math.cos(axisRad) * diffCos - Math.sin(axisRad) * diffSin);
777
+ const dS = Math.sin(axisRad - targetRad);
778
+ 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);
651
781
  };
652
-
782
+
653
783
  const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
654
-
784
+
655
785
  if (!isNavigating) {
656
786
  ui.tackHdg.innerHTML = "---&deg;";
657
787
  } else if (unstableH) {
658
788
  ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data');
659
789
  } else {
660
- const reflectedH = reflectAngle(hObj.val, twdObj.val);
661
- const outH = (radToDeg(reflectedH) + 360) % 360;
662
- ui.tackHdg.innerHTML = `${Math.round(outH).toString().padStart(3, '0')}&deg;`;
790
+ const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
791
+ ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
663
792
  ui.tackHdg.classList.remove('unstable-data');
664
793
  }
665
-
794
+
666
795
  if (cObj) {
667
796
  const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
668
797
  if (!isNavigating) {
@@ -670,20 +799,21 @@ function startDisplayLoop() {
670
799
  } else if (unstableC) {
671
800
  ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data');
672
801
  } else {
673
- const reflectedC = reflectAngle(cObj.val, twdObj.val);
674
- const outC = (radToDeg(reflectedC) + 360) % 360;
675
- ui.tackCog.innerHTML = `${Math.round(outC).toString().padStart(3, '0')}&deg;`;
802
+ const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
803
+ ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
676
804
  ui.tackCog.classList.remove('unstable-data');
677
805
  }
678
806
  }
679
807
  }
680
808
 
681
- // Rotazione Mini-Bussole
809
+ // Rotazione Mini-Icone nella bussola TWD (Mini-Bussole)
682
810
  const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
683
811
  const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
684
812
  if (smHdgIcons && smTwdIcons) {
685
- curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
686
- curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
813
+ curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
814
+ ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
815
+ curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val));
816
+ ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
687
817
  }
688
818
  }
689
819
  }, RENDER_INTERVAL_MS);
@@ -693,7 +823,7 @@ function startDisplayLoop() {
693
823
  // 8. CONFIGURAZIONE E GRAFICI UTILS
694
824
  // ==========================================================================
695
825
  /**
696
- * Recupera la configurazione e forza la sovrascrittura di ogni parametro.
826
+ * Recupera la configurazione dal server e applica migrazioni automatiche per le vecchie versioni
697
827
  */
698
828
  async function fetchServerConfig() {
699
829
  try {
@@ -704,19 +834,27 @@ async function fetchServerConfig() {
704
834
  // Stampa di debug per verificare cosa riceve il client
705
835
  console.log("🔍 Configurazione ricevuta dal Server:", data);
706
836
 
707
- // Merge intelligente dei dati ricevuti nel CONFIG esistente
837
+ // Merge intelligente dei dati ricevuti
708
838
  Object.assign(CONFIG.alarms, data.alarms || {});
709
839
  Object.assign(CONFIG.graphs, data.graphs || {});
710
840
  Object.assign(CONFIG.averaging, data.averaging || {});
711
841
 
712
- // Per le scale, siccome sono nidificate, facciamo un loop
842
+ // --- LOGICA DI MIGRAZIONE SILENZIOSA ---
843
+ // Se il valore ricevuto è il vecchio default (0.85) o inferiore, lo portiamo al nuovo standard 0.95.
844
+ // Questo è necessario perché con i nuovi filtri "Soft" lo 0.85 non farebbe quasi mai lampeggiare gli allarmi.
845
+ 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
+ }
849
+
850
+ // Per le scale, siccome sono nidificate, facciamo un loop di merge profondo
713
851
  if (data.scales) {
714
852
  for (let key in data.scales) {
715
853
  if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
716
854
  }
717
855
  }
718
856
 
719
- console.log("✅ Configurazione applicata. Alarmi attivi:", CONFIG.alarms);
857
+ console.log("✅ Configurazione applicata. Stabilità attiva:", CONFIG.averaging.stabilityThreshold);
720
858
  } catch (err) {
721
859
  console.warn("⚠️ Utilizzo default locali. Motivo:", err.message);
722
860
  }
@@ -750,133 +888,269 @@ function updateScaleLabels(t, min, max) {
750
888
  }
751
889
 
752
890
  /**
753
- * drawGraph: Disegna i grafici con griglia temporale intelligente
754
- * Usa un Gradiente Lineare SVG dinamico per eliminare le giunzioni dei poligoni.
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à.
755
900
  */
756
901
  /**
757
- * drawGraph: Disegna i grafici con griglia temporale intelligente
758
- * Versione bilanciata: 1.5px per allarmi critici, 1px per il resto.
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à.
759
915
  */
760
916
  function drawGraph(d, id, min, max, isTws, isHercules) {
761
917
  const svg = document.getElementById(id);
918
+
919
+ // Uscita di sicurezza se l'elemento non esiste o non ci sono abbastanza dati
762
920
  if (!svg || d.length < 2) return;
763
921
 
764
- const w = 200, h = 40;
765
- const range = max - min || 1;
766
- const isDepth = (id === 'depth-graph');
767
- const samples = CONFIG.graphs.samples;
922
+ // --- 1. CONFIGURAZIONE GEOMETRICA E SCALA ---
923
+ const w = 200; // Larghezza fissa del box grafico
924
+ const h = 40; // Altezza fissa del box grafico
925
+ const range = max - min || 1; // Range dei valori per il calcolo dell'altezza Y
926
+ const isDepth = (id === 'depth-graph'); // Flag specifico per il box profondità
927
+ const samples = CONFIG.graphs.samples; // Numero di campioni previsti (asse X)
928
+
929
+ // --- 2. DEFINIZIONE TAVOLOZZA COLORI "VIVID GLASS" ---
930
+ // Colori di Allerta
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
951
+
952
+ // A. LOGICA PER IL VENTO (TWS o AWS)
953
+ if (isTws) {
954
+ // Scegliamo il colore di base a seconda se stiamo guardando Reale o Apparente
955
+ const baseWind = (displayModeTws === 'AWS') ? colAws : colTws;
956
+
957
+ if (val >= CONFIG.graphs.reef2) {
958
+ color = colDanger; opacity = "0.55"; // Zona Pericolo
959
+ } else if (val >= CONFIG.graphs.reef1) {
960
+ color = colWarning; opacity = "0.45"; // Zona Attenzione
961
+ } else {
962
+ color = baseWind; // Zona Sicura
963
+ }
964
+ }
965
+ // B. LOGICA PER LA PROFONDITÀ (Inversa: allerta se il valore scende)
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
+ }
983
+ }
984
+ return { color, opacity, stroke };
985
+ };
768
986
 
769
- // 1. Griglia (Sottile 0.5px)
987
+ // --- 3. COSTRUZIONE DELLE GRIGLIE DI RIFERIMENTO ---
770
988
  let grids = "";
989
+ // Linee orizzontali di livello (25%, 50%, 75%)
771
990
  [0.25, 0.5, 0.75].forEach(p => {
772
991
  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" />`;
773
992
  });
993
+ // Linee verticali temporali (dinamiche in base alla storia impostata)
774
994
  const gridInterval = (CONFIG.graphs.historyMinutes <= 15) ? 1 : 5;
775
995
  for (let m = gridInterval; m < CONFIG.graphs.historyMinutes; m += gridInterval) {
776
996
  const x = w - (m / CONFIG.graphs.historyMinutes) * w;
777
997
  grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
778
998
  }
779
999
 
780
- // 2. Tavolozza Colori Vividi
781
- const baseColorTws = "#2c3e50";
782
- const baseColorDepth = "#0088cc";
783
-
784
- const getColor = (val) => {
785
- if (isTws) return (val >= CONFIG.graphs.reef2) ? "#ff3b30" : (val >= CONFIG.graphs.reef1 ? "#ff9800" : baseColorTws);
786
- if (isDepth) return (val < CONFIG.alarms.depthDanger) ? "#ff3b30" : (val < CONFIG.alarms.depthWarning ? "#ff9800" : baseColorDepth);
787
- switch(id) {
788
- case 'stw-graph': return "#00C851";
789
- case 'sog-graph': return (displayModeSog === 'VMG') ? "#00b8d4" : "#ffbb33";
790
- default: return baseColorTws;
791
- }
792
- };
793
-
794
- // 3. Creazione Gradiente e Linee
795
- let gradientStops = "";
796
- let lines = "";
797
- let areaPath = `M 0 ${h} `;
1000
+ // --- 4. CICLO DI ELABORAZIONE DEI DATI ---
1001
+ let gradientStops = ""; // Contiene i cambi di colore per l'area
1002
+ let lines = ""; // Contiene i segmenti della linea superiore
1003
+ let areaPath = `M 0 ${h} `; // Inizio del path del riempimento dal fondo sinistro
798
1004
 
799
1005
  for (let i = 1; i < d.length; i++) {
800
- const percentPrev = ((i - 1) / (samples - 1)) * 100;
801
- const percentCurr = (i / (samples - 1)) * 100;
802
-
1006
+ // Calcolo delle posizioni percentuali (per il gradiente) e pixel (per il disegno)
1007
+ const p1 = ((i - 1) / (samples - 1)) * 100;
1008
+ const p2 = (i / (samples - 1)) * 100;
803
1009
  const x1 = ((i - 1) / (samples - 1)) * w;
804
1010
  const y1 = h - (Math.max(0, Math.min(1, (d[i - 1] - min) / range)) * h);
805
1011
  const x2 = (i / (samples - 1)) * w;
806
1012
  const y2 = h - (Math.max(0, Math.min(1, (d[i] - min) / range)) * h);
807
1013
 
808
- const color = getColor(d[i]);
809
-
810
- // Logica Opacità e Spessore (Tua configurazione)
811
- let fillOpacity = "0.15";
812
- let strokeWidth = "1";
813
-
814
- if (color === "#ff3b30") {
815
- fillOpacity = "0.85"; // Rosso: Molto visibile
816
- strokeWidth = "1.5"; // Rosso: Più marcato
817
- } else if (color === "#ff9800") {
818
- fillOpacity = "0.45"; // Arancio: Velo medio
819
- strokeWidth = "1"; // Arancio: Sottile
820
- }
1014
+ // Otteniamo lo stile per questo specifico segmento
1015
+ const props = getColorProps(d[i]);
821
1016
 
822
- // Costruzione Gradiente (stacchi netti tra i colori)
823
- gradientStops += `<stop offset="${percentPrev}%" stop-color="${color}" stop-opacity="${fillOpacity}" />`;
824
- gradientStops += `<stop offset="${percentCurr}%" stop-color="${color}" stop-opacity="${fillOpacity}" />`;
1017
+ // AGGIUNTA STOP AL GRADIENTE:
1018
+ // Usiamo due stop identici alla stessa percentuale per creare stacchi di colore netti,
1019
+ // evitando sfumature tra una zona sicura e una di allarme.
1020
+ gradientStops += `<stop offset="${p1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
1021
+ gradientStops += `<stop offset="${p2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
825
1022
 
826
- // Disegno Linea con precisione geometrica
1023
+ // DISEGNO DELLA LINEA SUPERIORE:
1024
+ // Usiamo linee individuali per poter gestire spessori e colori diversi (se necessario in futuro)
827
1025
  lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
828
- style="stroke: ${color}; stroke-width: ${strokeWidth}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
1026
+ style="stroke: ${props.color}; stroke-width: ${props.stroke}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
829
1027
 
830
- if (i === 1) areaPath += `L ${x1} ${y1} `;
831
- areaPath += `L ${x2} ${y2} `;
832
-
833
- // Salviamo l'ultima coordinata X calcolata per chiudere correttamente il path
834
- if (i === d.length - 1) {
835
- areaPath += `L ${x2} ${h} Z`;
836
- }
1028
+ // AGGIORNAMENTO DEL PATH DELL'AREA:
1029
+ if (i === 1) areaPath += `L ${x1} ${y1} `; // Primo collegamento con la base
1030
+ areaPath += `L ${x2} ${y2} `; // Tracciato dei picchi
1031
+
1032
+ // --- FIX ARTEFATTO DIAGONALE FINALE ---
1033
+ // Se siamo arrivati all'ultimo dato disponibile nel buffer, chiudiamo il path
1034
+ // scendendo verticalmente verso l'asse zero (h), poi chiudiamo con 'Z'.
1035
+ if (i === d.length - 1) {
1036
+ areaPath += `L ${x2} ${h} Z`;
837
1037
  }
1038
+ }
838
1039
 
839
- // 4. Iniezione del Gradiente
840
- const gradId = `grad-${id}`;
1040
+ // --- 5. CREAZIONE DEL GRADIENTE LINEARE DINAMICO ---
1041
+ const gradId = `grad-${id}`; // ID unico basato sul box (es: grad-tws-graph)
1042
+
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
+ */
841
1048
  const defs = `
842
1049
  <defs>
843
- <linearGradient id="${gradId}" x1="0%" y1="0%" x2="100%" y2="0%">
1050
+ <linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">
844
1051
  ${gradientStops}
845
1052
  </linearGradient>
846
1053
  </defs>
847
1054
  `;
848
1055
 
849
- // 5. Render Finale
850
- if (isTws || isDepth) {
851
- svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
852
- } else {
853
- // Per STW/SOG usiamo il colore dell'ultimo punto con area fissa 0.15
854
- const currentPathColor = getColor(d[d.length - 1]);
855
- svg.innerHTML = `${grids}<path d="${areaPath}" fill="${currentPathColor}" fill-opacity="0.15" stroke="none" />${lines}`;
856
- }
1056
+ // --- 6. AGGIORNAMENTO FINALE DEL DOM ---
1057
+ // Inseriamo tutto nell'elemento SVG: Defs (Gradienti) -> Griglie -> Area (Fill) -> Linee (Stroke)
1058
+ svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
857
1059
  }
858
1060
 
859
1061
  // ==========================================================================
860
1062
  // 9. INTERAZIONI E GESTI
861
1063
  // ==========================================================================
1064
+ /**
1065
+ * toggleFocusMode: Gestisce l'attivazione della modalità Split-Screen (Focus).
1066
+ * Ingrandisce un box specifico e mantiene visibile la bussola centrale.
1067
+ */
862
1068
  function toggleFocusMode(type, element) {
863
1069
  const container = document.querySelector('.main-container');
1070
+
1071
+ // Identifica se il box appartiene alla colonna sinistra (per il layout CSS)
864
1072
  const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
1073
+
1074
+ // Inverte lo stato del Focus
865
1075
  isFocusActive = !isFocusActive;
866
- if (isFocusActive) { container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right'); element.classList.add('is-focused'); }
867
- else { container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right'); document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused')); }
1076
+
1077
+ if (isFocusActive) {
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
+ }
868
1086
  }
869
1087
 
1088
+ /**
1089
+ * Inizializzazione Gesti e Interazioni sui box con grafico.
1090
+ * Gestisce: Singolo Tap (Switch), Doppio Tap (Zoom), Long Press (Focus).
1091
+ */
870
1092
  ['stw', 'sog', 'tws', 'depth'].forEach(type => {
871
1093
  const el = document.getElementById(type + '-graph').closest('.data-box');
872
1094
  let lastTapTime = 0, tapTimeout, isLongPressActive = false;
873
- el.addEventListener('pointerdown', (e) => { isLongPressActive = false; pressTimer = setTimeout(() => { if (!isFocusActive) { isLongPressActive = true; toggleFocusMode(type, el); lastTapTime = 0; } }, 1000); });
1095
+
1096
+ // --- GESTIONE PRESSIONE (Inizio) ---
1097
+ el.addEventListener('pointerdown', (e) => {
1098
+ isLongPressActive = false;
1099
+ // Timer per attivare il Focus Mode dopo 1 secondo di pressione continua
1100
+ pressTimer = setTimeout(() => {
1101
+ if (!isFocusActive) {
1102
+ isLongPressActive = true;
1103
+ toggleFocusMode(type, el);
1104
+ lastTapTime = 0; // Evita che al rilascio scatti un click
1105
+ }
1106
+ }, 1000);
1107
+ });
1108
+
1109
+ // --- GESTIONE RILASCIO (Fine Gesto) ---
874
1110
  el.addEventListener('pointerup', (e) => {
875
- clearTimeout(pressTimer); if (isLongPressActive) return;
876
- const currentTime = new Date().getTime(), tapDelay = currentTime - lastTapTime;
877
- if (tapDelay < 300 && tapDelay > 0) { clearTimeout(tapTimeout); graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); refreshGraph(type); lastTapTime = 0; }
878
- else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(0, 0, 0, 0.05)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
1111
+ clearTimeout(pressTimer); // Cancella il timer del long press
1112
+ if (isLongPressActive) return; // Se è scattato il focus, non fare altro
1113
+
1114
+ const currentTime = new Date().getTime();
1115
+ const tapDelay = currentTime - lastTapTime;
1116
+
1117
+ // 1. GESTIONE DOPPIO CLICK (Zoom Hercules)
1118
+ if (tapDelay < 300 && tapDelay > 0) {
1119
+ clearTimeout(tapTimeout);
1120
+ // Switch tra modalità scala Standard e Zoom Hercules
1121
+ graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard';
1122
+ localStorage.setItem('mode_' + type, graphModes[type]);
1123
+ refreshGraph(type);
1124
+ lastTapTime = 0;
1125
+ }
1126
+ // 2. GESTIONE SINGOLO CLICK (Switch Dati o Esci dal Focus)
1127
+ else {
1128
+ lastTapTime = currentTime;
1129
+ tapTimeout = setTimeout(() => {
1130
+ // Se il box è in focus, il singolo click lo chiude
1131
+ if (isFocusActive && el.classList.contains('is-focused')) {
1132
+ toggleFocusMode(type, el);
1133
+ }
1134
+ // Se siamo in visualizzazione standard, gestiamo gli switch dati
1135
+ else if (!isFocusActive) {
1136
+ // Switch SOG <-> VMG
1137
+ if (type === 'sog') {
1138
+ displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
1139
+ }
1140
+ // Switch TWS <-> AWS (Nuova Logica)
1141
+ else if (type === 'tws') {
1142
+ displayModeTws = (displayModeTws === 'TWS') ? 'AWS' : 'TWS';
1143
+ }
1144
+
1145
+ // Feedback visivo (lampeggio leggero) dell'avvenuto switch
1146
+ el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
1147
+ setTimeout(() => el.style.backgroundColor = "", 150);
1148
+ }
1149
+ }, 250);
1150
+ }
879
1151
  });
1152
+
1153
+ // --- GESTIONE USCITA (Se l'utente trascina il dito fuori) ---
880
1154
  el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
881
1155
  });
882
1156
 
package/index.html CHANGED
@@ -168,7 +168,10 @@
168
168
  </div>
169
169
 
170
170
  <div class="data-box box-tws">
171
- <div class="label-row"><span class="label">TWS</span><span class="unit">kts</span></div>
171
+ <div class="label-row">
172
+ <span class="label" id="tws-aws-label">TWS</span>
173
+ <span class="unit">kts</span>
174
+ </div>
172
175
  <span class="value" id="tws">0.0</span>
173
176
  <div class="graph-wrapper">
174
177
  <div class="scale-labels" id="tws-scale"></div>
package/index.js CHANGED
@@ -84,19 +84,19 @@ module.exports = function (app) {
84
84
  reef1: {
85
85
  type: 'number',
86
86
  title: '1st Reef Alert (Orange)',
87
- description: "Wind speed (TWS) at which the graph turns orange, suggesting it's time to prepare for the first sail reduction.",
87
+ description: "Wind speed at which the graph turns orange. This threshold applies to the active mode: in TWS it indicates weather intensity, in AWS it indicates pressure on sails/rigging.",
88
88
  default: 15.0
89
89
  },
90
90
  reef2: {
91
91
  type: 'number',
92
92
  title: '2nd Reef Alert (Red)',
93
- description: "Wind speed (TWS) at which the graph turns red, indicating urgent need for sail reduction.",
93
+ description: "Critical wind speed at which the graph turns red. This threshold applies to the active mode: in TWS it warns of high sea state, in AWS it warns of excessive load on the mast/sails.",
94
94
  default: 20.0
95
95
  },
96
96
  historyMinutes: {
97
97
  type: 'number',
98
98
  title: 'Strategic Timeline (Minutes)',
99
- description: "Total duration shown in the charts. Vertical grid lines mark 1-minute intervals for short durations and 5-minute intervals for long ones.",
99
+ description: "Sets the time duration for all charts. It also defines the comparison window for the Strategic Weather Trend (the dot in the TWD compass) to detect long-term wind shifts.",
100
100
  default: 5,
101
101
  enum: [5, 10, 15, 30, 60]
102
102
  }
@@ -127,10 +127,10 @@ module.exports = function (app) {
127
127
  default: 0.5
128
128
  },
129
129
  stabilityThreshold: {
130
- type: 'number',
131
- title: 'Steering Precision (Sensitivity)',
132
- description: "Controls how strictly the system judges your course coherence. 0.95 requires pro precision; 0.85 is more realistic for cruising in waves.",
133
- default: 0.85
130
+ type: 'number',
131
+ title: 'Steering Precision (Sensitivity)',
132
+ description: "How strictly the system judges data coherence (0.0 to 1.0). Due to internal smoothing, 0.97-0.98 requires racing precision; 0.93-0.95 is ideal for cruising in waves. Below this, the display rarely alerts for instability.",
133
+ default: 0.95
134
134
  },
135
135
  stabilityBreakout: {
136
136
  type: 'number',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "4.0.20",
4
- "description": "Public Wind Dashboard with navigation and course aids",
3
+ "version": "5.0.2",
4
+ "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
7
7
  "access": "public"
package/settings.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "alarms": {
3
3
  "depthDanger": 2.5,
4
- "depthWarning": 5.0
4
+ "depthWarning": 3.5
5
5
  },
6
6
  "graphs": {
7
7
  "reef1": 15.0,
8
8
  "reef2": 20.0,
9
- "historyMinutes": 5
9
+ "historyMinutes": 30
10
10
  },
11
11
  "averaging": {
12
12
  "longWindow": 30000,
13
13
  "smoothWindow": 2000,
14
14
  "minSpeed": 0.5,
15
- "stabilityThreshold": 0.85,
15
+ "stabilityThreshold": 0.95,
16
16
  "stabilityBreakout": 15
17
17
  },
18
18
  "scales": {
package/style.css CHANGED
@@ -175,7 +175,7 @@ body.night-mode .alarm-danger {
175
175
 
176
176
  .focus-active .is-focused .sparkline path,
177
177
  .focus-active .is-focused .sparkline line {
178
- stroke-width: 1.5px !important;
178
+ stroke-width: 1.5px !important; /* Un briciolo più spessa per la stabilità visiva su schermi grandi */
179
179
  }
180
180
 
181
181
  .focus-active.focus-side-left {
@@ -441,10 +441,11 @@ body.night-mode {
441
441
 
442
442
  /* --- 10.3 GRAFICI (STILE DINAMICO NIGHT MODE) --- */
443
443
  .night-mode .graph-wrapper {
444
- background: rgba(10, 0, 0, 0.8) !important;
444
+ background: rgba(10, 0, 0, 0.8) !important; /* Sfondo scurissimo per alto contrasto */
445
445
  border: 1px solid #330000;
446
446
  }
447
447
 
448
+ /* Griglia temporale (minuti e livelli): quasi impercettibile */
448
449
  .night-mode .sparkline line[stroke="rgba(0,0,0,0.12)"],
449
450
  .night-mode .sparkline line[stroke="rgba(0,0,0,0.08)"] {
450
451
  stroke: rgba(255, 0, 0, 0.1) !important; /* Griglia temporale rossa soffusa */