@sailingrotevista/rotevista-dash 6.2.8 → 7.0.1

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/radar.html CHANGED
@@ -112,18 +112,28 @@
112
112
  }
113
113
 
114
114
  .status-badge {
115
- display: inline-block;
116
- padding: 2px 6px;
117
- border-radius: 3px;
118
- font-weight: bold;
119
- color: #fff;
120
- text-transform: uppercase;
121
- font-size: 9px;
122
- }
123
- .status-online { background: #00C851; }
124
- .status-offline { background: #ff3b30; }
125
- </style>
126
- </head>
115
+ display: inline-block;
116
+ padding: 2px 6px;
117
+ border-radius: 3px;
118
+ font-weight: bold;
119
+ color: #fff;
120
+ text-transform: uppercase;
121
+ font-size: 9px;
122
+ }
123
+ .status-online { background: #00C851; }
124
+ .status-offline { background: #ff3b30; }
125
+
126
+ /* Chirurgico: Animazione e stile per la punta infuocata del trend meteo */
127
+ @keyframes blink-trend {
128
+ 0%, 100% { opacity: 1; filter: drop-shadow(0 0 6px currentColor); }
129
+ 50% { opacity: 0.3; filter: drop-shadow(0 0 1px currentColor); }
130
+ }
131
+ .is-trending {
132
+ animation: blink-trend 1.2s infinite ease-in-out !important;
133
+ transform-origin: center;
134
+ }
135
+ </style>
136
+ </head>
127
137
  <body>
128
138
 
129
139
  <!-- AREA SUPERIORE: BUSSOLA RADAR TATTICA -->
@@ -251,10 +261,74 @@
251
261
  const BORDER_STROKE_WIDTH = ARC_STROKE_WIDTH + 2; // 7px
252
262
 
253
263
  const radToDeg = (rad) => (rad * 180 / Math.PI);
254
- const degToRad = (deg) => (deg * Math.PI / 180);
255
- const msToKts = (ms) => ms * 1.94384;
264
+ const degToRad = (deg) => (deg * Math.PI / 180);
265
+ const msToKts = (ms) => ms * 1.94384;
266
+
267
+ // Chirurgico: Inserita la funzione vettoriale circolare con calcolo trigonometrico al volo per l'analisi dei trend meteo
268
+ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
269
+ now = now || Date.now();
270
+ const len = bufferArray.length;
271
+ if (len === 0) return null;
272
+
273
+ let sSin = 0, sCos = 0, count = 0;
274
+ let newestTime = 0, oldestTime = 0;
275
+
276
+ let pilotSin = 0, pilotCos = 0;
277
+ const pilotSamples = Math.min(len, 15);
278
+ for (let i = len - 1; i >= len - pilotSamples; i--) {
279
+ const val = bufferArray[i].val;
280
+ pilotSin += Math.sin(val);
281
+ pilotCos += Math.cos(val);
282
+ }
283
+ const pilotRad = Math.atan2(pilotSin, pilotCos);
284
+ const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
285
+
286
+ for (let i = len - 1; i >= 0; i--) {
287
+ const item = bufferArray[i];
288
+ if ((now - item.time) > windowMs) break;
289
+
290
+ const itemVal = item.val;
291
+ const itemSin = Math.sin(itemVal);
292
+ const itemCos = Math.cos(itemVal);
293
+
294
+ let diffRad = Math.atan2(Math.sin(itemVal - pilotRad), Math.cos(itemVal - pilotRad));
295
+ let finalSin, finalCos;
296
+
297
+ if (Math.abs(diffRad) > limitRad) {
298
+ const clampedDiff = Math.sign(diffRad) * limitRad;
299
+ const clampedRad = pilotRad + clampedDiff;
300
+ finalSin = Math.sin(clampedRad);
301
+ finalCos = Math.cos(clampedRad);
302
+ } else {
303
+ finalSin = itemSin;
304
+ finalCos = itemCos;
305
+ }
306
+
307
+ sSin += finalSin;
308
+ sCos += finalCos;
309
+
310
+ if (count === 0) newestTime = item.time;
311
+ oldestTime = item.time;
312
+ count++;
313
+ }
314
+
315
+ if (count === 0) return null;
316
+
317
+ const R = Math.hypot(sSin, sCos) / count;
318
+ const avgRad = Math.atan2(sSin, sCos);
319
+ const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
320
+ const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
321
+ const safeR = Math.max(R, 1e-9);
322
+
323
+ return {
324
+ val: finalVal,
325
+ stable: historyDuration > 10000 && R > (CONFIG.averaging.stabilityThreshold || 0.95),
326
+ dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
327
+ samples: count
328
+ };
329
+ }
256
330
 
257
- function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
331
+ function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
258
332
  const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
259
333
  return {
260
334
  x: centerX + (radius * Math.cos(angleInRadians)),
@@ -278,51 +352,56 @@
278
352
  }
279
353
 
280
354
  // --- 2. GESTIONE SEGNALI IN TEMPO REALE ---
281
- function processIncomingDelta(path, val, source, timeMs) {
282
- const now = timeMs || Date.now();
283
- store.timestamps[path] = now;
284
- store.raw[path] = val;
285
-
286
- if (path === 'navigation.speedThroughWater') document.getElementById('debug-stw').innerText = msToKts(val).toFixed(1);
287
- else if (path === 'navigation.speedOverGround') document.getElementById('debug-sog').innerText = msToKts(val).toFixed(1);
288
- else if (path === 'navigation.headingTrue') document.getElementById('debug-hdg').innerText = Math.round(radToDeg(val)) + '°';
289
- else if (path === 'navigation.courseOverGroundTrue') document.getElementById('debug-cog').innerText = Math.round(radToDeg(val)) + '°';
290
- else if (path === 'environment.wind.speedApparent') document.getElementById('debug-aws').innerText = msToKts(val).toFixed(1);
291
- else if (path === 'environment.wind.angleApparent') document.getElementById('debug-awa').innerText = Math.round(radToDeg(val)) + '°';
292
- else if (path === 'navigation.position') {
293
- document.getElementById('debug-gps').innerText = val.latitude.toFixed(4) + '; ' + val.longitude.toFixed(4);
294
- }
355
+ function setDebugText(id, text) {
356
+ const el = document.getElementById(id);
357
+ if (el) el.innerText = text;
358
+ }
295
359
 
296
- const aws = store.raw["environment.wind.speedApparent"];
297
- const awa = store.raw["environment.wind.angleApparent"];
298
- if (aws !== undefined && awa !== undefined) {
299
- const stw = store.raw["navigation.speedThroughWater"] || 0;
300
- const awsKts = msToKts(aws);
301
- const stwKts = msToKts(stw);
360
+ function processIncomingDelta(path, val, source, timeMs) {
361
+ const now = timeMs || Date.now();
362
+ store.timestamps[path] = now;
363
+ store.raw[path] = val;
364
+
365
+ if (path === 'navigation.speedThroughWater') setDebugText('debug-stw', msToKts(val).toFixed(1));
366
+ else if (path === 'navigation.speedOverGround') setDebugText('debug-sog', msToKts(val).toFixed(1));
367
+ else if (path === 'navigation.headingTrue') setDebugText('debug-hdg', Math.round(radToDeg(val)) + '°');
368
+ else if (path === 'navigation.courseOverGroundTrue') setDebugText('debug-cog', Math.round(radToDeg(val)) + '°');
369
+ else if (path === 'environment.wind.speedApparent') setDebugText('debug-aws', msToKts(val).toFixed(1));
370
+ else if (path === 'environment.wind.angleApparent') setDebugText('debug-awa', Math.round(radToDeg(val)) + '°');
371
+ else if (path === 'navigation.position') {
372
+ setDebugText('debug-gps', val.latitude.toFixed(4) + '; ' + val.longitude.toFixed(4));
373
+ }
302
374
 
303
- const tw_water_x = awsKts * Math.cos(awa) - stwKts;
304
- const tw_water_y = awsKts * Math.sin(awa);
305
-
306
- const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
307
- const twa = Math.atan2(tw_water_y, tw_water_x);
308
- const hdg = store.raw["navigation.headingTrue"] || 0;
309
- const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
375
+ const aws = store.raw["environment.wind.speedApparent"];
376
+ const awa = store.raw["environment.wind.angleApparent"];
377
+ if (aws !== undefined && awa !== undefined) {
378
+ const stw = store.raw["navigation.speedThroughWater"] || 0;
379
+ const awsKts = msToKts(aws);
380
+ const stwKts = msToKts(stw);
310
381
 
311
- document.getElementById('debug-tws').innerText = tws.toFixed(1);
312
- document.getElementById('debug-twa').innerText = Math.round(radToDeg(twa)) + '°';
313
- document.getElementById('debug-twd').innerText = Math.round(radToDeg(twd)) + '°';
382
+ const tw_water_x = awsKts * Math.cos(awa) - stwKts;
383
+ const tw_water_y = awsKts * Math.sin(awa);
384
+
385
+ const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
386
+ const twa = Math.atan2(tw_water_y, tw_water_x);
387
+ const hdg = store.raw["navigation.headingTrue"] || 0;
388
+ const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
314
389
 
315
- // --- ACCUMULO DATI REALI LOCALE (1Hz) ---
316
- // Popoliamo i buffer locali ogni secondo per garantire il disegno immediato del Presente
317
- store.twdMinuteBuffer.push({ val: twd, min: twd, max: twd, time: now });
318
- while(store.twdMinuteBuffer.length > 1800) store.twdMinuteBuffer.shift(); // conserva 30 minuti a 1Hz
390
+ setDebugText('debug-tws', tws.toFixed(1));
391
+ setDebugText('debug-twa', Math.round(radToDeg(twa)) + '°');
392
+ setDebugText('debug-twd', Math.round(radToDeg(twd)) + '°');
319
393
 
320
- store.twsMinuteBuffer.push({ val: tws, time: now });
321
- while(store.twsMinuteBuffer.length > 1800) store.twsMinuteBuffer.shift();
394
+ // --- AGGIORNAMENTO DATI ISTANTANEI PER L'ELABORAZIONE ---
395
+ // Manteniamo aggiornato lo stato raw istantaneo in memoria per i calcoli del radar...
396
+ store.raw["environment.wind.directionTrue"] = twd;
397
+ store.raw["environment.wind.speedTrue"] = tws;
322
398
 
323
- renderRadar(); // Rinfresco grafico istantaneo reattivo
324
- }
325
- }
399
+ // NOTA: Non popoliamo più i buffer locali a 1Hz. Il presente dei 30 minuti (Anello 1)
400
+ // viene gestito e sincronizzato direttamente dal server tramite fetchConfigAndHistory().
401
+
402
+ renderRadar(); // Rinfresco grafico istantaneo reattivo per gli indicatori/trend
403
+ }
404
+ }
326
405
 
327
406
  // --- 3. RETE E SINCRONIZZAZIONE REST ---
328
407
  function getApiUrl(path) {
@@ -750,28 +829,58 @@
750
829
  mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
751
830
  mainPath.setAttribute("stroke-linecap", "round");
752
831
  // Chirurgico: Per l'anello futuro, usiamo un'opacità morbida al 50% (0.5) continua (senza tratteggio) per un effetto "glowing" pulitissimo
753
- mainPath.setAttribute("opacity", data.isFuture ? "0.5" : opacityValue);
754
- ringsContainer.appendChild(mainPath);
755
- });
756
-
757
- // Aggiorna il pannello di debug ad ogni ridisegno del radar
758
- updateDebugPanel();
759
- }
832
+ mainPath.setAttribute("opacity", data.isFuture ? "0.5" : opacityValue);
833
+ mainPath.id = data.isFuture ? "" : (index === 1 ? "active-present-arc" : ""); // Identifichiamo l'Anello 1 per calcoli geometrici
834
+ ringsContainer.appendChild(mainPath);
835
+ });
836
+
837
+ // --- Chirurgico: Motore di calcolo e posizionamento LED dinamici sulle punte dell'Anello 1 ---
838
+ if (activeRing && !activeRing.isCalm) {
839
+ const twdNow = getCircularAverageFromBuffer(store.twdMinuteBuffer, 60000, false, now); // 1 minuto fisso
840
+ const isNavigating = (store.raw["navigation.speedThroughWater"] || 0) > 0.25 || (store.raw["navigation.speedOverGround"] || 0) > 0.25;
841
+ const strategicWindowMs = (isNavigating ? 15 : 60) * 60000;
842
+ const twdRef = getCircularAverageFromBuffer(store.twdMinuteBuffer, strategicWindowMs, false, now);
843
+
844
+ if (twdNow && twdRef) {
845
+ let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
846
+
847
+ if (Math.abs(deltaMeteo) > 6.0) {
848
+ const isSouth = store.raw["navigation.position"] && store.raw["navigation.position"].latitude < 0;
849
+ let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#00C851" : "#ff3b30") : (deltaMeteo > 0 ? "#00C851" : "#ff3b30");
850
+
851
+ // Calcoliamo la posizione geometrica esatta delle due punte dell'Anello 1 (raggio 67.2px)
852
+ const radiusAnello1 = ringRadii[1];
853
+ const angleTarget = deltaMeteo > 0 ? activeRing.twdMax : activeRing.twdMin; // Estremità destra (CW) o sinistra (CCW)
854
+ const pt = polarToCartesian(200, 200, radiusAnello1, angleTarget);
855
+
856
+ // Disegniamo il LED lampeggiante esattamente sulla punta dell'arco
857
+ const led = document.createElementNS("http://www.w3.org/2000/svg", "circle");
858
+ led.setAttribute("cx", pt.x.toFixed(1));
859
+ led.setAttribute("cy", pt.y.toFixed(1));
860
+ led.setAttribute("r", "5.5"); // Leggermente più spesso dell'arco per renderlo evidente
861
+ led.setAttribute("fill", meteoColor);
862
+ led.setAttribute("class", "is-trending"); // Attiva il lampeggio CSS
863
+ led.setAttribute("filter", "url(#center-glow)"); // Aggiunge un leggero bagliore visivo
864
+ ringsContainer.appendChild(led);
865
+ }
866
+ }
867
+ }
868
+
869
+ // Aggiorna il pannello di debug ad ogni ridisegno del radar
870
+ updateDebugPanel();
871
+ }
760
872
 
761
873
  // --- 6. CICLO DI VITA E AVVIO ---
762
- function init() {
763
- drawCompassTicks();
764
- connect();
765
-
766
- // Sincronizzazione strategica: scarica lo storico consolidato dal server ogni 30 secondi e ridisegna
767
- setInterval(async () => {
768
- await fetchConfigAndHistory();
769
- renderRadar();
770
- }, 30000);
771
- }
772
-
773
- window.onload = init;
774
-
874
+ function init() {
875
+ drawCompassTicks();
876
+ connect();
877
+
878
+ // Sincronizzazione strategica: scarica lo storico consolidato dal server ogni 15 secondi e ridisegna
879
+ setInterval(async () => {
880
+ await fetchConfigAndHistory();
881
+ renderRadar();
882
+ }, 15000);
883
+ }
775
884
  window.onload = init;
776
885
  </script>
777
886
  </body>
package/utils.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * ==========================================================================
3
+ * Sailing Dashboard Pro - Math, Conversions & Audio Utilities
4
+ * ==========================================================================
5
+ * Raccoglie le funzioni pure di calcolo vettoriale e sintesi sonora.
6
+ */
7
+
8
+ // --- 1. CONVERSIONI STANDARD ---
9
+ const radToDeg = (rad) => rad * (180 / Math.PI);
10
+ const degToRad = (deg) => deg * (Math.PI / 180);
11
+ const msToKts = (ms) => ms * 1.94384;
12
+ const ktsToMs = (kts) => kts / 1.94384;
13
+
14
+ // Calcola la rotta di rotazione più breve tra due angoli (evita scatti a 360°)
15
+ function getShortestRotation(curr, target) {
16
+ let diff = (target - curr) % 360;
17
+ if (diff > 180) diff -= 360;
18
+ else if (diff < -180) diff += 360;
19
+ return curr + diff;
20
+ }
21
+
22
+ // Scrive testo in sicurezza evitando reflow inutili nel DOM/SVG
23
+ function safeSetText(el, text) {
24
+ if (!el) return;
25
+ const isSVG = el instanceof SVGElement;
26
+ if (isSVG) {
27
+ if (el.textContent !== text) el.textContent = text;
28
+ } else {
29
+ if (el.innerHTML !== text) el.innerHTML = text;
30
+ }
31
+ }
32
+
33
+ // --- 2. MOTORE MATEMATICO: MEDIA CIRCOLARE VETTORIALE ---
34
+ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now, stabilityThreshold = 0.95, stabilityBreakout = 15) {
35
+ now = now || Date.now();
36
+ const len = bufferArray.length;
37
+ if (len === 0) return null;
38
+
39
+ let sSin = 0, sCos = 0, count = 0;
40
+ let newestTime = 0, oldestTime = 0;
41
+
42
+ let pilotSin = 0, pilotCos = 0;
43
+ const pilotSamples = Math.min(len, 15);
44
+ for (let i = len - 1; i >= len - pilotSamples; i--) {
45
+ pilotSin += bufferArray[i].sin;
46
+ pilotCos += bufferArray[i].cos;
47
+ }
48
+ const pilotRad = Math.atan2(pilotSin, pilotCos);
49
+ const limitRad = stabilityBreakout * (Math.PI / 180);
50
+
51
+ for (let i = len - 1; i >= 0; i--) {
52
+ const item = bufferArray[i];
53
+ if ((now - item.time) > windowMs) break;
54
+
55
+ let diffRad = Math.atan2(Math.sin(item.val - pilotRad), Math.cos(item.val - pilotRad));
56
+ let finalSin, finalCos;
57
+
58
+ if (Math.abs(diffRad) > limitRad) {
59
+ const clampedDiff = Math.sign(diffRad) * limitRad;
60
+ const clampedRad = pilotRad + clampedDiff;
61
+ finalSin = Math.sin(clampedRad);
62
+ finalCos = Math.cos(clampedRad);
63
+ } else {
64
+ finalSin = item.sin;
65
+ finalCos = item.cos;
66
+ }
67
+
68
+ sSin += finalSin;
69
+ sCos += finalCos;
70
+
71
+ if (count === 0) newestTime = item.time;
72
+ oldestTime = item.time;
73
+ count++;
74
+ }
75
+
76
+ if (count === 0) return null;
77
+
78
+ const R = Math.hypot(sSin, sCos) / count;
79
+ const avgRad = Math.atan2(sSin, sCos);
80
+ const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
81
+ const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
82
+ const safeR = Math.max(R, 1e-9);
83
+
84
+ return {
85
+ val: finalVal,
86
+ stable: historyDuration > 10000 && R > stabilityThreshold,
87
+ dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
88
+ samples: count
89
+ };
90
+ }
91
+
92
+ // --- 3. SINTESI AUDIO WEB AUDIO API (ALLARMI ACUSTICI) ---
93
+ let audioCtx = null;
94
+ let lastAlarmTime = 0;
95
+
96
+ function playBingBing() {
97
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
98
+ const n = Date.now();
99
+ if (n - lastAlarmTime < 3000) return;
100
+ lastAlarmTime = n;
101
+
102
+ function b(f, s) {
103
+ const o = audioCtx.createOscillator();
104
+ const g = audioCtx.createGain();
105
+ o.connect(g);
106
+ g.connect(audioCtx.destination);
107
+ o.frequency.value = f;
108
+ g.gain.setValueAtTime(0.1, s);
109
+ g.gain.exponentialRampToValueAtTime(0.01, s + 0.4);
110
+ o.start(s);
111
+ o.stop(s + 0.5);
112
+ }
113
+ b(880, audioCtx.currentTime);
114
+ b(880, audioCtx.currentTime + 0.6);
115
+ }
116
+
117
+ function playGybeAlarm() {
118
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
119
+ const n = Date.now();
120
+ if (n - lastAlarmTime < 2000) return;
121
+ lastAlarmTime = n;
122
+
123
+ function note(f, s, d) {
124
+ const o = audioCtx.createOscillator();
125
+ const g = audioCtx.createGain();
126
+ o.connect(g);
127
+ g.connect(audioCtx.destination);
128
+ o.type = 'square';
129
+ o.frequency.value = f;
130
+ g.gain.setValueAtTime(0.05, s);
131
+ g.gain.exponentialRampToValueAtTime(0.001, s + d);
132
+ o.start(s);
133
+ o.stop(s + d);
134
+ }
135
+ for (let i = 0; i < 4; i++) {
136
+ note(1800, audioCtx.currentTime + (i * 0.15), 0.1);
137
+ note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1);
138
+ }
139
+ }