@sailingrotevista/rotevista-dash 2.0.14 → 2.0.15
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 +70 -53
- package/package.json +1 -1
package/app.js
CHANGED
|
@@ -41,7 +41,10 @@ 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
|
+
|
|
45
|
+
// Variabili di stato per filtri e trend
|
|
44
46
|
let smoothedLeeway = 0;
|
|
47
|
+
let rotationTrend = 0;
|
|
45
48
|
let pressTimer, isFocusActive = false, blockNextClick = false;
|
|
46
49
|
|
|
47
50
|
const graphModes = {
|
|
@@ -137,8 +140,41 @@ function processIncomingData(path, val) {
|
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
// ==========================================================================
|
|
140
|
-
// 5. MOTORE RENDERING PRINCIPALE
|
|
143
|
+
// 5. MOTORE RENDERING PRINCIPALE E TREND
|
|
141
144
|
// ==========================================================================
|
|
145
|
+
|
|
146
|
+
// Calcolo Trend del Vento (Definita prima del loop per evitare ReferenceError)
|
|
147
|
+
function updateWindTrend() {
|
|
148
|
+
const longAvg = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
149
|
+
const shortAvg = getCircularAverageFromBuffer(store.longBuf.twd, 15000, false);
|
|
150
|
+
|
|
151
|
+
if (!longAvg || !shortAvg) return;
|
|
152
|
+
|
|
153
|
+
let diff = (shortAvg.val - longAvg.val + 540) % 360 - 180;
|
|
154
|
+
|
|
155
|
+
// Smoothing del trend per evitare scatti
|
|
156
|
+
rotationTrend = (rotationTrend * 0.95) + (diff * 0.05);
|
|
157
|
+
|
|
158
|
+
// Seleziona sia i pallini della bussola che del gauge centrale
|
|
159
|
+
const cwDots = [document.getElementById('trend-dot-cw'), document.getElementById('trend-gauge-cw')];
|
|
160
|
+
const ccwDots = [document.getElementById('trend-dot-ccw'), document.getElementById('trend-gauge-ccw')];
|
|
161
|
+
|
|
162
|
+
// Soglia: il vento deve ruotare di almeno 1.5 gradi di media per attivarsi
|
|
163
|
+
if (Math.abs(rotationTrend) > 1.5) {
|
|
164
|
+
if (rotationTrend > 0) { // Orario
|
|
165
|
+
cwDots.forEach(el => el && el.classList.add('is-trending'));
|
|
166
|
+
ccwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
167
|
+
} else { // Antiorario
|
|
168
|
+
ccwDots.forEach(el => el && el.classList.add('is-trending'));
|
|
169
|
+
cwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
// Vento stabile, spegni tutti i pallini
|
|
173
|
+
cwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
174
|
+
ccwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
142
178
|
function startDisplayLoop() {
|
|
143
179
|
renderInterval = setInterval(() => {
|
|
144
180
|
const now = Date.now();
|
|
@@ -149,7 +185,7 @@ function startDisplayLoop() {
|
|
|
149
185
|
let curSog = 0; if (store.raw["navigation.speedOverGround"] !== undefined) { curSog = msToKts(store.raw["navigation.speedOverGround"]); ui.sog.innerText = curSog.toFixed(1); manageHistory('sog', curSog); }
|
|
150
186
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) { const d = store.raw["environment.depth.belowTransducer"]; ui.depth.innerText = d.toFixed(1); checkDepthAlarm(d); manageHistory('depth', d); }
|
|
151
187
|
|
|
152
|
-
// --- 1. RENDERING TWS CON COLORE DINAMICO
|
|
188
|
+
// --- 1. RENDERING TWS CON COLORE DINAMICO ---
|
|
153
189
|
if (store.raw["environment.wind.speedTrue"] !== undefined) {
|
|
154
190
|
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
|
|
155
191
|
ui.tws.innerText = twsKts.toFixed(1);
|
|
@@ -167,27 +203,20 @@ function startDisplayLoop() {
|
|
|
167
203
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
168
204
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
169
205
|
|
|
206
|
+
// --- CALCOLO E SMUSSAMENTO LEEWAY ---
|
|
170
207
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
171
|
-
// 1. Calcolo grezzo
|
|
172
208
|
let rawDrift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
|
|
173
209
|
if (rawDrift > 180) rawDrift -= 360;
|
|
174
210
|
|
|
175
|
-
// 2. Filtro di stabilità (Low Pass Filter)
|
|
176
|
-
// Se la barca è ferma, azzera brutalmente. Altrimenti filtra il valore.
|
|
177
211
|
if (curSog < CONFIG.averages.minSpeed) {
|
|
178
212
|
smoothedLeeway = 0;
|
|
179
213
|
} else {
|
|
180
|
-
// Smoothing: 90% valore precedente, 10% nuovo valore
|
|
181
214
|
smoothedLeeway = (smoothedLeeway * 0.9) + (rawDrift * 0.1);
|
|
182
215
|
}
|
|
183
216
|
|
|
184
|
-
// 3. Renderizziamo il valore filtrato
|
|
185
217
|
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
186
218
|
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);
|
|
219
|
+
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
191
220
|
} else {
|
|
192
221
|
updateLeewayDisplay(0);
|
|
193
222
|
}
|
|
@@ -208,14 +237,8 @@ function startDisplayLoop() {
|
|
|
208
237
|
let val = Math.round(obj.val);
|
|
209
238
|
let displayVal;
|
|
210
239
|
|
|
211
|
-
if (isCompass)
|
|
212
|
-
|
|
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
|
-
}
|
|
240
|
+
if (isCompass) displayVal = ((val + 360) % 360).toString().padStart(3, '0');
|
|
241
|
+
else displayVal = val.toString();
|
|
219
242
|
|
|
220
243
|
el.innerHTML = `${displayVal}°`;
|
|
221
244
|
|
|
@@ -224,11 +247,11 @@ function startDisplayLoop() {
|
|
|
224
247
|
}
|
|
225
248
|
};
|
|
226
249
|
|
|
227
|
-
upUI(ui.hdg, hObj, true);
|
|
228
|
-
upUI(ui.cog, cObj, true);
|
|
229
|
-
upUI(ui.awaAvg, awObj, false);
|
|
230
|
-
upUI(ui.twaAvg, twObj, false);
|
|
231
|
-
upUI(ui.twdAvg, twdObj, true);
|
|
250
|
+
upUI(ui.hdg, hObj, true);
|
|
251
|
+
upUI(ui.cog, cObj, true);
|
|
252
|
+
upUI(ui.awaAvg, awObj, false);
|
|
253
|
+
upUI(ui.twaAvg, twObj, false);
|
|
254
|
+
upUI(ui.twdAvg, twdObj, true);
|
|
232
255
|
|
|
233
256
|
if (hObj && twObj && hObj.val !== null) {
|
|
234
257
|
let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
@@ -240,7 +263,8 @@ function startDisplayLoop() {
|
|
|
240
263
|
|
|
241
264
|
// Rotazione Bussola Tattica e Colore Sincronizzato
|
|
242
265
|
if (twdObj && hObj) {
|
|
243
|
-
updateWindTrend();
|
|
266
|
+
updateWindTrend(); // Aggiorna i pallini
|
|
267
|
+
|
|
244
268
|
curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val);
|
|
245
269
|
ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`);
|
|
246
270
|
ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`);
|
|
@@ -378,16 +402,14 @@ function toggleFocusMode(type, element) {
|
|
|
378
402
|
// ==========================================================================
|
|
379
403
|
if (ui.hotspot) {
|
|
380
404
|
let pressTimer;
|
|
381
|
-
const HOLD_DURATION = 1000;
|
|
405
|
+
const HOLD_DURATION = 1000;
|
|
382
406
|
|
|
383
407
|
ui.hotspot.addEventListener('pointerdown', (e) => {
|
|
384
|
-
// Avvia il timer al tocco
|
|
385
408
|
pressTimer = setTimeout(() => {
|
|
386
409
|
document.body.classList.toggle('night-mode');
|
|
387
|
-
// Feedback visivo immediato al cambio modalità
|
|
388
410
|
ui.hotspot.style.opacity = "0.5";
|
|
389
411
|
setTimeout(() => ui.hotspot.style.opacity = "1", 200);
|
|
390
|
-
pressTimer = null;
|
|
412
|
+
pressTimer = null;
|
|
391
413
|
}, HOLD_DURATION);
|
|
392
414
|
});
|
|
393
415
|
|
|
@@ -395,7 +417,6 @@ if (ui.hotspot) {
|
|
|
395
417
|
if (pressTimer) {
|
|
396
418
|
clearTimeout(pressTimer);
|
|
397
419
|
pressTimer = null;
|
|
398
|
-
// Se arriviamo qui, è stato un click rapido -> Fullscreen
|
|
399
420
|
const doc = document.documentElement;
|
|
400
421
|
const isF = document.fullscreenElement || document.webkitFullscreenElement;
|
|
401
422
|
if (!isF) {
|
|
@@ -422,7 +443,9 @@ window.addEventListener('load', init);
|
|
|
422
443
|
function checkDepthAlarm(m) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
|
|
423
444
|
function playBingBing() { if (!audioCtx) return; const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
|
|
424
445
|
|
|
425
|
-
//
|
|
446
|
+
// ==========================================================================
|
|
447
|
+
// 9. MOTORE SIMULAZIONE DINAMICA E AVVIO (3 Click su Depth)
|
|
448
|
+
// ==========================================================================
|
|
426
449
|
ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
427
450
|
let dC = 0, lC = 0;
|
|
428
451
|
return function() {
|
|
@@ -433,7 +456,7 @@ ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
|
433
456
|
simulationMode = !simulationMode;
|
|
434
457
|
if (simulationMode) {
|
|
435
458
|
if (socket) socket.close();
|
|
436
|
-
startDynamicSimulation();
|
|
459
|
+
startDynamicSimulation();
|
|
437
460
|
} else {
|
|
438
461
|
location.reload();
|
|
439
462
|
}
|
|
@@ -442,56 +465,52 @@ ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
|
442
465
|
};
|
|
443
466
|
})());
|
|
444
467
|
|
|
445
|
-
// ==========================================================================
|
|
446
|
-
// 9. MOTORE SIMULAZIONE DINAMICA (VERSIONE STOCASTICA)
|
|
447
|
-
// ==========================================================================
|
|
448
468
|
function startDynamicSimulation() {
|
|
449
469
|
ui.status.innerText = "SIM ATTIVO";
|
|
450
470
|
|
|
451
|
-
// 1. STATO INIZIALE (Aggiunto leeway a 0)
|
|
452
471
|
let sim = {
|
|
453
472
|
hdg: Math.random() * 360,
|
|
454
473
|
tws: 12,
|
|
455
474
|
twd: Math.random() * 360,
|
|
456
475
|
depth: 12,
|
|
457
476
|
stw: 5,
|
|
458
|
-
leeway: 0,
|
|
477
|
+
leeway: 0,
|
|
459
478
|
startTime: Date.now()
|
|
460
479
|
};
|
|
461
480
|
|
|
462
481
|
simInterval = setInterval(() => {
|
|
463
482
|
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
464
483
|
|
|
465
|
-
//
|
|
466
|
-
if (elapsed > 120 && elapsed < 121)
|
|
467
|
-
|
|
468
|
-
}
|
|
484
|
+
// Vento: Rotazione lenta + Salto netto a 120s
|
|
485
|
+
if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
|
|
486
|
+
sim.twd = (sim.twd + (Math.sin(elapsed / 20) * 0.5) + 360) % 360;
|
|
469
487
|
|
|
470
|
-
//
|
|
488
|
+
// Raffica
|
|
471
489
|
const inGust = (elapsed % 40) < 5;
|
|
472
490
|
const targetTws = (inGust ? 18 : 10) + Math.sin(elapsed / 10) * 2;
|
|
473
|
-
sim.tws += (targetTws - sim.tws) * 0.05;
|
|
491
|
+
sim.tws += (targetTws - sim.tws) * 0.05;
|
|
492
|
+
|
|
493
|
+
// Barca segue il vento pigramente
|
|
494
|
+
sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
|
|
474
495
|
|
|
475
|
-
// 4. POLARE SEMPLIFICATA (STW in base al TWA)
|
|
476
496
|
let twaRel = (sim.twd - sim.hdg + 360) % 360;
|
|
477
497
|
if (twaRel > 180) twaRel -= 360;
|
|
478
498
|
|
|
499
|
+
// Polare smussata
|
|
479
500
|
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;
|
|
501
|
+
sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
|
|
481
502
|
|
|
482
|
-
//
|
|
503
|
+
// Leeway smussato
|
|
483
504
|
const rawLeeway = Math.sin(degToRad(twaRel)) * 4;
|
|
484
|
-
sim.leeway += (rawLeeway - sim.leeway) * 0.05;
|
|
505
|
+
sim.leeway += (rawLeeway - sim.leeway) * 0.05;
|
|
485
506
|
|
|
486
|
-
//
|
|
507
|
+
// Vettori Reali
|
|
487
508
|
const cog = (sim.hdg - sim.leeway + 360) % 360;
|
|
488
|
-
|
|
489
|
-
// 7. CALCOLO VENTO APPARENTE (AWS/AWA)
|
|
490
509
|
const twaRad = degToRad(twaRel);
|
|
491
510
|
const aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
|
|
492
511
|
const awa = radToDeg(Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad)));
|
|
493
512
|
|
|
494
|
-
//
|
|
513
|
+
// Invio Dati (Con tutto il set necessario per la UI)
|
|
495
514
|
processIncomingData("environment.wind.speedTrue", ktsToMs(sim.tws));
|
|
496
515
|
processIncomingData("environment.wind.directionTrue", degToRad(sim.twd));
|
|
497
516
|
processIncomingData("environment.wind.angleTrueWater", degToRad(twaRel));
|
|
@@ -503,7 +522,5 @@ function startDynamicSimulation() {
|
|
|
503
522
|
processIncomingData("navigation.speedOverGround", ktsToMs(sim.stw));
|
|
504
523
|
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
|
|
505
524
|
|
|
506
|
-
// Aggiorna display grafico
|
|
507
|
-
updateLeewayDisplay(sim.leeway);
|
|
508
525
|
}, 1000);
|
|
509
526
|
}
|