@sailingrotevista/rotevista-dash 4.0.18 → 4.0.20
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 +239 -86
- package/package.json +1 -1
- package/style.css +6 -0
package/app.js
CHANGED
|
@@ -17,7 +17,7 @@ let CONFIG = {
|
|
|
17
17
|
smoothWindow: 2000,
|
|
18
18
|
longWindow: 30000,
|
|
19
19
|
stabilityTolerance: 2000,
|
|
20
|
-
stabilityThreshold: 0.
|
|
20
|
+
stabilityThreshold: 0.9,
|
|
21
21
|
minSpeed: 0,
|
|
22
22
|
stabilityBreakout: 15
|
|
23
23
|
},
|
|
@@ -35,6 +35,8 @@ const RENDER_INTERVAL_MS = 1000;
|
|
|
35
35
|
const TIMEOUT_MS = 5000;
|
|
36
36
|
const SIM_SAMPLE_INTERVAL = 1000;
|
|
37
37
|
const DASH_VERSION = "2.4"; // Versione per la gestione della memoria locale
|
|
38
|
+
const sourceLocks = {};
|
|
39
|
+
|
|
38
40
|
|
|
39
41
|
// ==========================================================================
|
|
40
42
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
@@ -53,6 +55,10 @@ let twDirty = false, isNavigating = false, reconnectDelay = 1000;
|
|
|
53
55
|
|
|
54
56
|
let pressTimer, isFocusActive = false;
|
|
55
57
|
|
|
58
|
+
let emaTwaSin = 0;
|
|
59
|
+
let emaTwaCos = 0;
|
|
60
|
+
let firstEmaRun = true;
|
|
61
|
+
|
|
56
62
|
// Stato dei singoli grafici (Standard vs Hercules Zoom)
|
|
57
63
|
const graphModes = {
|
|
58
64
|
stw: 'standard',
|
|
@@ -110,39 +116,87 @@ function getShortestRotation(curr, target) {
|
|
|
110
116
|
/**
|
|
111
117
|
* Inserisce un dato nel buffer circolare limitandolo a 2000 campioni (30 min)
|
|
112
118
|
*/
|
|
113
|
-
function safePush(buffer, val, time, maxLen = 2000) {
|
|
114
|
-
buffer.push({ val: val, time: time });
|
|
115
|
-
if (buffer.length > maxLen) { buffer.shift(); }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
119
|
/**
|
|
119
|
-
*
|
|
120
|
+
* Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
|
|
120
121
|
*/
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
validData.forEach(item => {
|
|
128
|
-
sSin += Math.sin(item.val);
|
|
129
|
-
sCos += Math.cos(item.val);
|
|
122
|
+
function safePush(buffer, val, time) {
|
|
123
|
+
buffer.push({
|
|
124
|
+
val: val,
|
|
125
|
+
time: time,
|
|
126
|
+
sin: Math.sin(val),
|
|
127
|
+
cos: Math.cos(val)
|
|
130
128
|
});
|
|
131
|
-
|
|
132
|
-
let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
|
|
133
|
-
let historyDuration = (validData.length > 2) ? (validData[validData.length - 1].time - validData[0].time) : 0;
|
|
134
|
-
|
|
135
|
-
// Un dato è stabile se abbiamo abbastanza storia e la coerenza vettoriale R è alta
|
|
136
|
-
let isStable = (historyDuration > 10000) && (R > CONFIG.averaging.stabilityThreshold);
|
|
137
|
-
let avgRad = Math.atan2(sSin, sCos);
|
|
138
129
|
|
|
139
|
-
//
|
|
140
|
-
|
|
130
|
+
// Teniamo sempre in memoria il DOPPIO della storia impostata (per la modalità ancoraggio)
|
|
131
|
+
// + 1 minuto di margine
|
|
132
|
+
const maxHistoryMs = (CONFIG.graphs.historyMinutes * 60000 * 2) + 60000;
|
|
133
|
+
|
|
134
|
+
while (buffer.length > 0 && (time - buffer[0].time) > maxHistoryMs) {
|
|
135
|
+
buffer.shift();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Tetto massimo di campioni per sicurezza (circa 2 ore a 5Hz)
|
|
139
|
+
if (buffer.length > 36000) buffer.shift();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
|
|
143
|
+
now = now || Date.now();
|
|
144
|
+
|
|
145
|
+
const len = bufferArray.length;
|
|
146
|
+
if (len === 0) return null;
|
|
147
|
+
|
|
148
|
+
let sSin = 0;
|
|
149
|
+
let sCos = 0;
|
|
150
|
+
let count = 0;
|
|
151
|
+
|
|
152
|
+
let newestTime = 0;
|
|
153
|
+
let oldestTime = 0;
|
|
154
|
+
|
|
155
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
156
|
+
const item = bufferArray[i];
|
|
157
|
+
|
|
158
|
+
if ((now - item.time) > windowMs) break;
|
|
159
|
+
|
|
160
|
+
sSin += item.sin;
|
|
161
|
+
sCos += item.cos;
|
|
162
|
+
|
|
163
|
+
if (count === 0) newestTime = item.time;
|
|
164
|
+
oldestTime = item.time;
|
|
165
|
+
|
|
166
|
+
count++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (count === 0) return null;
|
|
170
|
+
|
|
171
|
+
const R = Math.hypot(sSin, sCos) / count;
|
|
172
|
+
|
|
173
|
+
const avgRad = Math.atan2(sSin, sCos);
|
|
174
|
+
|
|
175
|
+
const finalVal = signed
|
|
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
|
+
|
|
188
|
+
const safeR = Math.max(R, 1e-9);
|
|
141
189
|
|
|
142
190
|
return {
|
|
143
|
-
val:
|
|
191
|
+
val: finalVal,
|
|
144
192
|
stable: isStable,
|
|
145
|
-
dev:
|
|
193
|
+
dev: (R < 1)
|
|
194
|
+
? Math.round(
|
|
195
|
+
Math.sqrt(-2 * Math.log(safeR)) *
|
|
196
|
+
(180 / Math.PI)
|
|
197
|
+
)
|
|
198
|
+
: 0,
|
|
199
|
+
samples: count
|
|
146
200
|
};
|
|
147
201
|
}
|
|
148
202
|
|
|
@@ -291,14 +345,58 @@ function computeTrueWind() {
|
|
|
291
345
|
}
|
|
292
346
|
}
|
|
293
347
|
|
|
294
|
-
function processIncomingData(path, val) {
|
|
295
|
-
const now = Date.now();
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
348
|
+
function processIncomingData(path, val, source) {
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
|
|
351
|
+
// --- 1. FILTRO SORGENTE STICKY (Elimina conflitti yacht_device vs Unknown) ---
|
|
352
|
+
if (!sourceLocks[path] || sourceLocks[path].label === source || (now - sourceLocks[path].lastSeen > 2000)) {
|
|
353
|
+
sourceLocks[path] = { label: source, lastSeen: now };
|
|
354
|
+
} else {
|
|
355
|
+
// Se arriva un dato da un'altra sorgente mentre il lock è attivo, lo scartiamo
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// --- 2. AGGIORNAMENTO DATI (Solo per la sorgente eletta) ---
|
|
360
|
+
store.timestamps[path] = now;
|
|
361
|
+
store.raw[path] = val;
|
|
362
|
+
|
|
363
|
+
// Buffer per AWA (Vento Apparente)
|
|
364
|
+
if (path === "environment.wind.angleApparent") {
|
|
365
|
+
safePush(store.smoothBuf.awa, val, now);
|
|
366
|
+
safePush(store.longBuf.awa, val, now);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Buffer per HDG (Prua)
|
|
370
|
+
if (path === "navigation.headingTrue") {
|
|
371
|
+
safePush(store.smoothBuf.hdg, val, now);
|
|
372
|
+
safePush(store.longBuf.hdg, val, now);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Buffer per COG (Rotta Fondo)
|
|
376
|
+
if (path === "navigation.courseOverGroundTrue") {
|
|
377
|
+
safePush(store.smoothBuf.cog, val, now);
|
|
378
|
+
safePush(store.longBuf.cog, val, now);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// --- 3. TRIGGER CALCOLO VENTO REALE (TWA/TWS/TWD) ---
|
|
382
|
+
const twPaths = [
|
|
383
|
+
"environment.wind.speedApparent",
|
|
384
|
+
"environment.wind.angleApparent",
|
|
385
|
+
"navigation.speedThroughWater",
|
|
386
|
+
"navigation.speedOverGround",
|
|
387
|
+
"navigation.headingTrue",
|
|
388
|
+
"navigation.courseOverGroundTrue"
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
if (twPaths.includes(path)) {
|
|
392
|
+
twDirty = true;
|
|
393
|
+
// Calcolo limitato a 10Hz per non pesare sulla CPU
|
|
394
|
+
if (twDirty && (now - lastTWCompute > 100)) {
|
|
395
|
+
computeTrueWind();
|
|
396
|
+
lastTWCompute = now;
|
|
397
|
+
twDirty = false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
302
400
|
}
|
|
303
401
|
|
|
304
402
|
// ==========================================================================
|
|
@@ -312,8 +410,9 @@ function updateWindTrend() {
|
|
|
312
410
|
|
|
313
411
|
// STRATEGIA (Bussola TWD): 1 min vs 30 minuti (tendenza meteo profonda)
|
|
314
412
|
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
315
|
-
const
|
|
316
|
-
|
|
413
|
+
const multiplier = isNavigating ? 1 : 2;
|
|
414
|
+
const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
|
|
415
|
+
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
317
416
|
if (!twaNow || !twaRef || !twdNow || !twdRef) return;
|
|
318
417
|
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
319
418
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
@@ -357,12 +456,41 @@ function updateWindTrend() {
|
|
|
357
456
|
}
|
|
358
457
|
} else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
359
458
|
|
|
360
|
-
// ALLARME STRAMBATA (GYBE)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
459
|
+
// --- LOGICA ALLARME STRAMBATA (GYBE) FILTRATA ---
|
|
460
|
+
const instTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
461
|
+
|
|
462
|
+
if (instTwaRad !== undefined) {
|
|
463
|
+
// Calcolo Alfa basato sulla Steering Precision (più precisione = filtro più lento e stabile)
|
|
464
|
+
const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
|
|
465
|
+
|
|
466
|
+
const currentSin = Math.sin(instTwaRad);
|
|
467
|
+
const currentCos = Math.cos(instTwaRad);
|
|
468
|
+
|
|
469
|
+
if (firstEmaRun) {
|
|
470
|
+
emaTwaSin = currentSin;
|
|
471
|
+
emaTwaCos = currentCos;
|
|
472
|
+
firstEmaRun = false;
|
|
473
|
+
} else {
|
|
474
|
+
// Media Esponenziale Vettoriale
|
|
475
|
+
emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
|
|
476
|
+
emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Angolo risultante filtrato
|
|
480
|
+
const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
|
|
481
|
+
|
|
482
|
+
// Verifichiamo il cambio di mure (> 155° e inversione di segno confermata dal filtro)
|
|
483
|
+
if (Math.abs(smoothedTwaDeg) > 155) {
|
|
484
|
+
if (Math.sign(smoothedTwaDeg) !== Math.sign(lastInstantTwa) && lastInstantTwa !== null) {
|
|
485
|
+
if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
|
|
486
|
+
lastGybeAlarmTime = now;
|
|
487
|
+
playGybeAlarm();
|
|
488
|
+
console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
lastInstantTwa = smoothedTwaDeg;
|
|
493
|
+
}
|
|
366
494
|
}
|
|
367
495
|
|
|
368
496
|
// ==========================================================================
|
|
@@ -390,7 +518,7 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
390
518
|
} else {
|
|
391
519
|
let valDeg = Math.round(radToDeg(obj.val));
|
|
392
520
|
let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
393
|
-
let dev = (obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.
|
|
521
|
+
let dev = (obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.8em; opacity: 0.4; margin-left: 6px;">±${obj.dev}</span>` : "";
|
|
394
522
|
el.innerHTML = mainVal + dev;
|
|
395
523
|
|
|
396
524
|
let diff = Math.abs((radToDeg(instantRaw) - radToDeg(obj.val) + 540) % 360 - 180);
|
|
@@ -512,30 +640,39 @@ function startDisplayLoop() {
|
|
|
512
640
|
upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
|
|
513
641
|
upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
|
|
514
642
|
|
|
515
|
-
// --- LOGICA TACK STRATEGICA (
|
|
643
|
+
// --- LOGICA TACK STRATEGICA (VETTORIALE) ---
|
|
516
644
|
if (hObj && twdObj) {
|
|
517
|
-
|
|
518
|
-
const
|
|
645
|
+
// Funzione interna per riflettere un angolo rispetto all'asse del vento (TWD)
|
|
646
|
+
const reflectAngle = (targetRad, axisRad) => {
|
|
647
|
+
const diffSin = Math.sin(axisRad - targetRad);
|
|
648
|
+
const diffCos = Math.cos(axisRad - targetRad);
|
|
649
|
+
return Math.atan2(Math.sin(axisRad) * diffCos + Math.cos(axisRad) * diffSin,
|
|
650
|
+
Math.cos(axisRad) * diffCos - Math.sin(axisRad) * diffSin);
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
519
654
|
|
|
520
655
|
if (!isNavigating) {
|
|
521
|
-
ui.tackHdg.innerHTML = "---°";
|
|
656
|
+
ui.tackHdg.innerHTML = "---°";
|
|
522
657
|
} else if (unstableH) {
|
|
523
658
|
ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data');
|
|
524
659
|
} else {
|
|
525
|
-
|
|
660
|
+
const reflectedH = reflectAngle(hObj.val, twdObj.val);
|
|
661
|
+
const outH = (radToDeg(reflectedH) + 360) % 360;
|
|
662
|
+
ui.tackHdg.innerHTML = `${Math.round(outH).toString().padStart(3, '0')}°`;
|
|
526
663
|
ui.tackHdg.classList.remove('unstable-data');
|
|
527
664
|
}
|
|
528
665
|
|
|
529
666
|
if (cObj) {
|
|
530
|
-
const
|
|
531
|
-
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout || twdObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
532
|
-
|
|
667
|
+
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
533
668
|
if (!isNavigating) {
|
|
534
|
-
ui.tackCog.innerHTML = "---°";
|
|
669
|
+
ui.tackCog.innerHTML = "---°";
|
|
535
670
|
} else if (unstableC) {
|
|
536
671
|
ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data');
|
|
537
672
|
} else {
|
|
538
|
-
|
|
673
|
+
const reflectedC = reflectAngle(cObj.val, twdObj.val);
|
|
674
|
+
const outC = (radToDeg(reflectedC) + 360) % 360;
|
|
675
|
+
ui.tackCog.innerHTML = `${Math.round(outC).toString().padStart(3, '0')}°`;
|
|
539
676
|
ui.tackCog.classList.remove('unstable-data');
|
|
540
677
|
}
|
|
541
678
|
}
|
|
@@ -616,6 +753,10 @@ function updateScaleLabels(t, min, max) {
|
|
|
616
753
|
* drawGraph: Disegna i grafici con griglia temporale intelligente
|
|
617
754
|
* Usa un Gradiente Lineare SVG dinamico per eliminare le giunzioni dei poligoni.
|
|
618
755
|
*/
|
|
756
|
+
/**
|
|
757
|
+
* drawGraph: Disegna i grafici con griglia temporale intelligente
|
|
758
|
+
* Versione bilanciata: 1.5px per allarmi critici, 1px per il resto.
|
|
759
|
+
*/
|
|
619
760
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
620
761
|
const svg = document.getElementById(id);
|
|
621
762
|
if (!svg || d.length < 2) return;
|
|
@@ -625,7 +766,7 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
625
766
|
const isDepth = (id === 'depth-graph');
|
|
626
767
|
const samples = CONFIG.graphs.samples;
|
|
627
768
|
|
|
628
|
-
// 1. Griglia
|
|
769
|
+
// 1. Griglia (Sottile 0.5px)
|
|
629
770
|
let grids = "";
|
|
630
771
|
[0.25, 0.5, 0.75].forEach(p => {
|
|
631
772
|
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" />`;
|
|
@@ -636,7 +777,7 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
636
777
|
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
637
778
|
}
|
|
638
779
|
|
|
639
|
-
// 2. Colori
|
|
780
|
+
// 2. Tavolozza Colori Vividi
|
|
640
781
|
const baseColorTws = "#2c3e50";
|
|
641
782
|
const baseColorDepth = "#0088cc";
|
|
642
783
|
|
|
@@ -656,49 +797,46 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
656
797
|
let areaPath = `M 0 ${h} `;
|
|
657
798
|
|
|
658
799
|
for (let i = 1; i < d.length; i++) {
|
|
659
|
-
// Calcolo Percentuali per il gradiente
|
|
660
800
|
const percentPrev = ((i - 1) / (samples - 1)) * 100;
|
|
661
801
|
const percentCurr = (i / (samples - 1)) * 100;
|
|
662
802
|
|
|
663
|
-
// Coordinate geometriche
|
|
664
803
|
const x1 = ((i - 1) / (samples - 1)) * w;
|
|
665
804
|
const y1 = h - (Math.max(0, Math.min(1, (d[i - 1] - min) / range)) * h);
|
|
666
805
|
const x2 = (i / (samples - 1)) * w;
|
|
667
806
|
const y2 = h - (Math.max(0, Math.min(1, (d[i] - min) / range)) * h);
|
|
668
807
|
|
|
669
808
|
const color = getColor(d[i]);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
strokeWidth = "1"; // Manteniamo la linea spessa per leggerla bene
|
|
683
|
-
}
|
|
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
|
+
}
|
|
684
821
|
|
|
685
|
-
//
|
|
822
|
+
// Costruzione Gradiente (stacchi netti tra i colori)
|
|
686
823
|
gradientStops += `<stop offset="${percentPrev}%" stop-color="${color}" stop-opacity="${fillOpacity}" />`;
|
|
687
824
|
gradientStops += `<stop offset="${percentCurr}%" stop-color="${color}" stop-opacity="${fillOpacity}" />`;
|
|
688
825
|
|
|
689
|
-
//
|
|
826
|
+
// Disegno Linea con precisione geometrica
|
|
690
827
|
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
|
|
691
|
-
style="stroke: ${color}; stroke-width: ${strokeWidth}; stroke-linecap: round;" />`;
|
|
828
|
+
style="stroke: ${color}; stroke-width: ${strokeWidth}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
|
|
692
829
|
|
|
693
|
-
// AGGIORNAMENTO PATH UNICO
|
|
694
830
|
if (i === 1) areaPath += `L ${x1} ${y1} `;
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
831
|
+
areaPath += `L ${x2} ${y2} `;
|
|
832
|
+
|
|
833
|
+
// Salviamo l'ultima coordinata X calcolata per chiudere correttamente il path
|
|
834
|
+
if (i === d.length - 1) {
|
|
835
|
+
areaPath += `L ${x2} ${h} Z`;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
700
838
|
|
|
701
|
-
// 4. Iniezione del
|
|
839
|
+
// 4. Iniezione del Gradiente
|
|
702
840
|
const gradId = `grad-${id}`;
|
|
703
841
|
const defs = `
|
|
704
842
|
<defs>
|
|
@@ -709,12 +847,12 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
709
847
|
`;
|
|
710
848
|
|
|
711
849
|
// 5. Render Finale
|
|
712
|
-
// Se è Depth o Tws applichiamo il gradiente, altrimenti colore standard fisso (per SOG/STW)
|
|
713
850
|
if (isTws || isDepth) {
|
|
714
851
|
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
715
852
|
} else {
|
|
716
|
-
|
|
717
|
-
|
|
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}`;
|
|
718
856
|
}
|
|
719
857
|
}
|
|
720
858
|
|
|
@@ -777,7 +915,22 @@ function connect() {
|
|
|
777
915
|
try {
|
|
778
916
|
socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
|
|
779
917
|
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; reconnectDelay = 1000; };
|
|
780
|
-
|
|
918
|
+
|
|
919
|
+
socket.onmessage = (e) => {
|
|
920
|
+
const d = JSON.parse(e.data);
|
|
921
|
+
if (d.updates) {
|
|
922
|
+
d.updates.forEach(u => {
|
|
923
|
+
// 1. Estraiamo il nome del sensore/sorgente (es. "yacht_device" o "Unknown")
|
|
924
|
+
const sourceLabel = u.source ? (u.source.label || u.source.talker || "Unknown") : "Unknown";
|
|
925
|
+
|
|
926
|
+
// 2. Passiamo il nome della sorgente come TERZO parametro
|
|
927
|
+
if (u.values) {
|
|
928
|
+
u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
|
|
781
934
|
socket.onclose = () => { if (!simulationMode) { ui.status.className = "offline"; ui.status.innerText = "RECONNECTING..."; setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); } };
|
|
782
935
|
} catch (e) { setTimeout(connect, reconnectDelay); }
|
|
783
936
|
}
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -122,6 +122,12 @@ body {
|
|
|
122
122
|
.box-twd #twd-avg { font-size: clamp(1.4rem, 45cqh, 20cqw) !important; }
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/* --- OTTIMIZZAZIONE SPECIFICA TWD PER EVITARE ANDATA A CAPO in landscape--- */
|
|
126
|
+
.box-twd .value-large {
|
|
127
|
+
/* Riduciamo il limite di larghezza (18cqw) per far spazio al simbolo ± rimpicciolendo il testo se serve */
|
|
128
|
+
font-size: clamp(1.2rem, 55cqh, 18cqw) !important;
|
|
129
|
+
}
|
|
130
|
+
|
|
125
131
|
/* --- BOX TACK (Mure Opposte) --- */
|
|
126
132
|
.box-tack .dual-value-container { display: flex; flex-direction: row; justify-content: space-between; align-items: center; width: 100%; height: 100%; }
|
|
127
133
|
.box-tack .dual-value-col { display: flex; flex-direction: column; justify-content: center; }
|