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