@sailingrotevista/rotevista-dash 4.0.21 → 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 +383 -137
- package/index.html +4 -1
- package/index.js +3 -3
- package/package.json +2 -2
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.
|
|
20
|
+
stabilityThreshold: 0.99,
|
|
21
21
|
minSpeed: 0,
|
|
22
22
|
stabilityBreakout: 15
|
|
23
23
|
},
|
|
24
|
-
graphs: { reef1:
|
|
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 = "
|
|
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';
|
|
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
|
|
@@ -236,6 +237,7 @@ function saveDashboardState() {
|
|
|
236
237
|
histories: store.histories,
|
|
237
238
|
longBuf: store.longBuf,
|
|
238
239
|
displayModeSog: displayModeSog,
|
|
240
|
+
displayModeTws: displayModeTws,
|
|
239
241
|
graphModes: graphModes,
|
|
240
242
|
isNightMode: document.body.classList.contains('night-mode'),
|
|
241
243
|
isFocusActive: isFocusActive,
|
|
@@ -267,6 +269,13 @@ function loadDashboardState() {
|
|
|
267
269
|
const labelEl = document.getElementById('sog-vmg-label');
|
|
268
270
|
if (labelEl) labelEl.textContent = displayModeSog;
|
|
269
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
|
+
}
|
|
270
279
|
|
|
271
280
|
// Ripristino Tema Notte
|
|
272
281
|
if (state.isNightMode) document.body.classList.add('night-mode');
|
|
@@ -515,17 +524,52 @@ function updateWindTrend() {
|
|
|
515
524
|
// ==========================================================================
|
|
516
525
|
// 7. RENDERING ENGINE E AGGIORNAMENTO UI
|
|
517
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
|
+
*/
|
|
518
533
|
function refreshGraph(t) {
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
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);
|
|
522
553
|
|
|
523
|
-
//
|
|
524
|
-
const box = document.querySelector(`.box-${
|
|
525
|
-
if (box)
|
|
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
|
+
}
|
|
526
559
|
|
|
527
|
-
|
|
528
|
-
|
|
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
|
+
);
|
|
529
573
|
}
|
|
530
574
|
|
|
531
575
|
/**
|
|
@@ -548,9 +592,6 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
548
592
|
}
|
|
549
593
|
};
|
|
550
594
|
|
|
551
|
-
/**
|
|
552
|
-
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
553
|
-
*/
|
|
554
595
|
/**
|
|
555
596
|
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
556
597
|
* Gestisce la gerarchia di aggiornamento Live (1s), Heavy (2s) e Slow (3s).
|
|
@@ -558,95 +599,164 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
558
599
|
function startDisplayLoop() {
|
|
559
600
|
renderInterval = setInterval(() => {
|
|
560
601
|
const now = Date.now();
|
|
602
|
+
const isNight = document.body.classList.contains('night-mode');
|
|
603
|
+
|
|
604
|
+
// Conversione velocità da m/s a Nodi
|
|
561
605
|
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
|
|
562
606
|
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
563
607
|
|
|
564
|
-
// Verifica stato navigazione basato su soglia impostata
|
|
608
|
+
// Verifica stato navigazione basato su soglia impostata (minSpeed)
|
|
565
609
|
isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
|
|
566
610
|
|
|
567
611
|
// --- TIER LIVE (1s): CONTROLLO TIMEOUT DATI ---
|
|
568
|
-
|
|
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
|
+
};
|
|
569
622
|
for (let p in watch) {
|
|
570
623
|
if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
|
|
571
624
|
watch[p].innerText = "---"; delete store.raw[p];
|
|
572
625
|
}
|
|
573
626
|
}
|
|
574
627
|
|
|
575
|
-
// --- AGGIORNAMENTO
|
|
628
|
+
// --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
|
|
576
629
|
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
577
|
-
ui.stw.innerText = stwKts.toFixed(1);
|
|
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);
|
|
578
634
|
}
|
|
579
635
|
|
|
580
|
-
// --- LOGICA SOG / VMG
|
|
636
|
+
// --- LOGICA SOG / VMG (COLORI DINAMICI E FILTRO NAVIGAZIONE) ---
|
|
581
637
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
638
|
+
// Calcolo VMG istantanea per il display numerico
|
|
582
639
|
const vmg = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
583
|
-
|
|
640
|
+
|
|
641
|
+
// Registriamo i dati nelle storie (per i grafici)
|
|
642
|
+
manageHistory('vmg', vmg);
|
|
643
|
+
manageHistory('sog', sogKts);
|
|
584
644
|
|
|
585
645
|
const labelEl = document.getElementById('sog-vmg-label');
|
|
646
|
+
|
|
586
647
|
if (displayModeSog === 'VMG') {
|
|
648
|
+
// MODALITÀ VMG: Mostra valore istantaneo, colore Cyan fisso per identificare la modalità
|
|
587
649
|
ui.sog.innerText = vmg.toFixed(1);
|
|
588
|
-
// AGGIORNATO AL NUOVO COLORE CYAN VIBRANTE (#00b8d4)
|
|
589
650
|
ui.sog.style.setProperty('color', '#00b8d4', 'important');
|
|
590
651
|
if (labelEl) labelEl.textContent = 'VMG';
|
|
591
652
|
} else {
|
|
653
|
+
// MODALITÀ SOG: Mostra valore istantaneo
|
|
592
654
|
ui.sog.innerText = sogKts.toFixed(1);
|
|
593
655
|
if (labelEl) labelEl.textContent = 'SOG';
|
|
594
656
|
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
+
}
|
|
600
678
|
}
|
|
601
679
|
}
|
|
602
680
|
|
|
681
|
+
// --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
|
|
603
682
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
604
683
|
ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
|
|
684
|
+
// Il colore neutro/allarme è gestito internamente dalla funzione checkDepthAlarm
|
|
605
685
|
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
606
686
|
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
607
687
|
}
|
|
608
688
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
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);
|
|
617
696
|
|
|
618
|
-
|
|
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
|
+
}
|
|
619
719
|
}
|
|
620
720
|
|
|
721
|
+
// --- AGGIORNAMENTO NUMERO AWS CENTRALE (Bussola) ---
|
|
621
722
|
if (store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
622
|
-
|
|
723
|
+
const awsVal = msToKts(store.raw["environment.wind.speedApparent"]);
|
|
724
|
+
ui.awsSvg.textContent = awsVal.toFixed(1);
|
|
623
725
|
}
|
|
624
726
|
|
|
625
727
|
// --- PUNTATORI ANALOGICI (Smoothing 2s) ---
|
|
626
728
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
|
|
627
729
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
|
|
628
|
-
if (smAwa) {
|
|
629
|
-
|
|
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
|
+
}
|
|
630
738
|
|
|
631
739
|
// --- CALCOLO LEEWAY E TRACK POINTER ---
|
|
632
740
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
633
741
|
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
634
|
-
//
|
|
742
|
+
// Filtraggio scarroccio: azzeramento se barca ferma, altrimenti smoothing
|
|
635
743
|
smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
636
|
-
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
637
|
-
ui.
|
|
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" : "";
|
|
638
747
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
639
748
|
}
|
|
640
749
|
|
|
750
|
+
// Aggiorna i trend meteo/tattici (pallini) e l'allarme strambata
|
|
641
751
|
updateWindTrend();
|
|
642
752
|
|
|
643
|
-
// TIER HEAVY (2s) - Grafici e
|
|
753
|
+
// TIER HEAVY (2s) - Aggiornamento Grafici e Salvataggio Stato
|
|
644
754
|
if (lastAvgUIUpdate++ % 2 === 0) {
|
|
645
755
|
['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
|
|
646
756
|
saveDashboardState();
|
|
647
757
|
}
|
|
648
758
|
|
|
649
|
-
// TIER SLOW (3s) - Medie Lunghe e
|
|
759
|
+
// TIER SLOW (3s) - Calcolo Medie Lunghe e Tack Strategico
|
|
650
760
|
if (lastAvgUIUpdate % 3 === 0) {
|
|
651
761
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
|
|
652
762
|
let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
|
|
@@ -654,6 +764,7 @@ function startDisplayLoop() {
|
|
|
654
764
|
let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
|
|
655
765
|
let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
|
|
656
766
|
|
|
767
|
+
// Aggiornamento interfaccia per i valori mediati (Heading, Cog, Awa, Twa, Twd)
|
|
657
768
|
upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
|
|
658
769
|
upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
|
|
659
770
|
upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
|
|
@@ -662,27 +773,25 @@ function startDisplayLoop() {
|
|
|
662
773
|
|
|
663
774
|
// --- LOGICA TACK STRATEGICA (VETTORIALE) ---
|
|
664
775
|
if (hObj && twdObj) {
|
|
665
|
-
// Funzione interna per riflettere un angolo rispetto all'asse del vento (TWD)
|
|
666
776
|
const reflectAngle = (targetRad, axisRad) => {
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
return Math.atan2(Math.sin(axisRad) *
|
|
670
|
-
Math.cos(axisRad) *
|
|
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);
|
|
671
781
|
};
|
|
672
|
-
|
|
782
|
+
|
|
673
783
|
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
674
|
-
|
|
784
|
+
|
|
675
785
|
if (!isNavigating) {
|
|
676
786
|
ui.tackHdg.innerHTML = "---°";
|
|
677
787
|
} else if (unstableH) {
|
|
678
788
|
ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data');
|
|
679
789
|
} else {
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
ui.tackHdg.innerHTML = `${Math.round(outH).toString().padStart(3, '0')}°`;
|
|
790
|
+
const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
|
|
791
|
+
ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}°`;
|
|
683
792
|
ui.tackHdg.classList.remove('unstable-data');
|
|
684
793
|
}
|
|
685
|
-
|
|
794
|
+
|
|
686
795
|
if (cObj) {
|
|
687
796
|
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
688
797
|
if (!isNavigating) {
|
|
@@ -690,20 +799,21 @@ function startDisplayLoop() {
|
|
|
690
799
|
} else if (unstableC) {
|
|
691
800
|
ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data');
|
|
692
801
|
} else {
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
ui.tackCog.innerHTML = `${Math.round(outC).toString().padStart(3, '0')}°`;
|
|
802
|
+
const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
|
|
803
|
+
ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}°`;
|
|
696
804
|
ui.tackCog.classList.remove('unstable-data');
|
|
697
805
|
}
|
|
698
806
|
}
|
|
699
807
|
}
|
|
700
808
|
|
|
701
|
-
// Rotazione Mini-Bussole
|
|
809
|
+
// Rotazione Mini-Icone nella bussola TWD (Mini-Bussole)
|
|
702
810
|
const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
|
|
703
811
|
const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
704
812
|
if (smHdgIcons && smTwdIcons) {
|
|
705
|
-
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
|
|
706
|
-
|
|
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)`);
|
|
707
817
|
}
|
|
708
818
|
}
|
|
709
819
|
}, RENDER_INTERVAL_MS);
|
|
@@ -778,133 +888,269 @@ function updateScaleLabels(t, min, max) {
|
|
|
778
888
|
}
|
|
779
889
|
|
|
780
890
|
/**
|
|
781
|
-
*
|
|
782
|
-
*
|
|
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à.
|
|
783
900
|
*/
|
|
784
901
|
/**
|
|
785
|
-
*
|
|
786
|
-
*
|
|
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à.
|
|
787
915
|
*/
|
|
788
916
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
789
917
|
const svg = document.getElementById(id);
|
|
918
|
+
|
|
919
|
+
// Uscita di sicurezza se l'elemento non esiste o non ci sono abbastanza dati
|
|
790
920
|
if (!svg || d.length < 2) return;
|
|
791
921
|
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
const
|
|
795
|
-
const
|
|
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
|
+
};
|
|
796
986
|
|
|
797
|
-
//
|
|
987
|
+
// --- 3. COSTRUZIONE DELLE GRIGLIE DI RIFERIMENTO ---
|
|
798
988
|
let grids = "";
|
|
989
|
+
// Linee orizzontali di livello (25%, 50%, 75%)
|
|
799
990
|
[0.25, 0.5, 0.75].forEach(p => {
|
|
800
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" />`;
|
|
801
992
|
});
|
|
993
|
+
// Linee verticali temporali (dinamiche in base alla storia impostata)
|
|
802
994
|
const gridInterval = (CONFIG.graphs.historyMinutes <= 15) ? 1 : 5;
|
|
803
995
|
for (let m = gridInterval; m < CONFIG.graphs.historyMinutes; m += gridInterval) {
|
|
804
996
|
const x = w - (m / CONFIG.graphs.historyMinutes) * w;
|
|
805
997
|
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
806
998
|
}
|
|
807
999
|
|
|
808
|
-
//
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const getColor = (val) => {
|
|
813
|
-
if (isTws) return (val >= CONFIG.graphs.reef2) ? "#ff3b30" : (val >= CONFIG.graphs.reef1 ? "#ff9800" : baseColorTws);
|
|
814
|
-
if (isDepth) return (val < CONFIG.alarms.depthDanger) ? "#ff3b30" : (val < CONFIG.alarms.depthWarning ? "#ff9800" : baseColorDepth);
|
|
815
|
-
switch(id) {
|
|
816
|
-
case 'stw-graph': return "#00C851";
|
|
817
|
-
case 'sog-graph': return (displayModeSog === 'VMG') ? "#00b8d4" : "#ffbb33";
|
|
818
|
-
default: return baseColorTws;
|
|
819
|
-
}
|
|
820
|
-
};
|
|
821
|
-
|
|
822
|
-
// 3. Creazione Gradiente e Linee
|
|
823
|
-
let gradientStops = "";
|
|
824
|
-
let lines = "";
|
|
825
|
-
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
|
|
826
1004
|
|
|
827
1005
|
for (let i = 1; i < d.length; i++) {
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
|
|
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;
|
|
831
1009
|
const x1 = ((i - 1) / (samples - 1)) * w;
|
|
832
1010
|
const y1 = h - (Math.max(0, Math.min(1, (d[i - 1] - min) / range)) * h);
|
|
833
1011
|
const x2 = (i / (samples - 1)) * w;
|
|
834
1012
|
const y2 = h - (Math.max(0, Math.min(1, (d[i] - min) / range)) * h);
|
|
835
1013
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
// Logica Opacità e Spessore (Tua configurazione)
|
|
839
|
-
let fillOpacity = "0.15";
|
|
840
|
-
let strokeWidth = "1";
|
|
841
|
-
|
|
842
|
-
if (color === "#ff3b30") {
|
|
843
|
-
fillOpacity = "0.85"; // Rosso: Molto visibile
|
|
844
|
-
strokeWidth = "1.5"; // Rosso: Più marcato
|
|
845
|
-
} else if (color === "#ff9800") {
|
|
846
|
-
fillOpacity = "0.45"; // Arancio: Velo medio
|
|
847
|
-
strokeWidth = "1"; // Arancio: Sottile
|
|
848
|
-
}
|
|
1014
|
+
// Otteniamo lo stile per questo specifico segmento
|
|
1015
|
+
const props = getColorProps(d[i]);
|
|
849
1016
|
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
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}" />`;
|
|
853
1022
|
|
|
854
|
-
//
|
|
1023
|
+
// DISEGNO DELLA LINEA SUPERIORE:
|
|
1024
|
+
// Usiamo linee individuali per poter gestire spessori e colori diversi (se necessario in futuro)
|
|
855
1025
|
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
|
|
856
|
-
style="stroke: ${color}; stroke-width: ${
|
|
1026
|
+
style="stroke: ${props.color}; stroke-width: ${props.stroke}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
|
|
857
1027
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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`;
|
|
865
1037
|
}
|
|
1038
|
+
}
|
|
866
1039
|
|
|
867
|
-
//
|
|
868
|
-
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
|
+
*/
|
|
869
1048
|
const defs = `
|
|
870
1049
|
<defs>
|
|
871
|
-
<linearGradient id="${gradId}" x1="0
|
|
1050
|
+
<linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">
|
|
872
1051
|
${gradientStops}
|
|
873
1052
|
</linearGradient>
|
|
874
1053
|
</defs>
|
|
875
1054
|
`;
|
|
876
1055
|
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
} else {
|
|
881
|
-
// Per STW/SOG usiamo il colore dell'ultimo punto con area fissa 0.15
|
|
882
|
-
const currentPathColor = getColor(d[d.length - 1]);
|
|
883
|
-
svg.innerHTML = `${grids}<path d="${areaPath}" fill="${currentPathColor}" fill-opacity="0.15" stroke="none" />${lines}`;
|
|
884
|
-
}
|
|
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}`;
|
|
885
1059
|
}
|
|
886
1060
|
|
|
887
1061
|
// ==========================================================================
|
|
888
1062
|
// 9. INTERAZIONI E GESTI
|
|
889
1063
|
// ==========================================================================
|
|
1064
|
+
/**
|
|
1065
|
+
* toggleFocusMode: Gestisce l'attivazione della modalità Split-Screen (Focus).
|
|
1066
|
+
* Ingrandisce un box specifico e mantiene visibile la bussola centrale.
|
|
1067
|
+
*/
|
|
890
1068
|
function toggleFocusMode(type, element) {
|
|
891
1069
|
const container = document.querySelector('.main-container');
|
|
1070
|
+
|
|
1071
|
+
// Identifica se il box appartiene alla colonna sinistra (per il layout CSS)
|
|
892
1072
|
const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
|
|
1073
|
+
|
|
1074
|
+
// Inverte lo stato del Focus
|
|
893
1075
|
isFocusActive = !isFocusActive;
|
|
894
|
-
|
|
895
|
-
|
|
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
|
+
}
|
|
896
1086
|
}
|
|
897
1087
|
|
|
1088
|
+
/**
|
|
1089
|
+
* Inizializzazione Gesti e Interazioni sui box con grafico.
|
|
1090
|
+
* Gestisce: Singolo Tap (Switch), Doppio Tap (Zoom), Long Press (Focus).
|
|
1091
|
+
*/
|
|
898
1092
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
899
1093
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
900
1094
|
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
901
|
-
|
|
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) ---
|
|
902
1110
|
el.addEventListener('pointerup', (e) => {
|
|
903
|
-
clearTimeout(pressTimer);
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
+
}
|
|
907
1151
|
});
|
|
1152
|
+
|
|
1153
|
+
// --- GESTIONE USCITA (Se l'utente trascina il dito fuori) ---
|
|
908
1154
|
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
909
1155
|
});
|
|
910
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"
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sailingrotevista/rotevista-dash",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
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"
|