@sailingrotevista/rotevista-dash 4.0.20 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +449 -175
- package/index.html +4 -1
- package/index.js +7 -7
- package/package.json +2 -2
- package/settings.json +3 -3
- package/style.css +3 -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
|
|
@@ -120,6 +121,10 @@ function getShortestRotation(curr, target) {
|
|
|
120
121
|
* Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
|
|
121
122
|
*/
|
|
122
123
|
function safePush(buffer, val, time) {
|
|
124
|
+
// --- PROTEZIONE ANTI-NaN (FONDAMENTALE ALL'AVVIO) ---
|
|
125
|
+
// Se il valore è nullo, non definito o non è un numero, ignoriamo l'inserimento
|
|
126
|
+
if (val === null || val === undefined || isNaN(val)) return;
|
|
127
|
+
|
|
123
128
|
buffer.push({
|
|
124
129
|
val: val,
|
|
125
130
|
time: time,
|
|
@@ -139,63 +144,78 @@ function safePush(buffer, val, time) {
|
|
|
139
144
|
if (buffer.length > 36000) buffer.shift();
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Media Circolare Vettoriale - Versione "Soft Outlier Rejection"
|
|
149
|
+
* Riduce l'impatto degli sbalzi limitando il loro angolo massimo di discostamento.
|
|
150
|
+
*/
|
|
142
151
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
|
|
143
152
|
now = now || Date.now();
|
|
144
|
-
|
|
145
153
|
const len = bufferArray.length;
|
|
146
154
|
if (len === 0) return null;
|
|
147
155
|
|
|
148
|
-
let sSin = 0;
|
|
149
|
-
let
|
|
150
|
-
let count = 0;
|
|
156
|
+
let sSin = 0, sCos = 0, count = 0;
|
|
157
|
+
let newestTime = 0, oldestTime = 0;
|
|
151
158
|
|
|
152
|
-
|
|
153
|
-
let
|
|
159
|
+
// 1. MEDIA PILOTA: Guardiamo l'ultima frazione di dati (es. max 15 campioni) per sapere dove punta "ora"
|
|
160
|
+
let pilotSin = 0, pilotCos = 0;
|
|
161
|
+
const pilotSamples = Math.min(len, 15);
|
|
162
|
+
for (let i = len - 1; i >= len - pilotSamples; i--) {
|
|
163
|
+
pilotSin += bufferArray[i].sin;
|
|
164
|
+
pilotCos += bufferArray[i].cos;
|
|
165
|
+
}
|
|
166
|
+
const pilotRad = Math.atan2(pilotSin, pilotCos);
|
|
167
|
+
|
|
168
|
+
// Il limite elastico in radianti (basato sul tuo stabilityBreakout in gradi)
|
|
169
|
+
const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
|
|
154
170
|
|
|
171
|
+
// 2. CALCOLO AMMORTIZZATO
|
|
155
172
|
for (let i = len - 1; i >= 0; i--) {
|
|
156
173
|
const item = bufferArray[i];
|
|
157
|
-
|
|
158
174
|
if ((now - item.time) > windowMs) break;
|
|
159
175
|
|
|
160
|
-
|
|
161
|
-
|
|
176
|
+
// Troviamo la differenza angolare (da -Pi a +Pi) tra il dato e la Media Pilota
|
|
177
|
+
let diffRad = Math.atan2(
|
|
178
|
+
Math.sin(item.val - pilotRad),
|
|
179
|
+
Math.cos(item.val - pilotRad)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
let finalSin, finalCos;
|
|
183
|
+
|
|
184
|
+
// Se lo scarto è maggiore del limite, "Pattiniamo" (Ammortizzazione)
|
|
185
|
+
if (Math.abs(diffRad) > limitRad) {
|
|
186
|
+
// Tronchiamo la differenza al limite massimo consentito (mantenendo il segno)
|
|
187
|
+
const clampedDiff = Math.sign(diffRad) * limitRad;
|
|
188
|
+
// Ricalcoliamo l'angolo ammortizzato
|
|
189
|
+
const clampedRad = pilotRad + clampedDiff;
|
|
190
|
+
|
|
191
|
+
finalSin = Math.sin(clampedRad);
|
|
192
|
+
finalCos = Math.cos(clampedRad);
|
|
193
|
+
} else {
|
|
194
|
+
// Il dato è buono, usiamo i valori precalcolati
|
|
195
|
+
finalSin = item.sin;
|
|
196
|
+
finalCos = item.cos;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sSin += finalSin;
|
|
200
|
+
sCos += finalCos;
|
|
162
201
|
|
|
163
202
|
if (count === 0) newestTime = item.time;
|
|
164
203
|
oldestTime = item.time;
|
|
165
|
-
|
|
166
204
|
count++;
|
|
167
205
|
}
|
|
168
206
|
|
|
169
207
|
if (count === 0) return null;
|
|
170
208
|
|
|
171
209
|
const R = Math.hypot(sSin, sCos) / count;
|
|
172
|
-
|
|
173
210
|
const avgRad = Math.atan2(sSin, sCos);
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
? avgRad
|
|
177
|
-
: (avgRad + Math.PI * 2) % (Math.PI * 2);
|
|
178
|
-
|
|
179
|
-
const historyDuration =
|
|
180
|
-
(count > 2)
|
|
181
|
-
? (newestTime - oldestTime)
|
|
182
|
-
: 0;
|
|
183
|
-
|
|
184
|
-
const isStable =
|
|
185
|
-
historyDuration > 10000 &&
|
|
186
|
-
R > CONFIG.averaging.stabilityThreshold;
|
|
187
|
-
|
|
211
|
+
const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
|
|
212
|
+
const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
|
|
188
213
|
const safeR = Math.max(R, 1e-9);
|
|
189
214
|
|
|
190
215
|
return {
|
|
191
216
|
val: finalVal,
|
|
192
|
-
stable:
|
|
193
|
-
dev: (R < 1)
|
|
194
|
-
? Math.round(
|
|
195
|
-
Math.sqrt(-2 * Math.log(safeR)) *
|
|
196
|
-
(180 / Math.PI)
|
|
197
|
-
)
|
|
198
|
-
: 0,
|
|
217
|
+
stable: historyDuration > 10000 && R > CONFIG.averaging.stabilityThreshold,
|
|
218
|
+
dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
|
|
199
219
|
samples: count
|
|
200
220
|
};
|
|
201
221
|
}
|
|
@@ -217,6 +237,7 @@ function saveDashboardState() {
|
|
|
217
237
|
histories: store.histories,
|
|
218
238
|
longBuf: store.longBuf,
|
|
219
239
|
displayModeSog: displayModeSog,
|
|
240
|
+
displayModeTws: displayModeTws,
|
|
220
241
|
graphModes: graphModes,
|
|
221
242
|
isNightMode: document.body.classList.contains('night-mode'),
|
|
222
243
|
isFocusActive: isFocusActive,
|
|
@@ -248,6 +269,13 @@ function loadDashboardState() {
|
|
|
248
269
|
const labelEl = document.getElementById('sog-vmg-label');
|
|
249
270
|
if (labelEl) labelEl.textContent = displayModeSog;
|
|
250
271
|
}
|
|
272
|
+
|
|
273
|
+
// Ripristino TWS/AWS ---
|
|
274
|
+
if (state.displayModeTws) {
|
|
275
|
+
displayModeTws = state.displayModeTws;
|
|
276
|
+
const labelEl = document.getElementById('tws-aws-label');
|
|
277
|
+
if (labelEl) labelEl.textContent = displayModeTws;
|
|
278
|
+
}
|
|
251
279
|
|
|
252
280
|
// Ripristino Tema Notte
|
|
253
281
|
if (state.isNightMode) document.body.classList.add('night-mode');
|
|
@@ -496,25 +524,61 @@ function updateWindTrend() {
|
|
|
496
524
|
// ==========================================================================
|
|
497
525
|
// 7. RENDERING ENGINE E AGGIORNAMENTO UI
|
|
498
526
|
// ==========================================================================
|
|
527
|
+
/**
|
|
528
|
+
* refreshGraph: Recupera i dati corretti dallo store e coordina il disegno del grafico.
|
|
529
|
+
* Gestisce lo switch tra TWS/AWS e la mappatura VMG -> SOG.
|
|
530
|
+
*
|
|
531
|
+
* @param {string} t - Il tipo di dato da aggiornare ('stw', 'sog', 'depth', 'tws', 'vmg')
|
|
532
|
+
*/
|
|
499
533
|
function refreshGraph(t) {
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
534
|
+
// 1. Mappatura del box UI: il VMG condivide il riquadro fisico del SOG
|
|
535
|
+
const boxType = (t === 'vmg') ? 'sog' : t;
|
|
536
|
+
|
|
537
|
+
// 2. Selezione della sorgente dati corretta
|
|
538
|
+
let data;
|
|
539
|
+
if (t === 'tws' && displayModeTws === 'AWS') {
|
|
540
|
+
// Se siamo nel box vento e la modalità è AWS, carichiamo la storia dell'apparente
|
|
541
|
+
data = store.histories['aws'];
|
|
542
|
+
} else {
|
|
543
|
+
// Altrimenti carichiamo la storia standard (TWS, STW, SOG, Depth, VMG)
|
|
544
|
+
data = store.histories[t];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 3. Controllo integrità: se non ci sono dati sufficienti, non disegniamo nulla
|
|
548
|
+
if (!data || data.length < 2) return;
|
|
549
|
+
|
|
550
|
+
// 4. Configurazione Scala: recupera la modalità (standard/hercules) e calcola min/max
|
|
551
|
+
const mode = graphModes[boxType];
|
|
552
|
+
const cfg = calculateScale(boxType, data, mode);
|
|
503
553
|
|
|
504
|
-
//
|
|
505
|
-
const box = document.querySelector(`.box-${
|
|
506
|
-
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
|
+
}
|
|
507
559
|
|
|
508
|
-
|
|
509
|
-
|
|
560
|
+
// 6. Aggiornamento etichette numeriche della scala (Y-axis)
|
|
561
|
+
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
562
|
+
|
|
563
|
+
// 7. Render finale del grafico SVG
|
|
564
|
+
// Il parametro 't === tws' indica a drawGraph di attivare la logica dei colori Reef (Rosso/Arancio)
|
|
565
|
+
drawGraph(
|
|
566
|
+
data,
|
|
567
|
+
boxType + '-graph',
|
|
568
|
+
cfg.min,
|
|
569
|
+
cfg.max,
|
|
570
|
+
t === 'tws',
|
|
571
|
+
mode === 'hercules'
|
|
572
|
+
);
|
|
510
573
|
}
|
|
511
574
|
|
|
512
575
|
/**
|
|
513
576
|
* upUI: Aggiornamento valori digitali con logica anti-ritardo (Istantaneo vs Media)
|
|
514
577
|
*/
|
|
515
578
|
const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
516
|
-
if (!obj || obj.val === null || instantRaw === undefined) {
|
|
517
|
-
|
|
579
|
+
if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
|
|
580
|
+
el.innerHTML = "---°";
|
|
581
|
+
el.classList.remove('unstable-data');
|
|
518
582
|
} else {
|
|
519
583
|
let valDeg = Math.round(radToDeg(obj.val));
|
|
520
584
|
let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
@@ -528,9 +592,6 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
528
592
|
}
|
|
529
593
|
};
|
|
530
594
|
|
|
531
|
-
/**
|
|
532
|
-
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
533
|
-
*/
|
|
534
595
|
/**
|
|
535
596
|
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
536
597
|
* Gestisce la gerarchia di aggiornamento Live (1s), Heavy (2s) e Slow (3s).
|
|
@@ -538,95 +599,164 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
538
599
|
function startDisplayLoop() {
|
|
539
600
|
renderInterval = setInterval(() => {
|
|
540
601
|
const now = Date.now();
|
|
602
|
+
const isNight = document.body.classList.contains('night-mode');
|
|
603
|
+
|
|
604
|
+
// Conversione velocità da m/s a Nodi
|
|
541
605
|
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
|
|
542
606
|
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
543
607
|
|
|
544
|
-
// Verifica stato navigazione basato su soglia impostata
|
|
608
|
+
// Verifica stato navigazione basato su soglia impostata (minSpeed)
|
|
545
609
|
isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
|
|
546
610
|
|
|
547
611
|
// --- TIER LIVE (1s): CONTROLLO TIMEOUT DATI ---
|
|
548
|
-
|
|
612
|
+
// Se un dato non arriva da più di 5 secondi, mostra i trattini
|
|
613
|
+
const watch = {
|
|
614
|
+
"navigation.speedThroughWater": ui.stw,
|
|
615
|
+
"navigation.speedOverGround": ui.sog,
|
|
616
|
+
"navigation.headingTrue": ui.hdg,
|
|
617
|
+
"navigation.courseOverGroundTrue": ui.cog,
|
|
618
|
+
"environment.wind.speedApparent": ui.awsSvg,
|
|
619
|
+
"environment.depth.belowTransducer": ui.depth,
|
|
620
|
+
"environment.wind.speedTrue": ui.tws
|
|
621
|
+
};
|
|
549
622
|
for (let p in watch) {
|
|
550
623
|
if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
|
|
551
624
|
watch[p].innerText = "---"; delete store.raw[p];
|
|
552
625
|
}
|
|
553
626
|
}
|
|
554
627
|
|
|
555
|
-
// --- AGGIORNAMENTO
|
|
628
|
+
// --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
|
|
556
629
|
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
557
|
-
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);
|
|
558
634
|
}
|
|
559
635
|
|
|
560
|
-
// --- LOGICA SOG / VMG
|
|
636
|
+
// --- LOGICA SOG / VMG (COLORI DINAMICI E FILTRO NAVIGAZIONE) ---
|
|
561
637
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
638
|
+
// Calcolo VMG istantanea per il display numerico
|
|
562
639
|
const vmg = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
563
|
-
|
|
640
|
+
|
|
641
|
+
// Registriamo i dati nelle storie (per i grafici)
|
|
642
|
+
manageHistory('vmg', vmg);
|
|
643
|
+
manageHistory('sog', sogKts);
|
|
564
644
|
|
|
565
645
|
const labelEl = document.getElementById('sog-vmg-label');
|
|
646
|
+
|
|
566
647
|
if (displayModeSog === 'VMG') {
|
|
648
|
+
// MODALITÀ VMG: Mostra valore istantaneo, colore Cyan fisso per identificare la modalità
|
|
567
649
|
ui.sog.innerText = vmg.toFixed(1);
|
|
568
|
-
// AGGIORNATO AL NUOVO COLORE CYAN VIBRANTE (#00b8d4)
|
|
569
650
|
ui.sog.style.setProperty('color', '#00b8d4', 'important');
|
|
570
651
|
if (labelEl) labelEl.textContent = 'VMG';
|
|
571
652
|
} else {
|
|
653
|
+
// MODALITÀ SOG: Mostra valore istantaneo
|
|
572
654
|
ui.sog.innerText = sogKts.toFixed(1);
|
|
573
655
|
if (labelEl) labelEl.textContent = 'SOG';
|
|
574
656
|
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
657
|
+
// --- LOGICA COLORE DINAMICO (Solo se in navigazione reale) ---
|
|
658
|
+
if (isNavigating) {
|
|
659
|
+
// Usiamo la media dell'ultimo punto del grafico per stabilizzare il colore (anti-onda)
|
|
660
|
+
const lastAvgSog = store.histories.sog.length > 0 ? store.histories.sog[store.histories.sog.length - 1] : sogKts;
|
|
661
|
+
const lastAvgStw = store.histories.stw.length > 0 ? store.histories.stw[store.histories.stw.length - 1] : stwKts;
|
|
662
|
+
const deltaCurrent = lastAvgSog - lastAvgStw;
|
|
663
|
+
|
|
664
|
+
if (deltaCurrent < -0.3) {
|
|
665
|
+
// CORRENTE CONTRO: Rosso Vivido
|
|
666
|
+
ui.sog.style.setProperty('color', '#ff3b30', 'important');
|
|
667
|
+
} else if (deltaCurrent > 0.3) {
|
|
668
|
+
// CORRENTE A FAVORE: Verde Neon (Feedback Positivo)
|
|
669
|
+
ui.sog.style.setProperty('color', '#00C851', 'important');
|
|
670
|
+
} else {
|
|
671
|
+
// NEUTRO: Amber (Colore base della linea SOG)
|
|
672
|
+
ui.sog.style.setProperty('color', '#ffbb33', 'important');
|
|
673
|
+
}
|
|
674
|
+
} else {
|
|
675
|
+
// NON IN NAVIGAZIONE: Riportiamo il colore al default neutro
|
|
676
|
+
ui.sog.style.color = "";
|
|
677
|
+
}
|
|
580
678
|
}
|
|
581
679
|
}
|
|
582
680
|
|
|
681
|
+
// --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
|
|
583
682
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
584
683
|
ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
|
|
684
|
+
// Il colore neutro/allarme è gestito internamente dalla funzione checkDepthAlarm
|
|
585
685
|
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
586
686
|
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
587
687
|
}
|
|
588
688
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
else ui.tws.style.color = "";
|
|
689
|
+
// --- GESTIONE VENTO (TWS / AWS SWITCH CON COLORI COORDINATI) ---
|
|
690
|
+
const twsKts = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
691
|
+
const awsKts = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
|
|
692
|
+
|
|
693
|
+
// Registriamo sempre entrambe le storie per permettere lo switch fluido dei grafici
|
|
694
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsKts);
|
|
695
|
+
if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsKts);
|
|
597
696
|
|
|
598
|
-
|
|
697
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
698
|
+
const labelEl = document.getElementById('tws-aws-label');
|
|
699
|
+
const currentWindValue = (displayModeTws === 'AWS') ? awsKts : twsKts;
|
|
700
|
+
|
|
701
|
+
ui.tws.innerText = currentWindValue.toFixed(1);
|
|
702
|
+
if (labelEl) labelEl.textContent = displayModeTws;
|
|
703
|
+
|
|
704
|
+
// Logica Colore Testo Vento (Priorità ai Reef, poi colore di base modalità)
|
|
705
|
+
if (currentWindValue >= CONFIG.graphs.reef2) {
|
|
706
|
+
ui.tws.style.setProperty('color', '#ff3b30', 'important'); // Rosso Reef 2
|
|
707
|
+
} else if (currentWindValue >= CONFIG.graphs.reef1) {
|
|
708
|
+
ui.tws.style.setProperty('color', '#ff9800', 'important'); // Arancio Reef 1
|
|
709
|
+
} else {
|
|
710
|
+
// Colore di base differenziato per modalità (Indaco per AWS, Navy per TWS)
|
|
711
|
+
if (displayModeTws === 'AWS') {
|
|
712
|
+
ui.tws.style.setProperty('color', '#5c6bc0', 'important'); // Indigo
|
|
713
|
+
} else {
|
|
714
|
+
// Per il TWS, schiariamo il Navy in modalità notte per renderlo leggibile
|
|
715
|
+
const twsColor = isNight ? '#6c8ea0' : '#2c3e50';
|
|
716
|
+
ui.tws.style.setProperty('color', twsColor, 'important');
|
|
717
|
+
}
|
|
718
|
+
}
|
|
599
719
|
}
|
|
600
720
|
|
|
721
|
+
// --- AGGIORNAMENTO NUMERO AWS CENTRALE (Bussola) ---
|
|
601
722
|
if (store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
602
|
-
|
|
723
|
+
const awsVal = msToKts(store.raw["environment.wind.speedApparent"]);
|
|
724
|
+
ui.awsSvg.textContent = awsVal.toFixed(1);
|
|
603
725
|
}
|
|
604
726
|
|
|
605
727
|
// --- PUNTATORI ANALOGICI (Smoothing 2s) ---
|
|
606
728
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
|
|
607
729
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
|
|
608
|
-
if (smAwa) {
|
|
609
|
-
|
|
730
|
+
if (smAwa) {
|
|
731
|
+
curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val));
|
|
732
|
+
ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`);
|
|
733
|
+
}
|
|
734
|
+
if (smTwa) {
|
|
735
|
+
curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val));
|
|
736
|
+
ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`);
|
|
737
|
+
}
|
|
610
738
|
|
|
611
739
|
// --- CALCOLO LEEWAY E TRACK POINTER ---
|
|
612
740
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
613
741
|
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
614
|
-
//
|
|
742
|
+
// Filtraggio scarroccio: azzeramento se barca ferma, altrimenti smoothing
|
|
615
743
|
smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
616
|
-
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
617
|
-
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" : "";
|
|
618
747
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
619
748
|
}
|
|
620
749
|
|
|
750
|
+
// Aggiorna i trend meteo/tattici (pallini) e l'allarme strambata
|
|
621
751
|
updateWindTrend();
|
|
622
752
|
|
|
623
|
-
// TIER HEAVY (2s) - Grafici e
|
|
753
|
+
// TIER HEAVY (2s) - Aggiornamento Grafici e Salvataggio Stato
|
|
624
754
|
if (lastAvgUIUpdate++ % 2 === 0) {
|
|
625
755
|
['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
|
|
626
756
|
saveDashboardState();
|
|
627
757
|
}
|
|
628
758
|
|
|
629
|
-
// TIER SLOW (3s) - Medie Lunghe e
|
|
759
|
+
// TIER SLOW (3s) - Calcolo Medie Lunghe e Tack Strategico
|
|
630
760
|
if (lastAvgUIUpdate % 3 === 0) {
|
|
631
761
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
|
|
632
762
|
let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
|
|
@@ -634,6 +764,7 @@ function startDisplayLoop() {
|
|
|
634
764
|
let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
|
|
635
765
|
let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
|
|
636
766
|
|
|
767
|
+
// Aggiornamento interfaccia per i valori mediati (Heading, Cog, Awa, Twa, Twd)
|
|
637
768
|
upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
|
|
638
769
|
upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
|
|
639
770
|
upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
|
|
@@ -642,27 +773,25 @@ function startDisplayLoop() {
|
|
|
642
773
|
|
|
643
774
|
// --- LOGICA TACK STRATEGICA (VETTORIALE) ---
|
|
644
775
|
if (hObj && twdObj) {
|
|
645
|
-
// Funzione interna per riflettere un angolo rispetto all'asse del vento (TWD)
|
|
646
776
|
const reflectAngle = (targetRad, axisRad) => {
|
|
647
|
-
const
|
|
648
|
-
const
|
|
649
|
-
return Math.atan2(Math.sin(axisRad) *
|
|
650
|
-
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);
|
|
651
781
|
};
|
|
652
|
-
|
|
782
|
+
|
|
653
783
|
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
654
|
-
|
|
784
|
+
|
|
655
785
|
if (!isNavigating) {
|
|
656
786
|
ui.tackHdg.innerHTML = "---°";
|
|
657
787
|
} else if (unstableH) {
|
|
658
788
|
ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data');
|
|
659
789
|
} else {
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
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')}°`;
|
|
663
792
|
ui.tackHdg.classList.remove('unstable-data');
|
|
664
793
|
}
|
|
665
|
-
|
|
794
|
+
|
|
666
795
|
if (cObj) {
|
|
667
796
|
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
668
797
|
if (!isNavigating) {
|
|
@@ -670,20 +799,21 @@ function startDisplayLoop() {
|
|
|
670
799
|
} else if (unstableC) {
|
|
671
800
|
ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data');
|
|
672
801
|
} else {
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
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')}°`;
|
|
676
804
|
ui.tackCog.classList.remove('unstable-data');
|
|
677
805
|
}
|
|
678
806
|
}
|
|
679
807
|
}
|
|
680
808
|
|
|
681
|
-
// Rotazione Mini-Bussole
|
|
809
|
+
// Rotazione Mini-Icone nella bussola TWD (Mini-Bussole)
|
|
682
810
|
const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
|
|
683
811
|
const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
684
812
|
if (smHdgIcons && smTwdIcons) {
|
|
685
|
-
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
|
|
686
|
-
|
|
813
|
+
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
|
|
814
|
+
ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
815
|
+
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val));
|
|
816
|
+
ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
687
817
|
}
|
|
688
818
|
}
|
|
689
819
|
}, RENDER_INTERVAL_MS);
|
|
@@ -693,7 +823,7 @@ function startDisplayLoop() {
|
|
|
693
823
|
// 8. CONFIGURAZIONE E GRAFICI UTILS
|
|
694
824
|
// ==========================================================================
|
|
695
825
|
/**
|
|
696
|
-
* Recupera la configurazione e
|
|
826
|
+
* Recupera la configurazione dal server e applica migrazioni automatiche per le vecchie versioni
|
|
697
827
|
*/
|
|
698
828
|
async function fetchServerConfig() {
|
|
699
829
|
try {
|
|
@@ -704,19 +834,27 @@ async function fetchServerConfig() {
|
|
|
704
834
|
// Stampa di debug per verificare cosa riceve il client
|
|
705
835
|
console.log("🔍 Configurazione ricevuta dal Server:", data);
|
|
706
836
|
|
|
707
|
-
// Merge intelligente dei dati ricevuti
|
|
837
|
+
// Merge intelligente dei dati ricevuti
|
|
708
838
|
Object.assign(CONFIG.alarms, data.alarms || {});
|
|
709
839
|
Object.assign(CONFIG.graphs, data.graphs || {});
|
|
710
840
|
Object.assign(CONFIG.averaging, data.averaging || {});
|
|
711
841
|
|
|
712
|
-
//
|
|
842
|
+
// --- LOGICA DI MIGRAZIONE SILENZIOSA ---
|
|
843
|
+
// Se il valore ricevuto è il vecchio default (0.85) o inferiore, lo portiamo al nuovo standard 0.95.
|
|
844
|
+
// Questo è necessario perché con i nuovi filtri "Soft" lo 0.85 non farebbe quasi mai lampeggiare gli allarmi.
|
|
845
|
+
if (CONFIG.averaging.stabilityThreshold <= 0.85) {
|
|
846
|
+
CONFIG.averaging.stabilityThreshold = 0.95;
|
|
847
|
+
console.log("♻️ Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85). Aggiornato a 0.95 per ottimizzazione filtri.");
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Per le scale, siccome sono nidificate, facciamo un loop di merge profondo
|
|
713
851
|
if (data.scales) {
|
|
714
852
|
for (let key in data.scales) {
|
|
715
853
|
if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
|
|
716
854
|
}
|
|
717
855
|
}
|
|
718
856
|
|
|
719
|
-
console.log("✅ Configurazione applicata.
|
|
857
|
+
console.log("✅ Configurazione applicata. Stabilità attiva:", CONFIG.averaging.stabilityThreshold);
|
|
720
858
|
} catch (err) {
|
|
721
859
|
console.warn("⚠️ Utilizzo default locali. Motivo:", err.message);
|
|
722
860
|
}
|
|
@@ -750,133 +888,269 @@ function updateScaleLabels(t, min, max) {
|
|
|
750
888
|
}
|
|
751
889
|
|
|
752
890
|
/**
|
|
753
|
-
*
|
|
754
|
-
*
|
|
891
|
+
* ==========================================================================
|
|
892
|
+
* drawGraph: Motore di Rendering SVG "Tactical Precision" (Integrale)
|
|
893
|
+
* ==========================================================================
|
|
894
|
+
* Caratteristiche:
|
|
895
|
+
* - Linea dinamica: 1.0px (base) / 1.5px (allerta).
|
|
896
|
+
* - Area segmentata: colore preciso sotto i picchi (0.15 / 0.45 / 0.85).
|
|
897
|
+
* - Fix Colore Bleeding: Usa 'userSpaceOnUse' per mantenere i colori al loro posto.
|
|
898
|
+
* - Fix Diagonale: Chiusura verticale sull'ultimo punto reale.
|
|
899
|
+
* - Palette Vivid Glass: Colori distinti per ogni modalità.
|
|
755
900
|
*/
|
|
756
901
|
/**
|
|
757
|
-
*
|
|
758
|
-
*
|
|
902
|
+
* ==========================================================================
|
|
903
|
+
* drawGraph: Motore di Rendering Grafico SVG "Tactical Glass" (Versione Pro)
|
|
904
|
+
* ==========================================================================
|
|
905
|
+
* Questa funzione trasforma un array di dati in una sparkline SVG dinamica.
|
|
906
|
+
*
|
|
907
|
+
* Caratteristiche principali:
|
|
908
|
+
* 1. LINEA CHIRURGICA: Mantiene uno spessore di 1px costante per un look tecnico.
|
|
909
|
+
* 2. AREA DINAMICA: L'area sottesa cambia colore solo sotto i picchi di allerta.
|
|
910
|
+
* 3. ZERO ARTEFATTI:
|
|
911
|
+
* - Usa LinearGradients con 'userSpaceOnUse' per evitare trascinamenti di colore.
|
|
912
|
+
* - Elimina le righe verticali di giunzione tra i segmenti.
|
|
913
|
+
* - Chiude il tracciato verticalmente eliminando la "coda" diagonale finale.
|
|
914
|
+
* 4. MULTI-MODALITÀ: Gestisce switch AWS/TWS, SOG/VMG e allarmi Profondità.
|
|
759
915
|
*/
|
|
760
916
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
761
917
|
const svg = document.getElementById(id);
|
|
918
|
+
|
|
919
|
+
// Uscita di sicurezza se l'elemento non esiste o non ci sono abbastanza dati
|
|
762
920
|
if (!svg || d.length < 2) return;
|
|
763
921
|
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
const
|
|
767
|
-
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
|
+
};
|
|
768
986
|
|
|
769
|
-
//
|
|
987
|
+
// --- 3. COSTRUZIONE DELLE GRIGLIE DI RIFERIMENTO ---
|
|
770
988
|
let grids = "";
|
|
989
|
+
// Linee orizzontali di livello (25%, 50%, 75%)
|
|
771
990
|
[0.25, 0.5, 0.75].forEach(p => {
|
|
772
991
|
grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(0,0,0,0.12)" stroke-width="0.5" />`;
|
|
773
992
|
});
|
|
993
|
+
// Linee verticali temporali (dinamiche in base alla storia impostata)
|
|
774
994
|
const gridInterval = (CONFIG.graphs.historyMinutes <= 15) ? 1 : 5;
|
|
775
995
|
for (let m = gridInterval; m < CONFIG.graphs.historyMinutes; m += gridInterval) {
|
|
776
996
|
const x = w - (m / CONFIG.graphs.historyMinutes) * w;
|
|
777
997
|
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
778
998
|
}
|
|
779
999
|
|
|
780
|
-
//
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const getColor = (val) => {
|
|
785
|
-
if (isTws) return (val >= CONFIG.graphs.reef2) ? "#ff3b30" : (val >= CONFIG.graphs.reef1 ? "#ff9800" : baseColorTws);
|
|
786
|
-
if (isDepth) return (val < CONFIG.alarms.depthDanger) ? "#ff3b30" : (val < CONFIG.alarms.depthWarning ? "#ff9800" : baseColorDepth);
|
|
787
|
-
switch(id) {
|
|
788
|
-
case 'stw-graph': return "#00C851";
|
|
789
|
-
case 'sog-graph': return (displayModeSog === 'VMG') ? "#00b8d4" : "#ffbb33";
|
|
790
|
-
default: return baseColorTws;
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
|
|
794
|
-
// 3. Creazione Gradiente e Linee
|
|
795
|
-
let gradientStops = "";
|
|
796
|
-
let lines = "";
|
|
797
|
-
let areaPath = `M 0 ${h} `;
|
|
1000
|
+
// --- 4. CICLO DI ELABORAZIONE DEI DATI ---
|
|
1001
|
+
let gradientStops = ""; // Contiene i cambi di colore per l'area
|
|
1002
|
+
let lines = ""; // Contiene i segmenti della linea superiore
|
|
1003
|
+
let areaPath = `M 0 ${h} `; // Inizio del path del riempimento dal fondo sinistro
|
|
798
1004
|
|
|
799
1005
|
for (let i = 1; i < d.length; i++) {
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
|
|
1006
|
+
// Calcolo delle posizioni percentuali (per il gradiente) e pixel (per il disegno)
|
|
1007
|
+
const p1 = ((i - 1) / (samples - 1)) * 100;
|
|
1008
|
+
const p2 = (i / (samples - 1)) * 100;
|
|
803
1009
|
const x1 = ((i - 1) / (samples - 1)) * w;
|
|
804
1010
|
const y1 = h - (Math.max(0, Math.min(1, (d[i - 1] - min) / range)) * h);
|
|
805
1011
|
const x2 = (i / (samples - 1)) * w;
|
|
806
1012
|
const y2 = h - (Math.max(0, Math.min(1, (d[i] - min) / range)) * h);
|
|
807
1013
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
// Logica Opacità e Spessore (Tua configurazione)
|
|
811
|
-
let fillOpacity = "0.15";
|
|
812
|
-
let strokeWidth = "1";
|
|
813
|
-
|
|
814
|
-
if (color === "#ff3b30") {
|
|
815
|
-
fillOpacity = "0.85"; // Rosso: Molto visibile
|
|
816
|
-
strokeWidth = "1.5"; // Rosso: Più marcato
|
|
817
|
-
} else if (color === "#ff9800") {
|
|
818
|
-
fillOpacity = "0.45"; // Arancio: Velo medio
|
|
819
|
-
strokeWidth = "1"; // Arancio: Sottile
|
|
820
|
-
}
|
|
1014
|
+
// Otteniamo lo stile per questo specifico segmento
|
|
1015
|
+
const props = getColorProps(d[i]);
|
|
821
1016
|
|
|
822
|
-
//
|
|
823
|
-
|
|
824
|
-
|
|
1017
|
+
// AGGIUNTA STOP AL GRADIENTE:
|
|
1018
|
+
// Usiamo due stop identici alla stessa percentuale per creare stacchi di colore netti,
|
|
1019
|
+
// evitando sfumature tra una zona sicura e una di allarme.
|
|
1020
|
+
gradientStops += `<stop offset="${p1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
1021
|
+
gradientStops += `<stop offset="${p2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
825
1022
|
|
|
826
|
-
//
|
|
1023
|
+
// DISEGNO DELLA LINEA SUPERIORE:
|
|
1024
|
+
// Usiamo linee individuali per poter gestire spessori e colori diversi (se necessario in futuro)
|
|
827
1025
|
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
|
|
828
|
-
style="stroke: ${color}; stroke-width: ${
|
|
1026
|
+
style="stroke: ${props.color}; stroke-width: ${props.stroke}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
|
|
829
1027
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1028
|
+
// AGGIORNAMENTO DEL PATH DELL'AREA:
|
|
1029
|
+
if (i === 1) areaPath += `L ${x1} ${y1} `; // Primo collegamento con la base
|
|
1030
|
+
areaPath += `L ${x2} ${y2} `; // Tracciato dei picchi
|
|
1031
|
+
|
|
1032
|
+
// --- FIX ARTEFATTO DIAGONALE FINALE ---
|
|
1033
|
+
// Se siamo arrivati all'ultimo dato disponibile nel buffer, chiudiamo il path
|
|
1034
|
+
// scendendo verticalmente verso l'asse zero (h), poi chiudiamo con 'Z'.
|
|
1035
|
+
if (i === d.length - 1) {
|
|
1036
|
+
areaPath += `L ${x2} ${h} Z`;
|
|
837
1037
|
}
|
|
1038
|
+
}
|
|
838
1039
|
|
|
839
|
-
//
|
|
840
|
-
const gradId = `grad-${id}`;
|
|
1040
|
+
// --- 5. CREAZIONE DEL GRADIENTE LINEARE DINAMICO ---
|
|
1041
|
+
const gradId = `grad-${id}`; // ID unico basato sul box (es: grad-tws-graph)
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* IMPORTANTE: gradientUnits="userSpaceOnUse" fissa il gradiente alle coordinate
|
|
1045
|
+
* assolute (0-200px). Questo impedisce al colore di allarme di "allungarsi"
|
|
1046
|
+
* erroneamente se il grafico è solo a metà schermo.
|
|
1047
|
+
*/
|
|
841
1048
|
const defs = `
|
|
842
1049
|
<defs>
|
|
843
|
-
<linearGradient id="${gradId}" x1="0
|
|
1050
|
+
<linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">
|
|
844
1051
|
${gradientStops}
|
|
845
1052
|
</linearGradient>
|
|
846
1053
|
</defs>
|
|
847
1054
|
`;
|
|
848
1055
|
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
} else {
|
|
853
|
-
// Per STW/SOG usiamo il colore dell'ultimo punto con area fissa 0.15
|
|
854
|
-
const currentPathColor = getColor(d[d.length - 1]);
|
|
855
|
-
svg.innerHTML = `${grids}<path d="${areaPath}" fill="${currentPathColor}" fill-opacity="0.15" stroke="none" />${lines}`;
|
|
856
|
-
}
|
|
1056
|
+
// --- 6. AGGIORNAMENTO FINALE DEL DOM ---
|
|
1057
|
+
// Inseriamo tutto nell'elemento SVG: Defs (Gradienti) -> Griglie -> Area (Fill) -> Linee (Stroke)
|
|
1058
|
+
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
857
1059
|
}
|
|
858
1060
|
|
|
859
1061
|
// ==========================================================================
|
|
860
1062
|
// 9. INTERAZIONI E GESTI
|
|
861
1063
|
// ==========================================================================
|
|
1064
|
+
/**
|
|
1065
|
+
* toggleFocusMode: Gestisce l'attivazione della modalità Split-Screen (Focus).
|
|
1066
|
+
* Ingrandisce un box specifico e mantiene visibile la bussola centrale.
|
|
1067
|
+
*/
|
|
862
1068
|
function toggleFocusMode(type, element) {
|
|
863
1069
|
const container = document.querySelector('.main-container');
|
|
1070
|
+
|
|
1071
|
+
// Identifica se il box appartiene alla colonna sinistra (per il layout CSS)
|
|
864
1072
|
const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
|
|
1073
|
+
|
|
1074
|
+
// Inverte lo stato del Focus
|
|
865
1075
|
isFocusActive = !isFocusActive;
|
|
866
|
-
|
|
867
|
-
|
|
1076
|
+
|
|
1077
|
+
if (isFocusActive) {
|
|
1078
|
+
// Applica le classi per dividere lo schermo e mettere in risalto il box
|
|
1079
|
+
container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right');
|
|
1080
|
+
element.classList.add('is-focused');
|
|
1081
|
+
} else {
|
|
1082
|
+
// Rimuove tutte le classi di focus e torna alla griglia standard
|
|
1083
|
+
container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
|
|
1084
|
+
document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
|
|
1085
|
+
}
|
|
868
1086
|
}
|
|
869
1087
|
|
|
1088
|
+
/**
|
|
1089
|
+
* Inizializzazione Gesti e Interazioni sui box con grafico.
|
|
1090
|
+
* Gestisce: Singolo Tap (Switch), Doppio Tap (Zoom), Long Press (Focus).
|
|
1091
|
+
*/
|
|
870
1092
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
871
1093
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
872
1094
|
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
873
|
-
|
|
1095
|
+
|
|
1096
|
+
// --- GESTIONE PRESSIONE (Inizio) ---
|
|
1097
|
+
el.addEventListener('pointerdown', (e) => {
|
|
1098
|
+
isLongPressActive = false;
|
|
1099
|
+
// Timer per attivare il Focus Mode dopo 1 secondo di pressione continua
|
|
1100
|
+
pressTimer = setTimeout(() => {
|
|
1101
|
+
if (!isFocusActive) {
|
|
1102
|
+
isLongPressActive = true;
|
|
1103
|
+
toggleFocusMode(type, el);
|
|
1104
|
+
lastTapTime = 0; // Evita che al rilascio scatti un click
|
|
1105
|
+
}
|
|
1106
|
+
}, 1000);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// --- GESTIONE RILASCIO (Fine Gesto) ---
|
|
874
1110
|
el.addEventListener('pointerup', (e) => {
|
|
875
|
-
clearTimeout(pressTimer);
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1111
|
+
clearTimeout(pressTimer); // Cancella il timer del long press
|
|
1112
|
+
if (isLongPressActive) return; // Se è scattato il focus, non fare altro
|
|
1113
|
+
|
|
1114
|
+
const currentTime = new Date().getTime();
|
|
1115
|
+
const tapDelay = currentTime - lastTapTime;
|
|
1116
|
+
|
|
1117
|
+
// 1. GESTIONE DOPPIO CLICK (Zoom Hercules)
|
|
1118
|
+
if (tapDelay < 300 && tapDelay > 0) {
|
|
1119
|
+
clearTimeout(tapTimeout);
|
|
1120
|
+
// Switch tra modalità scala Standard e Zoom Hercules
|
|
1121
|
+
graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard';
|
|
1122
|
+
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
1123
|
+
refreshGraph(type);
|
|
1124
|
+
lastTapTime = 0;
|
|
1125
|
+
}
|
|
1126
|
+
// 2. GESTIONE SINGOLO CLICK (Switch Dati o Esci dal Focus)
|
|
1127
|
+
else {
|
|
1128
|
+
lastTapTime = currentTime;
|
|
1129
|
+
tapTimeout = setTimeout(() => {
|
|
1130
|
+
// Se il box è in focus, il singolo click lo chiude
|
|
1131
|
+
if (isFocusActive && el.classList.contains('is-focused')) {
|
|
1132
|
+
toggleFocusMode(type, el);
|
|
1133
|
+
}
|
|
1134
|
+
// Se siamo in visualizzazione standard, gestiamo gli switch dati
|
|
1135
|
+
else if (!isFocusActive) {
|
|
1136
|
+
// Switch SOG <-> VMG
|
|
1137
|
+
if (type === 'sog') {
|
|
1138
|
+
displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
|
|
1139
|
+
}
|
|
1140
|
+
// Switch TWS <-> AWS (Nuova Logica)
|
|
1141
|
+
else if (type === 'tws') {
|
|
1142
|
+
displayModeTws = (displayModeTws === 'TWS') ? 'AWS' : 'TWS';
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Feedback visivo (lampeggio leggero) dell'avvenuto switch
|
|
1146
|
+
el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
|
1147
|
+
setTimeout(() => el.style.backgroundColor = "", 150);
|
|
1148
|
+
}
|
|
1149
|
+
}, 250);
|
|
1150
|
+
}
|
|
879
1151
|
});
|
|
1152
|
+
|
|
1153
|
+
// --- GESTIONE USCITA (Se l'utente trascina il dito fuori) ---
|
|
880
1154
|
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
881
1155
|
});
|
|
882
1156
|
|
package/index.html
CHANGED
|
@@ -168,7 +168,10 @@
|
|
|
168
168
|
</div>
|
|
169
169
|
|
|
170
170
|
<div class="data-box box-tws">
|
|
171
|
-
<div class="label-row"
|
|
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
|
}
|
|
@@ -127,10 +127,10 @@ module.exports = function (app) {
|
|
|
127
127
|
default: 0.5
|
|
128
128
|
},
|
|
129
129
|
stabilityThreshold: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
type: 'number',
|
|
131
|
+
title: 'Steering Precision (Sensitivity)',
|
|
132
|
+
description: "How strictly the system judges data coherence (0.0 to 1.0). Due to internal smoothing, 0.97-0.98 requires racing precision; 0.93-0.95 is ideal for cruising in waves. Below this, the display rarely alerts for instability.",
|
|
133
|
+
default: 0.95
|
|
134
134
|
},
|
|
135
135
|
stabilityBreakout: {
|
|
136
136
|
type: 'number',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sailingrotevista/rotevista-dash",
|
|
3
|
-
"version": "
|
|
4
|
-
"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"
|
package/settings.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"alarms": {
|
|
3
3
|
"depthDanger": 2.5,
|
|
4
|
-
"depthWarning": 5
|
|
4
|
+
"depthWarning": 3.5
|
|
5
5
|
},
|
|
6
6
|
"graphs": {
|
|
7
7
|
"reef1": 15.0,
|
|
8
8
|
"reef2": 20.0,
|
|
9
|
-
"historyMinutes":
|
|
9
|
+
"historyMinutes": 30
|
|
10
10
|
},
|
|
11
11
|
"averaging": {
|
|
12
12
|
"longWindow": 30000,
|
|
13
13
|
"smoothWindow": 2000,
|
|
14
14
|
"minSpeed": 0.5,
|
|
15
|
-
"stabilityThreshold": 0.
|
|
15
|
+
"stabilityThreshold": 0.95,
|
|
16
16
|
"stabilityBreakout": 15
|
|
17
17
|
},
|
|
18
18
|
"scales": {
|
package/style.css
CHANGED
|
@@ -175,7 +175,7 @@ body.night-mode .alarm-danger {
|
|
|
175
175
|
|
|
176
176
|
.focus-active .is-focused .sparkline path,
|
|
177
177
|
.focus-active .is-focused .sparkline line {
|
|
178
|
-
stroke-width: 1.5px !important;
|
|
178
|
+
stroke-width: 1.5px !important; /* Un briciolo più spessa per la stabilità visiva su schermi grandi */
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
.focus-active.focus-side-left {
|
|
@@ -441,10 +441,11 @@ body.night-mode {
|
|
|
441
441
|
|
|
442
442
|
/* --- 10.3 GRAFICI (STILE DINAMICO NIGHT MODE) --- */
|
|
443
443
|
.night-mode .graph-wrapper {
|
|
444
|
-
background: rgba(10, 0, 0, 0.8) !important;
|
|
444
|
+
background: rgba(10, 0, 0, 0.8) !important; /* Sfondo scurissimo per alto contrasto */
|
|
445
445
|
border: 1px solid #330000;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
/* Griglia temporale (minuti e livelli): quasi impercettibile */
|
|
448
449
|
.night-mode .sparkline line[stroke="rgba(0,0,0,0.12)"],
|
|
449
450
|
.night-mode .sparkline line[stroke="rgba(0,0,0,0.08)"] {
|
|
450
451
|
stroke: rgba(255, 0, 0, 0.1) !important; /* Griglia temporale rossa soffusa */
|