@sailingrotevista/rotevista-dash 2.0.13 → 2.0.14
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 +107 -45
- package/index.html +4 -1
- package/package.json +1 -1
- package/style.css +34 -8
package/app.js
CHANGED
|
@@ -41,7 +41,7 @@ let simulationMode = false;
|
|
|
41
41
|
let socket, renderInterval, simInterval;
|
|
42
42
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
43
43
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
44
|
-
|
|
44
|
+
let smoothedLeeway = 0;
|
|
45
45
|
let pressTimer, isFocusActive = false, blockNextClick = false;
|
|
46
46
|
|
|
47
47
|
const graphModes = {
|
|
@@ -167,12 +167,30 @@ function startDisplayLoop() {
|
|
|
167
167
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
168
168
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
169
169
|
|
|
170
|
-
if (store.raw["navigation.courseOverGroundTrue"] && store.raw["navigation.headingTrue"]) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
171
|
+
// 1. Calcolo grezzo
|
|
172
|
+
let rawDrift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
|
|
173
|
+
if (rawDrift > 180) rawDrift -= 360;
|
|
174
|
+
|
|
175
|
+
// 2. Filtro di stabilità (Low Pass Filter)
|
|
176
|
+
// Se la barca è ferma, azzera brutalmente. Altrimenti filtra il valore.
|
|
177
|
+
if (curSog < CONFIG.averages.minSpeed) {
|
|
178
|
+
smoothedLeeway = 0;
|
|
179
|
+
} else {
|
|
180
|
+
// Smoothing: 90% valore precedente, 10% nuovo valore
|
|
181
|
+
smoothedLeeway = (smoothedLeeway * 0.9) + (rawDrift * 0.1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3. Renderizziamo il valore filtrato
|
|
185
|
+
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
186
|
+
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
187
|
+
|
|
188
|
+
// Limita il valore grafico e aggiorna il testo
|
|
189
|
+
let ds = Math.max(-20, Math.min(20, smoothedLeeway));
|
|
190
|
+
updateLeewayDisplay(ds);
|
|
191
|
+
} else {
|
|
192
|
+
updateLeewayDisplay(0);
|
|
193
|
+
}
|
|
176
194
|
|
|
177
195
|
// --- 2. RENDERING MEDIE E BUSSOLA TATTICA ---
|
|
178
196
|
if (now - lastAvgUIUpdate > 3000) {
|
|
@@ -182,14 +200,35 @@ function startDisplayLoop() {
|
|
|
182
200
|
twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true),
|
|
183
201
|
twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
|
|
184
202
|
|
|
185
|
-
const upUI = (el, obj) => {
|
|
186
|
-
if (!obj
|
|
187
|
-
|
|
188
|
-
el.
|
|
189
|
-
|
|
203
|
+
const upUI = (el, obj, isCompass = false) => {
|
|
204
|
+
if (!obj || obj.val === null) {
|
|
205
|
+
el.innerHTML = "---°";
|
|
206
|
+
el.classList.remove('unstable-data');
|
|
207
|
+
} else {
|
|
208
|
+
let val = Math.round(obj.val);
|
|
209
|
+
let displayVal;
|
|
210
|
+
|
|
211
|
+
if (isCompass) {
|
|
212
|
+
// Formattazione per HEADING, COG, TWD (0-360°)
|
|
213
|
+
displayVal = ((val + 360) % 360).toString().padStart(3, '0');
|
|
214
|
+
} else {
|
|
215
|
+
// Formattazione per AWA, TWA (-180 a 180°)
|
|
216
|
+
// Se è negativo, il segno viene mantenuto correttamente
|
|
217
|
+
displayVal = val.toString();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
el.innerHTML = `${displayVal}°`;
|
|
221
|
+
|
|
222
|
+
if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data');
|
|
223
|
+
else el.classList.add('unstable-data');
|
|
190
224
|
}
|
|
191
225
|
};
|
|
192
|
-
|
|
226
|
+
|
|
227
|
+
upUI(ui.hdg, hObj, true); // Bussola
|
|
228
|
+
upUI(ui.cog, cObj, true); // Bussola
|
|
229
|
+
upUI(ui.awaAvg, awObj, false); // Angolo
|
|
230
|
+
upUI(ui.twaAvg, twObj, false); // Angolo
|
|
231
|
+
upUI(ui.twdAvg, twdObj, true); // Bussola (La direzione del vento è sempre 0-360)
|
|
193
232
|
|
|
194
233
|
if (hObj && twObj && hObj.val !== null) {
|
|
195
234
|
let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
@@ -201,6 +240,7 @@ function startDisplayLoop() {
|
|
|
201
240
|
|
|
202
241
|
// Rotazione Bussola Tattica e Colore Sincronizzato
|
|
203
242
|
if (twdObj && hObj) {
|
|
243
|
+
updateWindTrend();
|
|
204
244
|
curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val);
|
|
205
245
|
ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`);
|
|
206
246
|
ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`);
|
|
@@ -403,45 +443,67 @@ ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
|
403
443
|
})());
|
|
404
444
|
|
|
405
445
|
// ==========================================================================
|
|
406
|
-
// 9. MOTORE SIMULAZIONE DINAMICA
|
|
446
|
+
// 9. MOTORE SIMULAZIONE DINAMICA (VERSIONE STOCASTICA)
|
|
407
447
|
// ==========================================================================
|
|
408
448
|
function startDynamicSimulation() {
|
|
409
449
|
ui.status.innerText = "SIM ATTIVO";
|
|
410
|
-
|
|
411
|
-
//
|
|
412
|
-
let
|
|
413
|
-
|
|
450
|
+
|
|
451
|
+
// 1. STATO INIZIALE (Aggiunto leeway a 0)
|
|
452
|
+
let sim = {
|
|
453
|
+
hdg: Math.random() * 360,
|
|
454
|
+
tws: 12,
|
|
455
|
+
twd: Math.random() * 360,
|
|
456
|
+
depth: 12,
|
|
457
|
+
stw: 5,
|
|
458
|
+
leeway: 0, // Inerzia per il Leeway
|
|
459
|
+
startTime: Date.now()
|
|
460
|
+
};
|
|
461
|
+
|
|
414
462
|
simInterval = setInterval(() => {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
// AWA = atan2(tws*sin(twa), stw + tws*cos(twa))
|
|
431
|
-
const awa = radToDeg(Math.atan2(tws * Math.sin(twaRad), stw + tws * Math.cos(twaRad)));
|
|
463
|
+
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
464
|
+
|
|
465
|
+
// 2. SALTO DI VENTO (dopo 120 secondi)
|
|
466
|
+
if (elapsed > 120 && elapsed < 121) {
|
|
467
|
+
sim.twd = Math.random() * 360;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 3. RAFFICA (ogni 40s per 5s) con smoothing 0.05
|
|
471
|
+
const inGust = (elapsed % 40) < 5;
|
|
472
|
+
const targetTws = (inGust ? 18 : 10) + Math.sin(elapsed / 10) * 2;
|
|
473
|
+
sim.tws += (targetTws - sim.tws) * 0.05; // Smoothing lento
|
|
474
|
+
|
|
475
|
+
// 4. POLARE SEMPLIFICATA (STW in base al TWA)
|
|
476
|
+
let twaRel = (sim.twd - sim.hdg + 360) % 360;
|
|
477
|
+
if (twaRel > 180) twaRel -= 360;
|
|
432
478
|
|
|
433
|
-
|
|
434
|
-
|
|
479
|
+
let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125));
|
|
480
|
+
sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05; // Smoothing STW
|
|
481
|
+
|
|
482
|
+
// 5. CALCOLO LEEWAY CON FILTRO (Inerzia)
|
|
483
|
+
const rawLeeway = Math.sin(degToRad(twaRel)) * 4;
|
|
484
|
+
sim.leeway += (rawLeeway - sim.leeway) * 0.05; // Smoothing 0.05 per evitare i balli di +/- 3.1
|
|
485
|
+
|
|
486
|
+
// 6. CALCOLO VETTORIALE COG
|
|
487
|
+
const cog = (sim.hdg - sim.leeway + 360) % 360;
|
|
435
488
|
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
489
|
+
// 7. CALCOLO VENTO APPARENTE (AWS/AWA)
|
|
490
|
+
const twaRad = degToRad(twaRel);
|
|
491
|
+
const aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
|
|
492
|
+
const awa = radToDeg(Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad)));
|
|
493
|
+
|
|
494
|
+
// 8. INVIO DATI PULITI
|
|
495
|
+
processIncomingData("environment.wind.speedTrue", ktsToMs(sim.tws));
|
|
496
|
+
processIncomingData("environment.wind.directionTrue", degToRad(sim.twd));
|
|
497
|
+
processIncomingData("environment.wind.angleTrueWater", degToRad(twaRel));
|
|
439
498
|
processIncomingData("environment.wind.speedApparent", ktsToMs(aws));
|
|
440
499
|
processIncomingData("environment.wind.angleApparent", degToRad(awa));
|
|
441
|
-
processIncomingData("environment.depth.belowTransducer", depth);
|
|
442
|
-
processIncomingData("navigation.headingTrue", degToRad(
|
|
443
|
-
processIncomingData("navigation.speedThroughWater", ktsToMs(stw));
|
|
444
|
-
processIncomingData("navigation.
|
|
500
|
+
processIncomingData("environment.depth.belowTransducer", sim.depth);
|
|
501
|
+
processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
502
|
+
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw));
|
|
503
|
+
processIncomingData("navigation.speedOverGround", ktsToMs(sim.stw));
|
|
504
|
+
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
|
|
505
|
+
|
|
506
|
+
// Aggiorna display grafico
|
|
507
|
+
updateLeewayDisplay(sim.leeway);
|
|
445
508
|
}, 1000);
|
|
446
509
|
}
|
|
447
|
-
|
package/index.html
CHANGED
|
@@ -179,6 +179,8 @@
|
|
|
179
179
|
<g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
|
|
180
180
|
<path d="M200,90 L206,98 L200,125 L194,98 Z" fill="#ffff00" stroke="#000" stroke-width="0.8" />
|
|
181
181
|
<text x="200" y="104" fill="#000" font-size="8" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
|
|
182
|
+
<circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#ff0000" opacity="0" />
|
|
183
|
+
<circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#00ff00" opacity="0" />
|
|
182
184
|
</g>
|
|
183
185
|
|
|
184
186
|
<!-- Barra LEEWAY / SCARROCCIO Inferiore -->
|
|
@@ -282,7 +284,6 @@
|
|
|
282
284
|
<path d="M 20,17 L 17,26 L 20,24.5 L 23,26 Z" fill="white" opacity="0.2" />
|
|
283
285
|
</g>
|
|
284
286
|
|
|
285
|
-
<!-- 2. INDICATORE VENTO: Ruota con il TWD (id: twd-arrow) -->
|
|
286
287
|
<g id="twd-arrow" transform="rotate(0, 20, 20)">
|
|
287
288
|
<path id="twd-wind-chevron" d="M 17,5 L 20,7.5 L 23,5"
|
|
288
289
|
fill="none"
|
|
@@ -290,6 +291,8 @@
|
|
|
290
291
|
stroke-width="2.2"
|
|
291
292
|
stroke-linecap="round"
|
|
292
293
|
stroke-linejoin="round" />
|
|
294
|
+
<circle id="trend-dot-cw" cx="24" cy="7.5" r="1.5" fill="#ff0000" />
|
|
295
|
+
<circle id="trend-dot-ccw" cx="16" cy="7.5" r="1.5" fill="#00ff00" />
|
|
293
296
|
</g>
|
|
294
297
|
</svg>
|
|
295
298
|
<span class="value value-large" id="twd-avg">---°</span>
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -77,7 +77,7 @@ body {
|
|
|
77
77
|
.data-box {
|
|
78
78
|
position: relative;
|
|
79
79
|
border-bottom: 1px solid #222;
|
|
80
|
-
padding: 8px
|
|
80
|
+
padding: 4px 8px;
|
|
81
81
|
display: flex;
|
|
82
82
|
flex-direction: column;
|
|
83
83
|
width: 100%;
|
|
@@ -91,9 +91,18 @@ body {
|
|
|
91
91
|
.left-panel .data-box { align-items: flex-start; text-align: left; }
|
|
92
92
|
.right-panel .data-box { align-items: flex-end; text-align: right; }
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
.side-panel {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
height: 100%;
|
|
98
|
+
gap: 2px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.data-box {
|
|
102
|
+
flex: 1 1 0px; /* Ogni box si divide lo spazio equamente, ma può ridursi se serve */
|
|
103
|
+
min-height: 0; /* Fondamentale per permettere al grafico di non eccedere */
|
|
104
|
+
padding: 2px 4px; /* Ancora più compatto */
|
|
105
|
+
}
|
|
97
106
|
|
|
98
107
|
/* ==========================================================================
|
|
99
108
|
3. TACTICAL FOCUS MODE (AUTO-EXPANDING SPLIT)
|
|
@@ -208,20 +217,20 @@ body {
|
|
|
208
217
|
.graph-wrapper {
|
|
209
218
|
position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px;
|
|
210
219
|
display: flex; align-items: stretch; background: rgba(255, 255, 255, 0.03);
|
|
211
|
-
border-radius: 4px; gap:
|
|
220
|
+
border-radius: 4px; gap: 0px !important;
|
|
212
221
|
}
|
|
213
222
|
|
|
214
223
|
/* Spostamento verso il centro per eliminare spazi neri morti */
|
|
215
224
|
.left-panel .graph-wrapper { margin-right: -6px; }
|
|
216
225
|
.right-panel .graph-wrapper { margin-left: -6px; }
|
|
217
226
|
|
|
218
|
-
.sparkline { flex-grow: 1; height: 100%; background: transparent !important; display: block; }
|
|
227
|
+
.sparkline { flex-grow: 1; height: 100%; width: 100% !important; background: transparent !important; display: block; }
|
|
219
228
|
.sparkline path, .sparkline line { stroke-width: 1px !important; transition: all 0.3s ease; }
|
|
220
229
|
|
|
221
230
|
.scale-labels {
|
|
222
231
|
display: flex; flex-direction: column; justify-content: space-between;
|
|
223
|
-
font-size: 10px; color: #777; font-weight: bold; min-width:
|
|
224
|
-
height: 100%; line-height: 1; padding:
|
|
232
|
+
font-size: 10px; color: #777; font-weight: bold; min-width: 12px;
|
|
233
|
+
height: 100%; line-height: 1; padding: 0 px 0; border: none !important;
|
|
225
234
|
}
|
|
226
235
|
|
|
227
236
|
/* Simmetria: le scale numeriche "guardano" sempre il quadrante centrale */
|
|
@@ -452,3 +461,20 @@ body.night-mode {
|
|
|
452
461
|
fill: #ff3333 !important;
|
|
453
462
|
opacity: 0.4 !important;
|
|
454
463
|
}
|
|
464
|
+
|
|
465
|
+
/* Definiamo il lampeggio */
|
|
466
|
+
@keyframes blink-trend {
|
|
467
|
+
0%, 100% { opacity: 1; }
|
|
468
|
+
50% { opacity: 0; }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* Stile base dei pallini */
|
|
472
|
+
#trend-dot-cw, #trend-dot-ccw, #trend-gauge-cw, #trend-gauge-ccw {
|
|
473
|
+
opacity: 0.3;
|
|
474
|
+
transition: opacity 0.3s ease;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* Quando attivi la classe, forziamo l'animazione */
|
|
478
|
+
.is-trending {
|
|
479
|
+
animation: blink-trend 1s infinite !important;
|
|
480
|
+
}
|