@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.
Files changed (2) hide show
  1. package/app.js +70 -53
  2. 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 (BIANCO -> ARANCIO -> ROSSO) ---
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
- // 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
- }
240
+ if (isCompass) displayVal = ((val + 360) % 360).toString().padStart(3, '0');
241
+ else displayVal = val.toString();
219
242
 
220
243
  el.innerHTML = `${displayVal}&deg;`;
221
244
 
@@ -224,11 +247,11 @@ function startDisplayLoop() {
224
247
  }
225
248
  };
226
249
 
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)
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')}&deg;`;
@@ -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; // 1 secondo di pressione per attivare Night Mode
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; // Reset per evitare che il click scatti dopo
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
- // Simulatore su Depth (3 click rapidi)
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(); // Chiama la nuova funzione
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, // Inerzia per il Leeway
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
- // 2. SALTO DI VENTO (dopo 120 secondi)
466
- if (elapsed > 120 && elapsed < 121) {
467
- sim.twd = Math.random() * 360;
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
- // 3. RAFFICA (ogni 40s per 5s) con smoothing 0.05
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; // Smoothing lento
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; // Smoothing STW
501
+ sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
481
502
 
482
- // 5. CALCOLO LEEWAY CON FILTRO (Inerzia)
503
+ // Leeway smussato
483
504
  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
505
+ sim.leeway += (rawLeeway - sim.leeway) * 0.05;
485
506
 
486
- // 6. CALCOLO VETTORIALE COG
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
- // 8. INVIO DATI PULITI
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {