@sailingrotevista/rotevista-dash 7.0.19 → 7.0.23
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 +149 -64
- package/charts.js +36 -10
- package/index.html +90 -97
- package/index.js +16 -8
- package/package.json +1 -1
- package/style.css +0 -30
package/app.js
CHANGED
|
@@ -216,16 +216,25 @@ function checkDepthAlarm(m) {
|
|
|
216
216
|
function computeTrueWind() {
|
|
217
217
|
const aws = store.raw["environment.wind.speedApparent"], awa = store.raw["environment.wind.angleApparent"];
|
|
218
218
|
const stw = store.raw["navigation.speedThroughWater"], sog = store.raw["navigation.speedOverGround"] || 0;
|
|
219
|
-
const hdg = store.raw["navigation.headingTrue"] || 0
|
|
219
|
+
const hdg = store.raw["navigation.headingTrue"] || 0; // Il COG qui non serve più per il calcolo stabile
|
|
220
220
|
if (aws === undefined || awa === undefined) return;
|
|
221
221
|
|
|
222
|
-
// Usiamo il tempo esatto di arrivo del pacchetto del vento per la coerenza dei buffer
|
|
223
222
|
const now = store.timestamps["environment.wind.speedApparent"] || Date.now();
|
|
224
223
|
|
|
225
|
-
//
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
// RILEVAMENTO "LOG BLOCCATO" (Fouled Paddlewheel)
|
|
225
|
+
const hasFreshStw = store.timestamps["navigation.speedThroughWater"] && (now - store.timestamps["navigation.speedThroughWater"] < 15000);
|
|
226
|
+
let speedRef = 0;
|
|
227
|
+
|
|
228
|
+
if (hasFreshStw && stw !== undefined) {
|
|
229
|
+
// Se la barca naviga a GPS (> 1.5 nodi) ma l'elichetta legge quasi zero (< 0.5 nodi), il log è sporco/bloccato.
|
|
230
|
+
if (sog > 0.77 && stw < 0.25) {
|
|
231
|
+
speedRef = sog; // Forza il backup su SOG ignorando lo "0" fittizio del log
|
|
232
|
+
} else {
|
|
233
|
+
speedRef = stw;
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
speedRef = sog;
|
|
237
|
+
}
|
|
229
238
|
|
|
230
239
|
// ==========================================================================
|
|
231
240
|
// 1. GESTIONE TWS (NATIVO vs FALLBACK)
|
|
@@ -234,47 +243,51 @@ function computeTrueWind() {
|
|
|
234
243
|
let tws_water = 0;
|
|
235
244
|
|
|
236
245
|
if (hasNativeTws) {
|
|
237
|
-
// Se la barca invia il TWS nativo, usiamo direttamente quello
|
|
238
246
|
tws_water = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
239
247
|
} else {
|
|
240
|
-
// Altrimenti eseguiamo il calcolo vettoriale tattico utilizzando la velocità di riferimento (STW o SOG)
|
|
241
248
|
tws_water = Math.sqrt(aws * aws + speedRef * speedRef - 2 * aws * speedRef * Math.cos(awa));
|
|
242
249
|
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
243
250
|
}
|
|
244
251
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
+
let twa = 0;
|
|
253
|
+
// Verifica se riceviamo un TWA nativo valido ed efficiente negli ultimi 5 secondi
|
|
254
|
+
const hasNativeTwa = store.timestamps["environment.wind.angleTrueWater"] && (Date.now() - store.timestamps["environment.wind.angleTrueWater"] < 5000);
|
|
255
|
+
|
|
256
|
+
if (hasNativeTwa && store.raw["environment.wind.angleTrueWater"] !== undefined) {
|
|
257
|
+
// "Native First": diamo la precedenza assoluta al valore calcolato dalla centralina
|
|
258
|
+
twa = store.raw["environment.wind.angleTrueWater"];
|
|
259
|
+
} else {
|
|
260
|
+
// "Fallback Second": se manca il dato nativo, eseguiamo il calcolo vettoriale a mano
|
|
261
|
+
if (tws_water > 0.05) {
|
|
262
|
+
twa = Math.atan2(aws * Math.sin(awa), aws * Math.cos(awa) - speedRef);
|
|
263
|
+
store.raw["environment.wind.angleTrueWater"] = twa;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Salviamo e allineiamo i buffer delle medie se abbiamo un angolo valido (nativo o calcolato)
|
|
268
|
+
if (tws_water > 0.05 || hasNativeTwa) {
|
|
252
269
|
safePush(store.smoothBuf.twa, twa, now);
|
|
253
270
|
safePush(store.longBuf.twa, twa, now);
|
|
254
|
-
|
|
255
|
-
|
|
271
|
+
|
|
272
|
+
// Salviamo l'angolo apparente solo se il relativo sensore fisico è effettivamente attivo
|
|
273
|
+
if (awa !== undefined) {
|
|
274
|
+
safePush(store.smoothBuf.awa, awa, now);
|
|
275
|
+
safePush(store.longBuf.awa, awa, now);
|
|
276
|
+
}
|
|
256
277
|
}
|
|
257
278
|
|
|
258
279
|
// ==========================================================================
|
|
259
|
-
// 2. GESTIONE TWD (NATIVO vs FALLBACK)
|
|
280
|
+
// 2. GESTIONE TWD (NATIVO vs FALLBACK MATEMATICO STABILE)
|
|
260
281
|
// ==========================================================================
|
|
261
282
|
const hasNativeTwd = store.timestamps["environment.wind.directionTrue"] && (Date.now() - store.timestamps["environment.wind.directionTrue"] < 5000);
|
|
262
283
|
|
|
263
|
-
if (hasNativeTwd) {
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const tws_ground = Math.sqrt(tw_ground_x * tw_ground_x + tw_ground_y * tw_ground_y);
|
|
271
|
-
|
|
272
|
-
if (tws_ground > 0.05) {
|
|
273
|
-
let twd = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
|
|
274
|
-
store.raw["environment.wind.directionTrue"] = twd;
|
|
275
|
-
safePush(store.smoothBuf.twd, twd, now);
|
|
276
|
-
safePush(store.longBuf.twd, twd, now); // Alimenta la bussola meteo strategica!
|
|
277
|
-
}
|
|
284
|
+
if (!hasNativeTwd && tws_water > 0.05) {
|
|
285
|
+
// Calcolo TWD stabile e immune dal rollio: Prua + TWA
|
|
286
|
+
// Elimina i sobbalzi legati al brandeggio del COG e dello scarroccio fasullo
|
|
287
|
+
let twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
|
|
288
|
+
store.raw["environment.wind.directionTrue"] = twd;
|
|
289
|
+
safePush(store.smoothBuf.twd, twd, now);
|
|
290
|
+
safePush(store.longBuf.twd, twd, now);
|
|
278
291
|
}
|
|
279
292
|
}
|
|
280
293
|
|
|
@@ -387,6 +400,12 @@ function processIncomingData(path, val, source, timeMs) {
|
|
|
387
400
|
let speedVal = (val && typeof val === 'object' && val.val !== undefined) ? val.val : val;
|
|
388
401
|
store.raw["environment.wind.speedTrue"] = speedVal;
|
|
389
402
|
}
|
|
403
|
+
|
|
404
|
+
// INTERCETTAZIONE TWA NATIVO (Angolo Vento Reale pronto all'uso)
|
|
405
|
+
if (path === "environment.wind.angleTrueWater") {
|
|
406
|
+
let angleVal = (val && typeof val === 'object' && val.val !== undefined) ? val.val : val;
|
|
407
|
+
store.raw["environment.wind.angleTrueWater"] = angleVal;
|
|
408
|
+
}
|
|
390
409
|
|
|
391
410
|
// --- GESTIONE PRUA VERA / MAGNETICA CON AUTODIVIAZIONE ---
|
|
392
411
|
if (path === "navigation.headingTrue") {
|
|
@@ -665,7 +684,16 @@ function startDisplayLoop() {
|
|
|
665
684
|
|
|
666
685
|
// --- LOGICA SOG / VMG ---
|
|
667
686
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
668
|
-
|
|
687
|
+
// RILEVAMENTO "LOG BLOCCATO" PER IL CALCOLO VMG
|
|
688
|
+
const hasFreshStw = store.timestamps["navigation.speedThroughWater"] && (now - store.timestamps["navigation.speedThroughWater"] < 15000);
|
|
689
|
+
let speedRefKts = sogKts; // Fallback predefinito su GPS
|
|
690
|
+
|
|
691
|
+
if (hasFreshStw) {
|
|
692
|
+
// Se viaggiamo a più di 1.5 nodi (GPS) ma il log segna meno di 0.5 nodi, usa il SOG come backup
|
|
693
|
+
speedRefKts = (sogKts > 1.5 && stwKts < 0.5) ? sogKts : stwKts;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const vmgVal = Math.abs(speedRefKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
669
697
|
manageHistory('vmg', vmgVal);
|
|
670
698
|
manageHistory('sog', sogKts);
|
|
671
699
|
|
|
@@ -679,13 +707,22 @@ function startDisplayLoop() {
|
|
|
679
707
|
if (labelSogVmg) labelSogVmg.textContent = 'SOG';
|
|
680
708
|
|
|
681
709
|
if (isNavigating) {
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
710
|
+
const hasFreshStw = store.timestamps["navigation.speedThroughWater"] && (now - store.timestamps["navigation.speedThroughWater"] < 15000);
|
|
711
|
+
// Rileviamo se il log è plausibilmente sporco o bloccato (SOG in movimento, STW a zero)
|
|
712
|
+
const isFouledLog = (sogKts > 1.5 && stwKts < 0.5);
|
|
713
|
+
|
|
714
|
+
// Se non abbiamo un log affidabile, è impossibile calcolare la corrente. Evitiamo falsi colori tattici.
|
|
715
|
+
if (!hasFreshStw || isFouledLog) {
|
|
716
|
+
ui.sog.style.removeProperty('color');
|
|
717
|
+
} else {
|
|
718
|
+
const lastSog = store.histories.sog.length > 0 ? store.histories.sog[store.histories.sog.length - 1].val : sogKts;
|
|
719
|
+
const lastStw = store.histories.stw.length > 0 ? store.histories.stw[store.histories.stw.length - 1].val : stwKts;
|
|
720
|
+
const drift = lastSog - lastStw;
|
|
721
|
+
|
|
722
|
+
if (drift < -0.3) ui.sog.style.setProperty('color', '#ff3b30', 'important'); // Contro
|
|
723
|
+
else if (drift > 0.3) ui.sog.style.setProperty('color', '#00C851', 'important'); // Favore
|
|
724
|
+
else ui.sog.style.setProperty('color', '#ffbb33', 'important'); // Neutro SOG
|
|
725
|
+
}
|
|
689
726
|
} else {
|
|
690
727
|
ui.sog.style.removeProperty('color');
|
|
691
728
|
}
|
|
@@ -902,23 +939,74 @@ async function fetchServerHistory() {
|
|
|
902
939
|
const data = await response.json();
|
|
903
940
|
|
|
904
941
|
if (data && typeof data === 'object') {
|
|
942
|
+
// 1. RILEVAMENTO DEL DELTA DI COERENZA TEMPORALE
|
|
943
|
+
// Troviamo il timestamp più recente in assoluto presente nei dati storici del server
|
|
944
|
+
let maxServerTime = 0;
|
|
945
|
+
for (let key in data) {
|
|
946
|
+
if (store.histories[key] !== undefined && Array.isArray(data[key]) && data[key].length > 0) {
|
|
947
|
+
const lastPoint = data[key][data[key].length - 1];
|
|
948
|
+
if (lastPoint && lastPoint.time > maxServerTime) {
|
|
949
|
+
maxServerTime = lastPoint.time;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Calcoliamo lo sfasamento in millisecondi rispetto all'orologio del client (MacBook/Tablet)
|
|
955
|
+
const nowMac = Date.now();
|
|
956
|
+
const timeDelta = (maxServerTime > 0) ? (nowMac - maxServerTime) : 0;
|
|
957
|
+
|
|
958
|
+
if (Math.abs(timeDelta) > 500) {
|
|
959
|
+
console.log(`⏱️ [Clock Sync] Rilevato scostamento di ${(timeDelta/1000).toFixed(1)}s rispetto a einstein. Calibrazione attiva.`);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// 2. TRASLAZIONE DEI DATI STORICI
|
|
963
|
+
// Spostiamo nel tempo tutti i punti passati per incollarli millimetricamente al "now" del client
|
|
905
964
|
for (let key in data) {
|
|
906
965
|
if (store.histories[key] !== undefined) {
|
|
907
|
-
|
|
966
|
+
if (Array.isArray(data[key])) {
|
|
967
|
+
store.histories[key] = data[key].map(p => ({
|
|
968
|
+
val: p.val,
|
|
969
|
+
time: p.time + timeDelta, // Sposta il punto nel tempo per allinearlo al Mac
|
|
970
|
+
min: p.min !== undefined ? p.min : undefined,
|
|
971
|
+
max: p.max !== undefined ? p.max : undefined
|
|
972
|
+
}));
|
|
973
|
+
} else {
|
|
974
|
+
store.histories[key] = data[key];
|
|
975
|
+
}
|
|
908
976
|
}
|
|
909
977
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
if (data.
|
|
913
|
-
|
|
914
|
-
|
|
978
|
+
|
|
979
|
+
// Sincronizza i dati specifici del radar storici, previsionali e i buffer minuto per minuto (applicando il Delta)
|
|
980
|
+
if (data.windRadarSlots) {
|
|
981
|
+
store.windRadarSlots = data.windRadarSlots.map(s => ({
|
|
982
|
+
...s,
|
|
983
|
+
timestamp: s.timestamp + timeDelta
|
|
984
|
+
}));
|
|
985
|
+
}
|
|
986
|
+
if (data.futureForecast) {
|
|
987
|
+
store.futureForecast = {
|
|
988
|
+
...data.futureForecast,
|
|
989
|
+
timestamp: data.futureForecast.timestamp + timeDelta
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
if (data.twd) {
|
|
993
|
+
store.twdMinuteBuffer = data.twd.map(p => ({
|
|
994
|
+
...p,
|
|
995
|
+
time: p.time + timeDelta
|
|
996
|
+
}));
|
|
997
|
+
}
|
|
998
|
+
if (data.tws) {
|
|
999
|
+
store.twsMinuteBuffer = data.tws.map(p => ({
|
|
1000
|
+
...p,
|
|
1001
|
+
time: p.time + timeDelta
|
|
1002
|
+
}));
|
|
1003
|
+
}
|
|
915
1004
|
|
|
916
1005
|
// --- SILLABAZIONE STRATEGICA DELLA BUSSOLA METEO (TWD) ---
|
|
917
|
-
// Se il server ci invia lo storico del TWD, lo inseriamo calcolando i seni e coseni per i vettori
|
|
918
1006
|
if (data.twd && data.twd.length > 0) {
|
|
919
1007
|
store.longBuf.twd = data.twd.map(p => ({
|
|
920
1008
|
val: p.val,
|
|
921
|
-
time: p.time,
|
|
1009
|
+
time: p.time + timeDelta, // Allinea il tempo strategico del radar
|
|
922
1010
|
sin: Math.sin(p.val),
|
|
923
1011
|
cos: Math.cos(p.val)
|
|
924
1012
|
}));
|
|
@@ -928,7 +1016,7 @@ async function fetchServerHistory() {
|
|
|
928
1016
|
}
|
|
929
1017
|
} catch (err) {
|
|
930
1018
|
console.warn("⚠️ Impossibile caricare lo storico dal server. Utilizzo dati vuoti/simulati.");
|
|
931
|
-
throw err;
|
|
1019
|
+
throw err;
|
|
932
1020
|
}
|
|
933
1021
|
}
|
|
934
1022
|
|
|
@@ -1176,6 +1264,7 @@ function connect() {
|
|
|
1176
1264
|
{ path: "environment.wind.speedApparent", minPeriod: 333 }, // AWS a 3 Hz
|
|
1177
1265
|
{ path: "environment.wind.angleApparent", minPeriod: 333 }, // AWA a 3 Hz
|
|
1178
1266
|
{ path: "environment.wind.speedTrue", minPeriod: 333 }, // TWS (Nativo) a 3 Hz
|
|
1267
|
+
{ path: "environment.wind.angleTrueWater", minPeriod: 333 }, // TWA (Nativo) a 3 Hz (AGGIUNTO)
|
|
1179
1268
|
{ path: "environment.wind.directionTrue", minPeriod: 333 } // TWD (Nativo) a 3 Hz
|
|
1180
1269
|
]
|
|
1181
1270
|
};
|
|
@@ -1264,14 +1353,11 @@ async function init() {
|
|
|
1264
1353
|
if (activeInstrument === 'gauge') {
|
|
1265
1354
|
twdPressTimer = setTimeout(() => {
|
|
1266
1355
|
activeInstrument = 'radar';
|
|
1267
|
-
|
|
1268
|
-
// Spegne l'INTERO contenitore multilivello della bussola analogica
|
|
1269
|
-
document.querySelector('.wind-gauge-container').style.display = 'none';
|
|
1356
|
+
document.getElementById('wind-gauge').style.display = 'none';
|
|
1270
1357
|
document.getElementById('wind-radar').style.display = 'block';
|
|
1271
|
-
|
|
1272
1358
|
renderRadar(); // Disegna immediatamente il radar all'attivazione
|
|
1273
1359
|
twdPressTimer = null;
|
|
1274
|
-
longPressTriggered = true;
|
|
1360
|
+
longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
|
|
1275
1361
|
}, 1000);
|
|
1276
1362
|
}
|
|
1277
1363
|
});
|
|
@@ -1283,13 +1369,13 @@ async function init() {
|
|
|
1283
1369
|
}
|
|
1284
1370
|
} else if (activeInstrument === 'radar') {
|
|
1285
1371
|
if (longPressTriggered) {
|
|
1372
|
+
// Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
|
|
1286
1373
|
longPressTriggered = false;
|
|
1287
1374
|
} else {
|
|
1375
|
+
// Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
|
|
1288
1376
|
activeInstrument = 'gauge';
|
|
1289
1377
|
document.getElementById('wind-radar').style.display = 'none';
|
|
1290
|
-
|
|
1291
|
-
// Riaccende l'intero contenitore multilivello della bussola analogica
|
|
1292
|
-
document.querySelector('.wind-gauge-container').style.display = 'flex';
|
|
1378
|
+
document.getElementById('wind-gauge').style.display = 'block';
|
|
1293
1379
|
}
|
|
1294
1380
|
}
|
|
1295
1381
|
});
|
|
@@ -1301,7 +1387,7 @@ async function init() {
|
|
|
1301
1387
|
longPressTriggered = false;
|
|
1302
1388
|
});
|
|
1303
1389
|
}
|
|
1304
|
-
|
|
1390
|
+
|
|
1305
1391
|
// 2. COMANDO TATTICO: Click in qualsiasi punto del radar per tornare all'analogico
|
|
1306
1392
|
const windRadarSvg = document.getElementById('wind-radar');
|
|
1307
1393
|
if (windRadarSvg) {
|
|
@@ -1309,9 +1395,7 @@ async function init() {
|
|
|
1309
1395
|
if (activeInstrument === 'radar') {
|
|
1310
1396
|
activeInstrument = 'gauge';
|
|
1311
1397
|
windRadarSvg.style.display = 'none';
|
|
1312
|
-
|
|
1313
|
-
// Riaccende l'intero contenitore multilivello della bussola analogica
|
|
1314
|
-
document.querySelector('.wind-gauge-container').style.display = 'flex';
|
|
1398
|
+
document.getElementById('wind-gauge').style.display = 'block';
|
|
1315
1399
|
}
|
|
1316
1400
|
});
|
|
1317
1401
|
}
|
|
@@ -1336,11 +1420,12 @@ async function init() {
|
|
|
1336
1420
|
startDisplayLoop();
|
|
1337
1421
|
connect(); // Si collegherà in tempo reale al WebSocket reale della barca
|
|
1338
1422
|
|
|
1339
|
-
// 5. POLL LENTO (15 secondi): aggiorna i dati radar in background
|
|
1423
|
+
// 5. POLL LENTO (15 secondi): aggiorna i dati radar in background SOLO se lo strumento attivo è il radar,
|
|
1424
|
+
// evitando di sovraccaricare la rete ed eliminando il salto visivo di reset dei grafici a 1Hz
|
|
1340
1425
|
setInterval(async () => {
|
|
1341
1426
|
try {
|
|
1342
|
-
await fetchServerHistory();
|
|
1343
1427
|
if (activeInstrument === 'radar') {
|
|
1428
|
+
await fetchServerHistory();
|
|
1344
1429
|
renderRadar();
|
|
1345
1430
|
}
|
|
1346
1431
|
} catch (err) {
|
package/charts.js
CHANGED
|
@@ -19,7 +19,9 @@ function calculateScale(type, data, mode) {
|
|
|
19
19
|
|
|
20
20
|
if (!store.herculesScales) store.herculesScales = {};
|
|
21
21
|
if (!store.herculesScales[type]) {
|
|
22
|
-
|
|
22
|
+
// Inizializzazione a NULL dello stato di partenza per consentire un allineamento istantaneo
|
|
23
|
+
// e perfettamente centrato al primo doppio clic
|
|
24
|
+
store.herculesScales[type] = { min: null, max: null };
|
|
23
25
|
}
|
|
24
26
|
let currentScale = store.herculesScales[type];
|
|
25
27
|
|
|
@@ -65,9 +67,10 @@ function calculateScale(type, data, mode) {
|
|
|
65
67
|
targetMax = targetMin + 4;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
currentScale.
|
|
70
|
+
// Gestione dell'allineamento iniziale o dello scostamento dei limiti della scala
|
|
71
|
+
if (currentScale.min === null || currentVal < currentScale.min || currentVal > currentScale.max) {
|
|
72
|
+
currentScale.min = currentScale.min === null ? targetMin : Math.min(currentScale.min, targetMin);
|
|
73
|
+
currentScale.max = currentScale.max === null ? targetMax : Math.max(currentScale.max, targetMax);
|
|
71
74
|
} else {
|
|
72
75
|
const allStableInTarget = recentVals.every(val => val >= targetMin && val <= targetMax);
|
|
73
76
|
if (allStableInTarget) {
|
|
@@ -90,8 +93,25 @@ function calculateScale(type, data, mode) {
|
|
|
90
93
|
const oneThirdCount = Math.max(1, Math.floor(data.length / 3));
|
|
91
94
|
const recentThirdData = data.slice(-oneThirdCount);
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
let localMin = Math.min(...recentThirdData);
|
|
97
|
+
let localMax = Math.max(...recentThirdData);
|
|
98
|
+
|
|
99
|
+
// SMART MOTORING DETECTION (Specifica per i grafici di velocità: STW, SOG, VMG)
|
|
100
|
+
const isSpeedType = (type === 'stw' || type === 'sog' || type === 'vmg');
|
|
101
|
+
if (isSpeedType && data.length > oneThirdCount) {
|
|
102
|
+
const olderData = data.slice(0, data.length - oneThirdCount);
|
|
103
|
+
const olderMin = Math.min(...olderData);
|
|
104
|
+
const olderMax = Math.max(...olderData);
|
|
105
|
+
const olderRange = olderMax - olderMin; // Variazione nello storico passato
|
|
106
|
+
|
|
107
|
+
// Se lo storico passato non era piatto (range > 0.2 nodi) oppure eravamo fermi/all'ancora (< 1.5 nodi),
|
|
108
|
+
// significa che il passato è rilevante per il trend. Includiamo il passato nel calcolo della scala Y.
|
|
109
|
+
// Se invece era piatto (motore), lo ignoriamo per zoomare subito sulla vela attuale.
|
|
110
|
+
if (olderRange > 0.2 || olderMin < 0.77) {
|
|
111
|
+
localMin = Math.min(localMin, olderMin);
|
|
112
|
+
localMax = Math.max(localMax, olderMax);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
95
115
|
|
|
96
116
|
let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
|
|
97
117
|
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
@@ -100,9 +120,10 @@ function calculateScale(type, data, mode) {
|
|
|
100
120
|
targetMax = targetMin + roundStep;
|
|
101
121
|
}
|
|
102
122
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
currentScale.
|
|
123
|
+
// Gestione dell'allineamento iniziale o dello scostamento dei limiti della scala
|
|
124
|
+
if (currentScale.min === null || currentVal < currentScale.min || currentVal > currentScale.max) {
|
|
125
|
+
currentScale.min = currentScale.min === null ? targetMin : Math.min(currentScale.min, targetMin);
|
|
126
|
+
currentScale.max = currentScale.max === null ? targetMax : Math.max(currentScale.max, targetMax);
|
|
106
127
|
} else {
|
|
107
128
|
const allStableInTarget = recentThirdData.every(val => val >= targetMin && val <= targetMax);
|
|
108
129
|
if (allStableInTarget) {
|
|
@@ -256,7 +277,12 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
256
277
|
gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
257
278
|
gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
258
279
|
|
|
259
|
-
|
|
280
|
+
// CALCOLO ELASTICO DELL'INTERRUZIONE: Tollera buchi storici fino a 90 secondi (es. i 30s di einstein)
|
|
281
|
+
// per permettere di disegnare linee solide e continue anche se mostrate in una timeline corta e veloce
|
|
282
|
+
const maxAllowedGap = Math.max(expectedInterval * 2.5, 90000);
|
|
283
|
+
const isHistoryGap = deltaTime > maxAllowedGap;
|
|
284
|
+
|
|
285
|
+
if (isHistoryGap) {
|
|
260
286
|
if (started) {
|
|
261
287
|
areaPath += `L ${x1} ${h} `;
|
|
262
288
|
started = false;
|
package/index.html
CHANGED
|
@@ -67,108 +67,101 @@
|
|
|
67
67
|
<!-- STATO CONNESSIONE -->
|
|
68
68
|
<div id="status" class="offline">OFFLINE</div>
|
|
69
69
|
|
|
70
|
-
<!-- BUSSOLA 1: ANALOGICA STANDARD (
|
|
71
|
-
<
|
|
70
|
+
<!-- BUSSOLA 1: ANALOGICA STANDARD (Visibile all'avvio) -->
|
|
71
|
+
<svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
|
|
72
|
+
<defs>
|
|
73
|
+
<clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
74
|
+
<linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
75
|
+
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
|
76
|
+
<stop offset="100%" style="stop-color:#888888;stop-opacity:1" />
|
|
77
|
+
</linearGradient>
|
|
78
|
+
<linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
79
|
+
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
80
|
+
<stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" />
|
|
81
|
+
<stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" />
|
|
82
|
+
<stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" />
|
|
83
|
+
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
84
|
+
</linearGradient>
|
|
85
|
+
<linearGradient id="leeway-night-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
86
|
+
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
87
|
+
<stop offset="50%" style="stop-color:#330000;stop-opacity:1" />
|
|
88
|
+
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
89
|
+
</linearGradient>
|
|
90
|
+
<clipPath id="leeway-clip">
|
|
91
|
+
<rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" />
|
|
92
|
+
</clipPath>
|
|
93
|
+
<filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
94
|
+
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
95
|
+
</filter>
|
|
96
|
+
</defs>
|
|
72
97
|
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
<g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
|
|
111
|
-
<text font-size="16" transform="translate(200, 74)">0</text>
|
|
112
|
-
<text font-size="16" transform="translate(326, 200) rotate(90)">90</text>
|
|
113
|
-
<text font-size="16" transform="translate(74, 200) rotate(-90)">90</text>
|
|
114
|
-
<text font-size="16" transform="translate(200, 326) rotate(180)">180</text>
|
|
115
|
-
<text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
|
|
116
|
-
<text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
|
|
117
|
-
<text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
|
|
118
|
-
<text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
|
|
119
|
-
<text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
|
|
120
|
-
<text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
|
|
121
|
-
<text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
|
|
122
|
-
<text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
|
|
123
|
-
</g>
|
|
124
|
-
|
|
125
|
-
<circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
|
|
126
|
-
|
|
127
|
-
<path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
|
|
128
|
-
fill="url(#axiom-grad)" transform="translate(0, 5)" clip-path="url(#boat-clip)" style="pointer-events: none;" />
|
|
129
|
-
|
|
130
|
-
<g id="aws-display-group" transform="translate(200, 265)">
|
|
131
|
-
<text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
|
|
132
|
-
<text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
|
|
133
|
-
</g>
|
|
134
|
-
|
|
135
|
-
<g transform="translate(75, 395)">
|
|
136
|
-
<text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0°</text>
|
|
137
|
-
<rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
|
|
138
|
-
<rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
|
|
139
|
-
<g stroke="#555" stroke-width="1">
|
|
140
|
-
<line x1="0" y1="-2" x2="0" y2="14" /><line x1="62.5" y1="2" x2="62.5" y2="10" />
|
|
141
|
-
<line x1="125" y1="-2" x2="125" y2="14" /><line x1="187.5" y1="2" x2="187.5" y2="10" />
|
|
142
|
-
<line x1="250" y1="-2" x2="250" y2="14" />
|
|
143
|
-
</g>
|
|
144
|
-
<g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
|
|
145
|
-
<text x="0" y="24">-20°</text><text x="62.5" y="24">-10</text>
|
|
146
|
-
<text x="125" y="24">0°</text><text x="187.5" y="24">10</text>
|
|
147
|
-
<text x="250" y="24">20°</text>
|
|
148
|
-
</g>
|
|
149
|
-
</g>
|
|
150
|
-
</svg>
|
|
98
|
+
<circle cx="200" cy="200" r="160" fill="#050505" />
|
|
99
|
+
<circle cx="200" cy="200" r="125" fill="#121212" />
|
|
100
|
+
|
|
101
|
+
<path d="M 82.0 101.0 A 154 154 0 0 1 142.3 57.2" fill="none" stroke="#ff0000" stroke-width="12" />
|
|
102
|
+
<path d="M 257.7 57.2 A 154 154 0 0 1 318.0 101.0" fill="none" stroke="#00ff00" stroke-width="12" />
|
|
103
|
+
<path d="M 265.1 339.6 A 154 154 0 0 1 134.9 339.6" fill="none" stroke="#ff8800" stroke-width="12" />
|
|
104
|
+
|
|
105
|
+
<g id="ticks"></g>
|
|
106
|
+
|
|
107
|
+
<g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
|
|
108
|
+
<text font-size="16" transform="translate(200, 74)">0</text>
|
|
109
|
+
<text font-size="16" transform="translate(326, 200) rotate(90)">90</text>
|
|
110
|
+
<text font-size="16" transform="translate(74, 200) rotate(-90)">90</text>
|
|
111
|
+
<text font-size="16" transform="translate(200, 326) rotate(180)">180</text>
|
|
112
|
+
<text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
|
|
113
|
+
<text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
|
|
114
|
+
<text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
|
|
115
|
+
<text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
|
|
116
|
+
<text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
|
|
117
|
+
<text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
|
|
118
|
+
<text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
|
|
119
|
+
<text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
|
|
120
|
+
</g>
|
|
121
|
+
|
|
122
|
+
<g id="track-pointer" transform="rotate(0, 200, 200)">
|
|
123
|
+
<path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" />
|
|
124
|
+
</g>
|
|
125
|
+
|
|
126
|
+
<circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
|
|
127
|
+
|
|
128
|
+
<path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
|
|
129
|
+
fill="url(#axiom-grad)" transform="translate(0, 5)" clip-path="url(#boat-clip)" style="pointer-events: none;" />
|
|
130
|
+
|
|
131
|
+
<g id="aws-display-group" transform="translate(200, 265)">
|
|
132
|
+
<text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
|
|
133
|
+
<text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
|
|
134
|
+
</g>
|
|
151
135
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
</g>
|
|
136
|
+
<g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
|
|
137
|
+
<path d="M 200,70 L 213,95 L 200,145 L 187,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" />
|
|
138
|
+
<text x="200" y="90" fill="#000" font-size="10" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
|
|
139
|
+
</g>
|
|
157
140
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
141
|
+
<g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
|
|
142
|
+
<path d="M 200,92 A 8,8 0 0 1 208,100 C 208,108 200,128 200,128 C 200,128 192,108 192,100 A 8,8 0 0 1 200,92 Z"
|
|
143
|
+
fill="#ffff00" stroke="#000" stroke-width="0.8" />
|
|
144
|
+
<text x="200" y="106" fill="#000" font-size="9" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
|
|
145
|
+
<circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#bbb" />
|
|
146
|
+
<circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#bbb" />
|
|
147
|
+
</g>
|
|
148
|
+
|
|
149
|
+
<g transform="translate(75, 395)">
|
|
150
|
+
<text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0°</text>
|
|
151
|
+
<rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
|
|
152
|
+
<rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
|
|
153
|
+
<g stroke="#555" stroke-width="1">
|
|
154
|
+
<line x1="0" y1="-2" x2="0" y2="14" /><line x1="62.5" y1="2" x2="62.5" y2="10" />
|
|
155
|
+
<line x1="125" y1="-2" x2="125" y2="14" /><line x1="187.5" y1="2" x2="187.5" y2="10" />
|
|
156
|
+
<line x1="250" y1="-2" x2="250" y2="14" />
|
|
161
157
|
</g>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
<text x="200" y="106" fill="#000" font-size="9" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
|
|
167
|
-
<circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#bbb" />
|
|
168
|
-
<circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#bbb" />
|
|
158
|
+
<g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
|
|
159
|
+
<text x="0" y="24">-20°</text><text x="62.5" y="24">-10</text>
|
|
160
|
+
<text x="125" y="24">0°</text><text x="187.5" y="24">10</text>
|
|
161
|
+
<text x="250" y="24">20°</text>
|
|
169
162
|
</g>
|
|
170
|
-
</
|
|
171
|
-
</
|
|
163
|
+
</g>
|
|
164
|
+
</svg>
|
|
172
165
|
|
|
173
166
|
<!-- BUSSOLA 2: RADAR HISTORICAL (Nascosto all'avvio) -->
|
|
174
167
|
<svg id="wind-radar" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet" style="display: none;">
|
package/index.js
CHANGED
|
@@ -277,32 +277,40 @@ module.exports = function (app) {
|
|
|
277
277
|
const stw = raw["navigation.speedThroughWater"];
|
|
278
278
|
const sog = raw["navigation.speedOverGround"] || 0;
|
|
279
279
|
const hdg = raw["navigation.headingTrue"];
|
|
280
|
-
const cog = raw["navigation.courseOverGroundTrue"] || 0;
|
|
281
280
|
|
|
282
281
|
if (aws !== undefined && awa !== undefined) {
|
|
283
282
|
const awsKts = aws * 1.94384;
|
|
284
283
|
|
|
285
|
-
//
|
|
284
|
+
// RILEVAMENTO "LOG BLOCCATO" SUL SERVER
|
|
286
285
|
const hasStw = lastPathProcessTimes["navigation.speedThroughWater"] && (now - lastPathProcessTimes["navigation.speedThroughWater"] < 15000);
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
let speedRef = 0;
|
|
287
|
+
|
|
288
|
+
if (hasStw && stw !== undefined) {
|
|
289
|
+
if (sog > 0.77 && stw < 0.25) {
|
|
290
|
+
speedRef = sog; // Log sporco: forza il SOG
|
|
291
|
+
} else {
|
|
292
|
+
speedRef = stw;
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
speedRef = sog;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const speedKtsRef = speedRef * 1.94384;
|
|
289
299
|
|
|
290
300
|
const tw_water_x = awsKts * Math.cos(awa) - speedKtsRef;
|
|
291
301
|
const tw_water_y = awsKts * Math.sin(awa);
|
|
292
302
|
|
|
293
|
-
//
|
|
303
|
+
// Calcolo TWS
|
|
294
304
|
if (now - lastNativeTwsTime > 5000) {
|
|
295
305
|
const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
296
306
|
manageHistory('tws', tws);
|
|
297
307
|
}
|
|
298
308
|
|
|
299
309
|
const twa = Math.atan2(tw_water_y, tw_water_x);
|
|
300
|
-
|
|
301
|
-
// La VMG viene calcolata usando la velocità di riferimento (STW reale o SOG di fallback)
|
|
302
310
|
const vmg = Math.abs(speedKtsRef * Math.cos(twa));
|
|
303
311
|
manageHistory('vmg', vmg);
|
|
304
312
|
|
|
305
|
-
//
|
|
313
|
+
// Calcolo TWD stabile (Prua + TWA)
|
|
306
314
|
if (hdg !== undefined && (now - lastNativeTwdTime > 5000)) {
|
|
307
315
|
const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
|
|
308
316
|
manageHistory('twd', twd);
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -602,36 +602,6 @@ body.night-mode {
|
|
|
602
602
|
stroke: #440000 !important;
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
-
/* ==========================================================================
|
|
606
|
-
13. ARCHITETTURA MULTILIVELLO GPU (60FPS EXTRA LOW-POWER NEEDLES)
|
|
607
|
-
========================================================================== */
|
|
608
|
-
|
|
609
|
-
/* Contenitore di posizionamento assoluto speculare per i due livelli */
|
|
610
|
-
.wind-gauge-container {
|
|
611
|
-
position: relative;
|
|
612
|
-
width: 100%;
|
|
613
|
-
height: 100%;
|
|
614
|
-
max-height: 100%;
|
|
615
|
-
display: flex;
|
|
616
|
-
align-items: center;
|
|
617
|
-
justify-content: center;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/* Forziamo l'overlay delle sole lancette ad allinearsi perfettamente sopra lo sfondo */
|
|
621
|
-
#wind-gauge-pointers {
|
|
622
|
-
position: absolute;
|
|
623
|
-
top: 0; left: 0;
|
|
624
|
-
width: 100%; height: 100%;
|
|
625
|
-
object-fit: contain;
|
|
626
|
-
pointer-events: none; /* Cruciale: permette ai clic del mouse/tocco di passare sotto su #fullscreen-hotspot */
|
|
627
|
-
will-change: transform; /* Promuove l'intero overlay a livello GPU composited */
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/* Applichiamo lo smorzamento fluido a 60fps solo alle frecce ultra-leggere */
|
|
631
|
-
#awa-pointer, #twa-pointer, #track-pointer {
|
|
632
|
-
transition: transform 0.9s cubic-bezier(0.15, 0.85, 0.3, 1);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
605
|
/* ==========================================================================
|
|
636
606
|
13. SMORZAMENTO FLUIDO AGHI A 60FPS (DAMPING HARDWARE) non utilizzato consuma troppa cpu
|
|
637
607
|
==========================================================================
|