@sailingrotevista/rotevista-dash 2.0.13 → 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 (4) hide show
  1. package/app.js +135 -56
  2. package/index.html +4 -1
  3. package/package.json +1 -1
  4. package/style.css +34 -8
package/app.js CHANGED
@@ -42,6 +42,9 @@ 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
 
45
+ // Variabili di stato per filtri e trend
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,12 +203,23 @@ 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
 
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);
206
+ // --- CALCOLO E SMUSSAMENTO LEEWAY ---
207
+ if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
208
+ let rawDrift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
209
+ if (rawDrift > 180) rawDrift -= 360;
210
+
211
+ if (curSog < CONFIG.averages.minSpeed) {
212
+ smoothedLeeway = 0;
213
+ } else {
214
+ smoothedLeeway = (smoothedLeeway * 0.9) + (rawDrift * 0.1);
215
+ }
216
+
217
+ curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
218
+ ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
219
+ updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
220
+ } else {
221
+ updateLeewayDisplay(0);
222
+ }
176
223
 
177
224
  // --- 2. RENDERING MEDIE E BUSSOLA TATTICA ---
178
225
  if (now - lastAvgUIUpdate > 3000) {
@@ -182,14 +229,29 @@ function startDisplayLoop() {
182
229
  twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true),
183
230
  twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
184
231
 
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');
232
+ const upUI = (el, obj, isCompass = false) => {
233
+ if (!obj || obj.val === null) {
234
+ el.innerHTML = "---&deg;";
235
+ el.classList.remove('unstable-data');
236
+ } else {
237
+ let val = Math.round(obj.val);
238
+ let displayVal;
239
+
240
+ if (isCompass) displayVal = ((val + 360) % 360).toString().padStart(3, '0');
241
+ else displayVal = val.toString();
242
+
243
+ el.innerHTML = `${displayVal}&deg;`;
244
+
245
+ if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data');
246
+ else el.classList.add('unstable-data');
190
247
  }
191
248
  };
192
- upUI(ui.hdg, hObj); upUI(ui.cog, cObj); upUI(ui.awaAvg, awObj); upUI(ui.twaAvg, twObj); upUI(ui.twdAvg, twdObj);
249
+
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);
193
255
 
194
256
  if (hObj && twObj && hObj.val !== null) {
195
257
  let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
@@ -201,6 +263,8 @@ function startDisplayLoop() {
201
263
 
202
264
  // Rotazione Bussola Tattica e Colore Sincronizzato
203
265
  if (twdObj && hObj) {
266
+ updateWindTrend(); // Aggiorna i pallini
267
+
204
268
  curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val);
205
269
  ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`);
206
270
  ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`);
@@ -338,16 +402,14 @@ function toggleFocusMode(type, element) {
338
402
  // ==========================================================================
339
403
  if (ui.hotspot) {
340
404
  let pressTimer;
341
- const HOLD_DURATION = 1000; // 1 secondo di pressione per attivare Night Mode
405
+ const HOLD_DURATION = 1000;
342
406
 
343
407
  ui.hotspot.addEventListener('pointerdown', (e) => {
344
- // Avvia il timer al tocco
345
408
  pressTimer = setTimeout(() => {
346
409
  document.body.classList.toggle('night-mode');
347
- // Feedback visivo immediato al cambio modalità
348
410
  ui.hotspot.style.opacity = "0.5";
349
411
  setTimeout(() => ui.hotspot.style.opacity = "1", 200);
350
- pressTimer = null; // Reset per evitare che il click scatti dopo
412
+ pressTimer = null;
351
413
  }, HOLD_DURATION);
352
414
  });
353
415
 
@@ -355,7 +417,6 @@ if (ui.hotspot) {
355
417
  if (pressTimer) {
356
418
  clearTimeout(pressTimer);
357
419
  pressTimer = null;
358
- // Se arriviamo qui, è stato un click rapido -> Fullscreen
359
420
  const doc = document.documentElement;
360
421
  const isF = document.fullscreenElement || document.webkitFullscreenElement;
361
422
  if (!isF) {
@@ -382,7 +443,9 @@ window.addEventListener('load', init);
382
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'); }
383
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); }
384
445
 
385
- // Simulatore su Depth (3 click rapidi)
446
+ // ==========================================================================
447
+ // 9. MOTORE SIMULAZIONE DINAMICA E AVVIO (3 Click su Depth)
448
+ // ==========================================================================
386
449
  ui.depth.closest('.data-box').addEventListener('click', (function() {
387
450
  let dC = 0, lC = 0;
388
451
  return function() {
@@ -393,7 +456,7 @@ ui.depth.closest('.data-box').addEventListener('click', (function() {
393
456
  simulationMode = !simulationMode;
394
457
  if (simulationMode) {
395
458
  if (socket) socket.close();
396
- startDynamicSimulation(); // Chiama la nuova funzione
459
+ startDynamicSimulation();
397
460
  } else {
398
461
  location.reload();
399
462
  }
@@ -402,46 +465,62 @@ ui.depth.closest('.data-box').addEventListener('click', (function() {
402
465
  };
403
466
  })());
404
467
 
405
- // ==========================================================================
406
- // 9. MOTORE SIMULAZIONE DINAMICA
407
- // ==========================================================================
408
468
  function startDynamicSimulation() {
409
469
  ui.status.innerText = "SIM ATTIVO";
410
-
411
- // Parametri base iniziali
412
- let baseTws = 60, baseDepth = 12, baseHdg = 45;
413
-
470
+
471
+ let sim = {
472
+ hdg: Math.random() * 360,
473
+ tws: 12,
474
+ twd: Math.random() * 360,
475
+ depth: 12,
476
+ stw: 5,
477
+ leeway: 0,
478
+ startTime: Date.now()
479
+ };
480
+
414
481
  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)));
432
-
433
- // Calcolo Leeway (Scarroccio): più forte il vento, più scarroccia
434
- const leeway = (tws > 5) ? Math.sin(twaRad) * (tws * 0.2) : 0;
482
+ const elapsed = (Date.now() - sim.startTime) / 1000;
483
+
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;
487
+
488
+ // Raffica
489
+ const inGust = (elapsed % 40) < 5;
490
+ const targetTws = (inGust ? 18 : 10) + Math.sin(elapsed / 10) * 2;
491
+ sim.tws += (targetTws - sim.tws) * 0.05;
435
492
 
436
- // Invio dati al sistema
437
- processIncomingData("environment.wind.speedTrue", ktsToMs(tws));
438
- processIncomingData("environment.wind.angleTrueWater", degToRad(twa));
493
+ // Barca segue il vento pigramente
494
+ sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
495
+
496
+ let twaRel = (sim.twd - sim.hdg + 360) % 360;
497
+ if (twaRel > 180) twaRel -= 360;
498
+
499
+ // Polare smussata
500
+ let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125));
501
+ sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
502
+
503
+ // Leeway smussato
504
+ const rawLeeway = Math.sin(degToRad(twaRel)) * 4;
505
+ sim.leeway += (rawLeeway - sim.leeway) * 0.05;
506
+
507
+ // Vettori Reali
508
+ const cog = (sim.hdg - sim.leeway + 360) % 360;
509
+ const twaRad = degToRad(twaRel);
510
+ const aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
511
+ const awa = radToDeg(Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad)));
512
+
513
+ // Invio Dati (Con tutto il set necessario per la UI)
514
+ processIncomingData("environment.wind.speedTrue", ktsToMs(sim.tws));
515
+ processIncomingData("environment.wind.directionTrue", degToRad(sim.twd));
516
+ processIncomingData("environment.wind.angleTrueWater", degToRad(twaRel));
439
517
  processIncomingData("environment.wind.speedApparent", ktsToMs(aws));
440
518
  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));
519
+ processIncomingData("environment.depth.belowTransducer", sim.depth);
520
+ processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
521
+ processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw));
522
+ processIncomingData("navigation.speedOverGround", ktsToMs(sim.stw));
523
+ processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
524
+
445
525
  }, 1000);
446
526
  }
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.15",
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
+ }