@sailingrotevista/rotevista-dash 7.0.8 → 7.0.10
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 +232 -194
- package/charts.js +7 -4
- package/index.js +706 -658
- package/package.json +1 -1
package/app.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
* ==========================================================================
|
|
3
|
+
* Signal K Wind Dashboard - Pro Version 6.0 (Dynamic Envelope Architecture)
|
|
4
|
+
* ==========================================================================
|
|
5
|
+
* Autore: Sailing Rotevista
|
|
6
|
+
* Motore di calcolo tattico per navigazione e crociera.
|
|
7
|
+
* Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
|
|
8
|
+
* Memoria UI persistente, Modalità Hercules, Focus Split Screen e
|
|
9
|
+
* Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
|
|
10
|
+
* file app.js
|
|
11
|
+
*/
|
|
12
12
|
|
|
13
13
|
// ==========================================================================
|
|
14
14
|
// 1. CONFIGURAZIONE E DEFAULT
|
|
@@ -106,9 +106,9 @@ const ui = {
|
|
|
106
106
|
// ==========================================================================
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
* Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
|
|
110
|
+
* Mantiene sempre almeno 60 minuti di memoria locale per il calcolo della bussola meteo.
|
|
111
|
+
*/
|
|
112
112
|
function safePush(buffer, val, time) {
|
|
113
113
|
if (val === null || val === undefined || isNaN(val)) return;
|
|
114
114
|
|
|
@@ -197,16 +197,19 @@ function checkDepthAlarm(m) {
|
|
|
197
197
|
ui.depth.classList.remove('alarm-warning', 'alarm-danger', 'blink-alarm');
|
|
198
198
|
if (m < CONFIG.alarms.depthDanger) {
|
|
199
199
|
ui.depth.classList.add('alarm-danger', 'blink-alarm');
|
|
200
|
-
|
|
200
|
+
// Suona solo se siamo attivamente in navigazione (Harbor Silence acustico quando fermi)
|
|
201
|
+
if (isNavigating) {
|
|
202
|
+
playBingBing();
|
|
203
|
+
}
|
|
201
204
|
} else if (m < CONFIG.alarms.depthWarning) {
|
|
202
205
|
ui.depth.classList.add('alarm-warning');
|
|
203
206
|
}
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
* computeTrueWind: Calcola TWS, TWA e TWD strategico.
|
|
211
|
+
* Gestisce il fallback separato (Split-Fallback) in caso di dati parzialmente nativi di bordo.
|
|
212
|
+
*/
|
|
210
213
|
function computeTrueWind() {
|
|
211
214
|
const aws = store.raw["environment.wind.speedApparent"], awa = store.raw["environment.wind.angleApparent"];
|
|
212
215
|
const stw = store.raw["navigation.speedThroughWater"] || 0, sog = store.raw["navigation.speedOverGround"] || 0;
|
|
@@ -267,9 +270,9 @@ function computeTrueWind() {
|
|
|
267
270
|
// ==========================================================================
|
|
268
271
|
|
|
269
272
|
/**
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
* Assegna un punteggio di qualità statico alla sorgente basato sull'hardware.
|
|
274
|
+
* Più alto è il punteggio, maggiore è la priorità del sensore.
|
|
275
|
+
*/
|
|
273
276
|
function getSourcePriorityScore(sourceName) {
|
|
274
277
|
if (!sourceName) return 0;
|
|
275
278
|
const name = sourceName.toLowerCase();
|
|
@@ -293,6 +296,20 @@ function getSourcePriorityScore(sourceName) {
|
|
|
293
296
|
}
|
|
294
297
|
|
|
295
298
|
function processIncomingData(path, val, source, timeMs) {
|
|
299
|
+
// 1. Filtro anti-spike vento (> 100 nodi / 51.44 m/s)
|
|
300
|
+
if ((path === "environment.wind.speedApparent" || path === "environment.wind.speedTrue") && val > 51.44) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 2. Filtro anti-spike velocità barca STW/SOG (> 50 nodi / 25.72 m/s)
|
|
305
|
+
if ((path === "navigation.speedThroughWater" || path === "navigation.speedOverGround") && val > 25.72) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 3. Filtro validità profondità (ignora errori negativi e letture > 500m per lost-echo)
|
|
310
|
+
if (path === "environment.depth.belowTransducer" && (val < -2.0 || val > 500)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
296
313
|
// Usiamo il tempo reale del pacchetto del server per eliminare lo sfasamento
|
|
297
314
|
const now = timeMs || Date.now();
|
|
298
315
|
const score = getSourcePriorityScore(source);
|
|
@@ -380,6 +397,16 @@ function processIncomingData(path, val, source, timeMs) {
|
|
|
380
397
|
if (path === "navigation.courseOverGroundTrue") {
|
|
381
398
|
safePush(store.smoothBuf.cog, val, now);
|
|
382
399
|
safePush(store.longBuf.cog, val, now);
|
|
400
|
+
|
|
401
|
+
// Se non è installata alcuna bussola fisica sulla rete e la barca è in movimento stabile (> 1.5 nodi),
|
|
402
|
+
// emuliamo la Prua usando il COG per attivare il TWD, il mini-compass e la bussola radar.
|
|
403
|
+
const hasCompass = store.raw["navigation.headingTrue"] !== undefined || store.raw["navigation.headingMagnetic"] !== undefined;
|
|
404
|
+
const sog = store.raw["navigation.speedOverGround"] || 0;
|
|
405
|
+
if (!hasCompass && sog > 0.77) { // 0.77 m/s = 1.5 nodi
|
|
406
|
+
store.raw["navigation.headingTrue"] = val;
|
|
407
|
+
safePush(store.smoothBuf.hdg, val, now);
|
|
408
|
+
safePush(store.longBuf.hdg, val, now);
|
|
409
|
+
}
|
|
383
410
|
}
|
|
384
411
|
|
|
385
412
|
const twPaths = [
|
|
@@ -532,12 +559,12 @@ function updateWindTrend() {
|
|
|
532
559
|
// ==========================================================================
|
|
533
560
|
|
|
534
561
|
/**
|
|
535
|
-
|
|
536
|
-
|
|
562
|
+
* upUI: Aggiornamento valori digitali
|
|
563
|
+
*/
|
|
537
564
|
const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
538
565
|
if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
|
|
539
|
-
|
|
540
|
-
|
|
566
|
+
el.innerHTML = "---°";
|
|
567
|
+
el.classList.remove('unstable-data');
|
|
541
568
|
} else {
|
|
542
569
|
let valDeg = Math.round(radToDeg(obj.val));
|
|
543
570
|
let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
@@ -551,8 +578,8 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
551
578
|
};
|
|
552
579
|
|
|
553
580
|
/**
|
|
554
|
-
|
|
555
|
-
|
|
581
|
+
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
582
|
+
*/
|
|
556
583
|
function startDisplayLoop() {
|
|
557
584
|
renderInterval = setInterval(() => {
|
|
558
585
|
const now = Date.now();
|
|
@@ -567,43 +594,54 @@ function startDisplayLoop() {
|
|
|
567
594
|
updateWindTrend();
|
|
568
595
|
|
|
569
596
|
// --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
} else {
|
|
583
|
-
const availableMin = Math.max(1, Math.floor(availableMs / 60000));
|
|
584
|
-
ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
|
|
585
|
-
}
|
|
586
|
-
} else {
|
|
587
|
-
ui.status.innerText = `ONLINE`;
|
|
588
|
-
}
|
|
597
|
+
const isSocketOpen = socket && socket.readyState === WebSocket.OPEN;
|
|
598
|
+
|
|
599
|
+
if (isSocketOpen) {
|
|
600
|
+
ui.status.className = "online"; // Colore Verde
|
|
601
|
+
const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
602
|
+
const requiredMs = viewportMinutes * 60000;
|
|
603
|
+
const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
|
|
604
|
+
|
|
605
|
+
if (oldestStw) {
|
|
606
|
+
const availableMs = now - oldestStw.time;
|
|
607
|
+
if (availableMs >= requiredMs) {
|
|
608
|
+
ui.status.innerText = `ONLINE ${viewportMinutes}min`;
|
|
589
609
|
} else {
|
|
590
|
-
|
|
591
|
-
ui.status.innerText =
|
|
610
|
+
const availableMin = Math.max(1, Math.floor(availableMs / 60000));
|
|
611
|
+
ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
|
|
592
612
|
}
|
|
613
|
+
} else {
|
|
614
|
+
ui.status.innerText = `ONLINE`;
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
ui.status.className = "offline"; // Colore Rosso
|
|
618
|
+
ui.status.innerText = "OFFLINE"; // Chirurgico: Forza il testo a OFFLINE se il socket è chiuso, evitando scritte verdi in rosso
|
|
619
|
+
}
|
|
593
620
|
|
|
594
621
|
// --- WATCHDOG: CONTROLLO TIMEOUT ---
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
622
|
+
const watch = {
|
|
623
|
+
"navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
|
|
624
|
+
"navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
|
|
625
|
+
"environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
|
|
626
|
+
"environment.wind.speedTrue": ui.tws
|
|
627
|
+
};
|
|
628
|
+
for (let p in watch) {
|
|
629
|
+
if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
|
|
630
|
+
safeSetText(watch[p], "---"); // Sostituito innerText con la funzione protetta!
|
|
631
|
+
delete store.raw[p];
|
|
632
|
+
|
|
633
|
+
// Forza lo scorrimento dei dati fuori dallo schermo per i sensori in timeout
|
|
634
|
+
if (p === "navigation.speedThroughWater") {
|
|
635
|
+
refreshGraph('stw');
|
|
636
|
+
} else if (p === "navigation.speedOverGround") {
|
|
637
|
+
refreshGraph('sog');
|
|
638
|
+
} else if (p === "environment.depth.belowTransducer") {
|
|
639
|
+
refreshGraph('depth');
|
|
640
|
+
} else if (p === "environment.wind.speedApparent" || p === "environment.wind.speedTrue") {
|
|
641
|
+
refreshGraph('tws');
|
|
605
642
|
}
|
|
606
643
|
}
|
|
644
|
+
}
|
|
607
645
|
|
|
608
646
|
// --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
|
|
609
647
|
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
@@ -642,95 +680,95 @@ function startDisplayLoop() {
|
|
|
642
680
|
}
|
|
643
681
|
|
|
644
682
|
// --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
683
|
+
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
684
|
+
safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
|
|
685
|
+
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
686
|
+
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
687
|
+
}
|
|
650
688
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
// Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
|
|
654
|
-
const rawTws = store.raw["environment.wind.speedTrue"];
|
|
655
|
-
const rawAws = store.raw["environment.wind.speedApparent"];
|
|
656
|
-
|
|
657
|
-
const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
|
|
658
|
-
const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
|
|
689
|
+
// --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
|
|
659
690
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
691
|
+
// Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
|
|
692
|
+
const rawTws = store.raw["environment.wind.speedTrue"];
|
|
693
|
+
const rawAws = store.raw["environment.wind.speedApparent"];
|
|
694
|
+
|
|
695
|
+
const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
|
|
696
|
+
const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
|
|
697
|
+
|
|
698
|
+
if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
|
|
699
|
+
if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
|
|
700
|
+
|
|
701
|
+
// Disegno testo casella destra (TWS o AWS)
|
|
702
|
+
if (rawTws !== undefined || rawAws !== undefined) {
|
|
703
|
+
const labelWind = document.getElementById('tws-aws-label');
|
|
704
|
+
const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
|
|
705
|
+
|
|
706
|
+
safeSetText(ui.tws, currentWind.toFixed(1));
|
|
707
|
+
if (labelWind) labelWind.textContent = displayModeTws;
|
|
708
|
+
|
|
709
|
+
if (currentWind >= CONFIG.graphs.reef2) {
|
|
710
|
+
ui.tws.style.setProperty('color', '#ff3b30', 'important');
|
|
711
|
+
} else if (currentWind >= CONFIG.graphs.reef1) {
|
|
712
|
+
ui.tws.style.setProperty('color', '#ff9800', 'important');
|
|
713
|
+
} else {
|
|
714
|
+
if (displayModeTws === 'AWS') {
|
|
715
|
+
ui.tws.style.setProperty('color', '#5c6bc0', 'important');
|
|
716
|
+
} else {
|
|
717
|
+
const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
|
|
718
|
+
ui.tws.style.setProperty('color', navyNight, 'important');
|
|
683
719
|
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
684
722
|
|
|
685
723
|
// --- AGGIORNAMENTO DELLA BUSSOLA CENTRALE (BATTERY SAVER A 1Hz) ---
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
724
|
+
if (activeInstrument === 'gauge') {
|
|
725
|
+
updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
|
|
729
|
+
if (lastAvgUIUpdate++ % 10 === 0) {
|
|
730
|
+
saveDashboardState();
|
|
731
|
+
}
|
|
694
732
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
}
|
|
733
|
+
if (lastAvgUIUpdate % 3 === 0) {
|
|
734
|
+
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
|
|
735
|
+
let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
|
|
736
|
+
let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true);
|
|
737
|
+
let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
|
|
738
|
+
let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
|
|
739
|
+
|
|
740
|
+
upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
|
|
741
|
+
upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
|
|
742
|
+
upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
|
|
743
|
+
upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
|
|
744
|
+
upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
|
|
745
|
+
|
|
746
|
+
if (hObj && twdObj) {
|
|
747
|
+
const reflectAngle = (targetRad, axisRad) => {
|
|
748
|
+
const dS = Math.sin(axisRad - targetRad);
|
|
749
|
+
const dC = Math.cos(axisRad - targetRad);
|
|
750
|
+
return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
|
|
751
|
+
};
|
|
752
|
+
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
753
|
+
if (!isNavigating) ui.tackHdg.innerHTML = "---°";
|
|
754
|
+
else if (unstableH) { ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data'); }
|
|
755
|
+
else {
|
|
756
|
+
const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
|
|
757
|
+
ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}°`;
|
|
758
|
+
ui.tackHdg.classList.remove('unstable-data');
|
|
759
|
+
}
|
|
760
|
+
if (cObj) {
|
|
761
|
+
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
762
|
+
if (!isNavigating) ui.tackCog.innerHTML = "---°";
|
|
763
|
+
else if (unstableC) { ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data'); }
|
|
764
|
+
else {
|
|
765
|
+
const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
|
|
766
|
+
ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}°`;
|
|
767
|
+
ui.tackCog.classList.remove('unstable-data');
|
|
732
768
|
}
|
|
733
769
|
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
734
772
|
}, RENDER_INTERVAL_MS);
|
|
735
773
|
}
|
|
736
774
|
|
|
@@ -741,8 +779,8 @@ function startDisplayLoop() {
|
|
|
741
779
|
let currentConfigString = ""; // Memoria per rilevare cambiamenti nei settings
|
|
742
780
|
|
|
743
781
|
/**
|
|
744
|
-
|
|
745
|
-
|
|
782
|
+
* Risolve dinamicamente l'URL dell'API del Cerbo GX se siamo in locale su Mac/PC
|
|
783
|
+
*/
|
|
746
784
|
function getApiUrl(path) {
|
|
747
785
|
if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
748
786
|
return `http://${CONFIG.server.fallbackIp}${path}`;
|
|
@@ -751,8 +789,8 @@ function getApiUrl(path) {
|
|
|
751
789
|
}
|
|
752
790
|
|
|
753
791
|
/**
|
|
754
|
-
|
|
755
|
-
|
|
792
|
+
* Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
|
|
793
|
+
*/
|
|
756
794
|
function applyConfigData(data) {
|
|
757
795
|
Object.assign(CONFIG.alarms, data.alarms || {});
|
|
758
796
|
Object.assign(CONFIG.graphs, data.graphs || {});
|
|
@@ -772,8 +810,8 @@ function applyConfigData(data) {
|
|
|
772
810
|
}
|
|
773
811
|
|
|
774
812
|
/**
|
|
775
|
-
|
|
776
|
-
|
|
813
|
+
* Recupera la configurazione iniziale al caricamento della pagina
|
|
814
|
+
*/
|
|
777
815
|
async function fetchServerConfig() {
|
|
778
816
|
try {
|
|
779
817
|
const response = await fetch(getApiUrl('/rotevista-config'));
|
|
@@ -791,8 +829,8 @@ async function fetchServerConfig() {
|
|
|
791
829
|
}
|
|
792
830
|
|
|
793
831
|
/**
|
|
794
|
-
|
|
795
|
-
|
|
832
|
+
* Watchdog: Controlla in background se le impostazioni sul server sono cambiate
|
|
833
|
+
*/
|
|
796
834
|
async function watchConfigChanges() {
|
|
797
835
|
try {
|
|
798
836
|
const response = await fetch(getApiUrl('/rotevista-config'));
|
|
@@ -822,8 +860,8 @@ async function watchConfigChanges() {
|
|
|
822
860
|
}
|
|
823
861
|
|
|
824
862
|
/**
|
|
825
|
-
|
|
826
|
-
|
|
863
|
+
* Recupera lo storico dei grafici e dei radar pre-popolato dal server Signal K (Pro v6.0)
|
|
864
|
+
*/
|
|
827
865
|
async function fetchServerHistory() {
|
|
828
866
|
try {
|
|
829
867
|
const response = await fetch(getApiUrl('/rotevista-history'));
|
|
@@ -862,12 +900,12 @@ async function fetchServerHistory() {
|
|
|
862
900
|
}
|
|
863
901
|
|
|
864
902
|
/**
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
903
|
+
* manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
|
|
904
|
+
* Integrazioni:
|
|
905
|
+
* 1. Strict undefined check per lastUpdates.
|
|
906
|
+
* 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
|
|
907
|
+
* 3. Clamping di sicurezza (no negativi, no Infinity).
|
|
908
|
+
*/
|
|
871
909
|
function manageHistory(type, value) {
|
|
872
910
|
// --- 1. VALIDAZIONE INPUT RIGOROSA ---
|
|
873
911
|
if (value === undefined || value === null || !isFinite(value)) return;
|
|
@@ -1133,50 +1171,50 @@ async function init() {
|
|
|
1133
1171
|
initRadarTicks(); // Tacche del Wind Radar storico (weather-radar.js)
|
|
1134
1172
|
|
|
1135
1173
|
// 1. COMANDO TATTICO: Gestore Box TWD (Pressione prolungata -> Radar | Tocco rapido in modalità Radar -> Torna a Gauge)
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
if (twdPressTimer) {
|
|
1157
|
-
clearTimeout(twdPressTimer);
|
|
1158
|
-
twdPressTimer = null;
|
|
1159
|
-
}
|
|
1160
|
-
} else if (activeInstrument === 'radar') {
|
|
1161
|
-
if (longPressTriggered) {
|
|
1162
|
-
// Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
|
|
1163
|
-
longPressTriggered = false;
|
|
1164
|
-
} else {
|
|
1165
|
-
// Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
|
|
1166
|
-
activeInstrument = 'gauge';
|
|
1167
|
-
document.getElementById('wind-radar').style.display = 'none';
|
|
1168
|
-
document.getElementById('wind-gauge').style.display = 'block';
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
});
|
|
1172
|
-
twdBox.addEventListener('pointerleave', () => {
|
|
1174
|
+
const twdBox = document.querySelector('.box-twd');
|
|
1175
|
+
if (twdBox) {
|
|
1176
|
+
let twdPressTimer = null;
|
|
1177
|
+
let longPressTriggered = false; // Flag di controllo della pressione prolungata
|
|
1178
|
+
|
|
1179
|
+
twdBox.addEventListener('pointerdown', (e) => {
|
|
1180
|
+
longPressTriggered = false;
|
|
1181
|
+
if (activeInstrument === 'gauge') {
|
|
1182
|
+
twdPressTimer = setTimeout(() => {
|
|
1183
|
+
activeInstrument = 'radar';
|
|
1184
|
+
document.getElementById('wind-gauge').style.display = 'none';
|
|
1185
|
+
document.getElementById('wind-radar').style.display = 'block';
|
|
1186
|
+
renderRadar(); // Disegna immediatamente il radar all'attivazione
|
|
1187
|
+
twdPressTimer = null;
|
|
1188
|
+
longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
|
|
1189
|
+
}, 1000);
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
twdBox.addEventListener('pointerup', () => {
|
|
1193
|
+
if (activeInstrument === 'gauge') {
|
|
1173
1194
|
if (twdPressTimer) {
|
|
1174
1195
|
clearTimeout(twdPressTimer);
|
|
1175
1196
|
twdPressTimer = null;
|
|
1176
1197
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1198
|
+
} else if (activeInstrument === 'radar') {
|
|
1199
|
+
if (longPressTriggered) {
|
|
1200
|
+
// Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
|
|
1201
|
+
longPressTriggered = false;
|
|
1202
|
+
} else {
|
|
1203
|
+
// Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
|
|
1204
|
+
activeInstrument = 'gauge';
|
|
1205
|
+
document.getElementById('wind-radar').style.display = 'none';
|
|
1206
|
+
document.getElementById('wind-gauge').style.display = 'block';
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
twdBox.addEventListener('pointerleave', () => {
|
|
1211
|
+
if (twdPressTimer) {
|
|
1212
|
+
clearTimeout(twdPressTimer);
|
|
1213
|
+
twdPressTimer = null;
|
|
1214
|
+
}
|
|
1215
|
+
longPressTriggered = false;
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1180
1218
|
|
|
1181
1219
|
// 2. COMANDO TATTICO: Click in qualsiasi punto del radar per tornare all'analogico
|
|
1182
1220
|
const windRadarSvg = document.getElementById('wind-radar');
|
package/charts.js
CHANGED
|
@@ -156,14 +156,14 @@ function refreshGraph(t) {
|
|
|
156
156
|
// Genera fisicamente le curve e le aree SVG
|
|
157
157
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
158
158
|
const svg = document.getElementById(id);
|
|
159
|
-
if (!svg
|
|
159
|
+
if (!svg) return;
|
|
160
160
|
|
|
161
161
|
const w = 200, h = 40;
|
|
162
162
|
const range = max - min || 1;
|
|
163
163
|
const isDepth = (id === 'depth-graph');
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
const now =
|
|
165
|
+
// Utilizza sempre il tempo reale corrente per permettere lo scorrimento continuo dei dati
|
|
166
|
+
const now = Date.now();
|
|
167
167
|
|
|
168
168
|
const box = svg.closest('.data-box');
|
|
169
169
|
const isFocused = isFocusActive && box && box.classList.contains('is-focused');
|
|
@@ -173,7 +173,10 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
173
173
|
const viewportStart = now - viewportMs;
|
|
174
174
|
|
|
175
175
|
const visibleData = d.filter(p => p.time >= viewportStart);
|
|
176
|
-
if (visibleData.length < 2)
|
|
176
|
+
if (visibleData.length < 2) {
|
|
177
|
+
svg.innerHTML = ""; // Svuota completamente l'SVG se il sensore è spento e i dati sono scivolati fuori scala
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
177
180
|
|
|
178
181
|
const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
|
|
179
182
|
const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
|