@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.
Files changed (4) hide show
  1. package/app.js +107 -45
  2. package/index.html +4 -1
  3. package/package.json +1 -1
  4. 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
- let drift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
172
- if (curSog < CONFIG.averages.minSpeed) drift = 0;
173
- curTrackRot = getShortestRotation(curTrackRot, drift); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
174
- let ds = drift > 180 ? drift - 360 : drift; updateLeewayDisplay(Math.max(-20, Math.min(20, ds)));
175
- } else updateLeewayDisplay(0);
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) { el.innerHTML = "---&deg;"; el.classList.remove('unstable-data'); }
187
- else {
188
- el.innerHTML = `${obj.val.toString().padStart(3, '0')}&deg;`;
189
- if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data'); else el.classList.add('unstable-data');
203
+ const upUI = (el, obj, isCompass = false) => {
204
+ if (!obj || obj.val === null) {
205
+ el.innerHTML = "---&deg;";
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}&deg;`;
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
- upUI(ui.hdg, hObj); upUI(ui.cog, cObj); upUI(ui.awaAvg, awObj); upUI(ui.twaAvg, twObj); upUI(ui.twdAvg, twdObj);
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')}&deg;`;
@@ -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
- // Parametri base iniziali
412
- let baseTws = 60, baseDepth = 12, baseHdg = 45;
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
- // T rappresenta il progresso nel ciclo di 10 minuti (da 0 a 1)
416
- const t = (Date.now() % 600000) / 600000;
417
- const radT = t * 2 * Math.PI;
418
-
419
- // Oscillazioni fluide (Sinusoidali)
420
- const tws = baseTws + Math.sin(radT) * 5; // Oscilla +- 5 kts
421
- const depth = baseDepth + Math.sin(radT) * 3; // Oscilla +- 3 m
422
- const twa = 40 + Math.sin(radT * 2) * 10; // Oscilla angolo +- 10°
423
-
424
- // Calcoli Vettoriali (Nautica)
425
- const stw = Math.min(tws * 0.7, 8); // Velocità barca legata al vento
426
- const twaRad = degToRad(twa);
427
-
428
- // AWS = radq(stw² + tws² + 2*stw*tws*cos(twa))
429
- const aws = Math.sqrt(Math.pow(stw, 2) + Math.pow(tws, 2) + 2 * stw * tws * Math.cos(twaRad));
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
- // Calcolo Leeway (Scarroccio): più forte il vento, più scarroccia
434
- const leeway = (tws > 5) ? Math.sin(twaRad) * (tws * 0.2) : 0;
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
- // Invio dati al sistema
437
- processIncomingData("environment.wind.speedTrue", ktsToMs(tws));
438
- processIncomingData("environment.wind.angleTrueWater", degToRad(twa));
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(baseHdg));
443
- processIncomingData("navigation.speedThroughWater", ktsToMs(stw));
444
- processIncomingData("navigation.courseOverGroundTrue", degToRad(baseHdg + leeway));
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">---&deg;</span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "2.0.13",
3
+ "version": "2.0.14",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
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 12px;
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
- /* Altezze Proporzionali: 25% dell'altezza per i grafici, 16.6% per i MEAN/TACK */
95
- .data-box:nth-child(1), .data-box:nth-child(2) { height: 25vh; }
96
- .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 16.666vh; }
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: 2px;
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: 18px;
224
- height: 100%; line-height: 1; padding: 2px 0;
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
+ }