@sailingrotevista/rotevista-dash 7.0.1 → 7.0.3

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 CHANGED
@@ -7,6 +7,7 @@
7
7
  * Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
8
8
  * Memoria UI persistente, Modalità Hercules, Focus Split Screen e
9
9
  * Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
10
+ *file app.js
10
11
  */
11
12
 
12
13
  // ==========================================================================
@@ -29,7 +30,7 @@ let CONFIG = {
29
30
  tws: { stdMax: 15, hercSpan: 2, step: 1 },
30
31
  depth: { stdMax: 5, hercSpan: 2, step: 1 }
31
32
  },
32
- server: { fallbackIp: "192.168.111.240:3000" }
33
+ server: { fallbackIp: "venus.local:3000" }
33
34
  };
34
35
 
35
36
  const RENDER_INTERVAL_MS = 1000;
@@ -43,6 +44,7 @@ const DASH_VERSION = "6.0"; // Major Update: Server-Side History RAM Logging (Pr
43
44
  let simulationMode = false;
44
45
  let displayModeSog = 'SOG';
45
46
  let displayModeTws = 'TWS';
47
+ let activeInstrument = 'gauge'; // Modalità di default all'avvio: 'gauge' (analogico) o 'radar' (storico)
46
48
  let socket, renderInterval, simInterval;
47
49
  let lastAvgUIUpdate = 0; // Chirurgico: Rimosse audioCtx e lastAlarmTime poiché sono già dichiarate in utils.js
48
50
 
@@ -663,13 +665,15 @@ function startDisplayLoop() {
663
665
  }
664
666
  }
665
667
 
666
- // --- AGGIORNAMENTO DELLA BUSSOLA CENTRALE E MINI-COMPASS ---
667
- updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
668
-
669
- // --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
670
- if (lastAvgUIUpdate++ % 10 === 0) {
671
- saveDashboardState();
672
- }
668
+ // --- AGGIORNAMENTO DELLA BUSSOLA CENTRALE (BATTERY SAVER A 1Hz) ---
669
+ if (activeInstrument === 'gauge') {
670
+ updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
671
+ }
672
+
673
+ // --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
674
+ if (lastAvgUIUpdate++ % 10 === 0) {
675
+ saveDashboardState();
676
+ }
673
677
 
674
678
  if (lastAvgUIUpdate % 3 === 0) {
675
679
  let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
@@ -801,7 +805,7 @@ async function watchConfigChanges() {
801
805
  }
802
806
 
803
807
  /**
804
- * Recupera lo storico dei grafici pre-popolato dal server Signal K (Pro v6.0)
808
+ * Recupera lo storico dei grafici e dei radar pre-popolato dal server Signal K (Pro v6.0)
805
809
  */
806
810
  async function fetchServerHistory() {
807
811
  try {
@@ -815,6 +819,12 @@ async function fetchServerHistory() {
815
819
  store.histories[key] = data[key];
816
820
  }
817
821
  }
822
+ // Sincronizza i dati specifici del radar storici, previsionali e i buffer minuto per minuto
823
+ if (data.windRadarSlots) store.windRadarSlots = data.windRadarSlots;
824
+ if (data.futureForecast) store.futureForecast = data.futureForecast;
825
+ if (data.twd) store.twdMinuteBuffer = data.twd;
826
+ if (data.tws) store.twsMinuteBuffer = data.tws;
827
+
818
828
  // --- SILLABAZIONE STRATEGICA DELLA BUSSOLA METEO (TWD) ---
819
829
  // Se il server ci invia lo storico del TWD, lo inseriamo calcolando i seni e coseni per i vettori
820
830
  if (data.twd && data.twd.length > 0) {
@@ -825,7 +835,7 @@ async function fetchServerHistory() {
825
835
  cos: Math.cos(p.val)
826
836
  }));
827
837
  console.log(`📈 Memoria strategica TWD sincronizzata dal server (${data.twd.length} punti).`);
828
- }
838
+ }
829
839
  console.log("📈 Storico dei grafici pre-popolato caricato dal server.");
830
840
  }
831
841
  } catch (err) {
@@ -1100,29 +1110,101 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
1100
1110
 
1101
1111
  async function init() {
1102
1112
  loadDashboardState();
1103
- initCompassTicks(); // Genera i ticks sul quadrante usando il modulo gauge.js
1104
1113
 
1114
+ // Disegna la grafica statica delle tacche di calibrazione di entrambi gli strumenti
1115
+ initCompassTicks(); // Tacche del Wind Gauge analogico (gauge.js)
1116
+ initRadarTicks(); // Tacche del Wind Radar storico (weather-radar.js)
1117
+
1118
+ // 1. COMANDO TATTICO: Gestore Box TWD (Pressione prolungata -> Radar | Tocco rapido in modalità Radar -> Torna a Gauge)
1119
+ const twdBox = document.querySelector('.box-twd');
1120
+ if (twdBox) {
1121
+ let twdPressTimer = null;
1122
+ let longPressTriggered = false; // Flag di controllo della pressione prolungata
1123
+
1124
+ twdBox.addEventListener('pointerdown', (e) => {
1125
+ longPressTriggered = false;
1126
+ if (activeInstrument === 'gauge') {
1127
+ twdPressTimer = setTimeout(() => {
1128
+ activeInstrument = 'radar';
1129
+ document.getElementById('wind-gauge').style.display = 'none';
1130
+ document.getElementById('wind-radar').style.display = 'block';
1131
+ renderRadar(); // Disegna immediatamente il radar all'attivazione
1132
+ twdPressTimer = null;
1133
+ longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
1134
+ }, 1000);
1135
+ }
1136
+ });
1137
+ twdBox.addEventListener('pointerup', () => {
1138
+ if (activeInstrument === 'gauge') {
1139
+ if (twdPressTimer) {
1140
+ clearTimeout(twdPressTimer);
1141
+ twdPressTimer = null;
1142
+ }
1143
+ } else if (activeInstrument === 'radar') {
1144
+ if (longPressTriggered) {
1145
+ // Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
1146
+ longPressTriggered = false;
1147
+ } else {
1148
+ // Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
1149
+ activeInstrument = 'gauge';
1150
+ document.getElementById('wind-radar').style.display = 'none';
1151
+ document.getElementById('wind-gauge').style.display = 'block';
1152
+ }
1153
+ }
1154
+ });
1155
+ twdBox.addEventListener('pointerleave', () => {
1156
+ if (twdPressTimer) {
1157
+ clearTimeout(twdPressTimer);
1158
+ twdPressTimer = null;
1159
+ }
1160
+ longPressTriggered = false;
1161
+ });
1162
+ }
1163
+
1164
+ // 2. COMANDO TATTICO: Click in qualsiasi punto del radar per tornare all'analogico
1165
+ const windRadarSvg = document.getElementById('wind-radar');
1166
+ if (windRadarSvg) {
1167
+ windRadarSvg.addEventListener('pointerup', () => {
1168
+ if (activeInstrument === 'radar') {
1169
+ activeInstrument = 'gauge';
1170
+ windRadarSvg.style.display = 'none';
1171
+ document.getElementById('wind-gauge').style.display = 'block';
1172
+ }
1173
+ });
1174
+ }
1175
+
1105
1176
  // Rileviamo se siamo sul Mac tramite file:// (Ambiente di sviluppo locale)
1106
1177
  const isLocalFile = (window.location.protocol === 'file:');
1107
1178
 
1108
- // 1. CARICAMENTO STORICO GRAFICI REALI DAL CERBO GX
1179
+ // 3. CARICAMENTO STORICO GRAFICI E RADAR REALI DAL CERBO GX
1109
1180
  try {
1110
1181
  await fetchServerHistory();
1111
1182
  } catch (err) {
1112
1183
  console.warn("⚠️ Impossibile caricare lo storico reale dal server.");
1113
1184
  }
1114
1185
 
1115
- // 2. CARICAMENTO CONFIGURAZIONI REALI (Bypassato su Mac per preservare i tuoi test!)
1186
+ // 4. CARICAMENTO CONFIGURAZIONI REALI (Bypassato su Mac per preservare i tuoi test!)
1116
1187
  if (!isLocalFile) {
1117
1188
  await fetchServerConfig();
1118
1189
  } else {
1119
- // Mantiene la CONFIG locale di app.js per farti fare le prove delle scale sul Mac
1120
1190
  console.log("🎮 Esecuzione locale file://: utilizzo delle calibrazioni di CONFIG locali di debug.");
1121
1191
  }
1122
1192
 
1123
1193
  startDisplayLoop();
1124
1194
  connect(); // Si collegherà in tempo reale al WebSocket reale della barca
1125
1195
 
1196
+ // 5. POLL LENTO (15 secondi): aggiorna i dati radar in background e, se attivo, li ridisegna
1197
+ setInterval(async () => {
1198
+ try {
1199
+ await fetchServerHistory();
1200
+ if (activeInstrument === 'radar') {
1201
+ renderRadar();
1202
+ }
1203
+ } catch (err) {
1204
+ console.warn("⚠️ Errore aggiornamento periodico storico:", err);
1205
+ }
1206
+ }, 15000);
1207
+
1126
1208
  // Controlla le modifiche di configurazione sul Cerbo solo se non siamo sul Mac via file://
1127
1209
  if (!isLocalFile) {
1128
1210
  setInterval(watchConfigChanges, 10000);
package/charts.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * ==========================================================================
6
6
  * Gestisce l'adattamento delle scale dei grafici, l'arrotondamento dei limiti
7
7
  * (Snap a griglia) e la generazione dinamica delle curve SVG.
8
+ * file charts.js
8
9
  */
9
10
 
10
11
  // --- 1. MOTORE DI CALCOLO DELLE SCALE (Snap a griglia & Protezione Profondità) ---
package/gauge.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * ==========================================================================
5
5
  * Gestisce l'aggiornamento grafico dei puntatori analogici (AWA, TWA),
6
6
  * dello scarroccio (Leeway), della rotta (Track) e dei trend della bussola.
7
+ * file gauge,js
7
8
  */
8
9
 
9
10
  // 1. VARIABILI DI STATO DELLE ROTAZIONI (Estratte da app.js)
package/index.html CHANGED
@@ -63,104 +63,144 @@
63
63
  </div>
64
64
 
65
65
  <!-- BUSSOLA CENTRALE -->
66
- <div class="box-gauge">
67
- <!-- STATO CONNESSIONE -->
68
- <div id="status" class="offline">OFFLINE</div>
69
- <svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
70
- <defs>
71
- <clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
72
- <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
73
- <stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
74
- <stop offset="100%" style="stop-color:#888888;stop-opacity:1" />
75
- </linearGradient>
76
- <linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%">
77
- <stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
78
- <stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" />
79
- <stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" />
80
- <stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" />
81
- <stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
82
- </linearGradient>
83
- <linearGradient id="leeway-night-grad" x1="0%" y1="0%" x2="100%" y2="0%">
84
- <stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
85
- <stop offset="50%" style="stop-color:#330000;stop-opacity:1" />
86
- <stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
87
- </linearGradient>
88
- <clipPath id="leeway-clip">
89
- <rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" />
90
- </clipPath>
91
- <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
92
- <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
93
- </filter>
94
- </defs>
95
-
96
- <circle cx="200" cy="200" r="160" fill="#050505" />
97
- <circle cx="200" cy="200" r="125" fill="#121212" />
98
-
99
- <path d="M 82.0 101.0 A 154 154 0 0 1 142.3 57.2" fill="none" stroke="#ff0000" stroke-width="12" />
100
- <path d="M 257.7 57.2 A 154 154 0 0 1 318.0 101.0" fill="none" stroke="#00ff00" stroke-width="12" />
101
- <path d="M 265.1 339.6 A 154 154 0 0 1 134.9 339.6" fill="none" stroke="#ff8800" stroke-width="12" />
102
-
103
- <g id="ticks"></g>
104
-
105
- <g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
106
- <text font-size="16" transform="translate(200, 74)">0</text>
107
- <text font-size="16" transform="translate(326, 200) rotate(90)">90</text>
108
- <text font-size="16" transform="translate(74, 200) rotate(-90)">90</text>
109
- <text font-size="16" transform="translate(200, 326) rotate(180)">180</text>
110
- <text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
111
- <text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
112
- <text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
113
- <text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
114
- <text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
115
- <text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
116
- <text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
117
- <text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
118
- </g>
119
-
120
- <g id="track-pointer" transform="rotate(0, 200, 200)">
121
- <path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" />
122
- </g>
123
-
124
- <circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
125
-
126
- <path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
127
- fill="url(#axiom-grad)" transform="translate(0, 5)" clip-path="url(#boat-clip)" style="pointer-events: none;" />
128
-
129
- <g id="aws-display-group" transform="translate(200, 265)">
130
- <text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
131
- <text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
132
- </g>
133
-
134
- <g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
135
- <path d="M 200,70 L 213,95 L 200,145 L 187,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" />
136
- <text x="200" y="90" fill="#000" font-size="10" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
137
- </g>
138
-
139
- <g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
140
- <path d="M 200,92 A 8,8 0 0 1 208,100 C 208,108 200,128 200,128 C 200,128 192,108 192,100 A 8,8 0 0 1 200,92 Z"
141
- fill="#ffff00" stroke="#000" stroke-width="0.8" />
142
- <text x="200" y="106" fill="#000" font-size="9" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
143
- <circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#bbb" />
144
- <circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#bbb" />
145
- </g>
146
-
147
- <g transform="translate(75, 395)">
148
- <text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0&deg;</text>
149
- <rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
150
- <rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
151
- <g stroke="#555" stroke-width="1">
152
- <line x1="0" y1="-2" x2="0" y2="14" /><line x1="62.5" y1="2" x2="62.5" y2="10" />
153
- <line x1="125" y1="-2" x2="125" y2="14" /><line x1="187.5" y1="2" x2="187.5" y2="10" />
154
- <line x1="250" y1="-2" x2="250" y2="14" />
155
- </g>
156
- <g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
157
- <text x="0" y="24">-20&deg;</text><text x="62.5" y="24">-10</text>
158
- <text x="125" y="24">0&deg;</text><text x="187.5" y="24">10</text>
159
- <text x="250" y="24">20&deg;</text>
160
- </g>
161
- </g>
162
- </svg>
163
- </div>
66
+ <div class="box-gauge">
67
+ <!-- STATO CONNESSIONE -->
68
+ <div id="status" class="offline">OFFLINE</div>
69
+
70
+ <!-- BUSSOLA 1: ANALOGICA STANDARD (Visibile all'avvio) -->
71
+ <svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
72
+ <defs>
73
+ <clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
74
+ <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
75
+ <stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
76
+ <stop offset="100%" style="stop-color:#888888;stop-opacity:1" />
77
+ </linearGradient>
78
+ <linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%">
79
+ <stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
80
+ <stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" />
81
+ <stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" />
82
+ <stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" />
83
+ <stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
84
+ </linearGradient>
85
+ <linearGradient id="leeway-night-grad" x1="0%" y1="0%" x2="100%" y2="0%">
86
+ <stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
87
+ <stop offset="50%" style="stop-color:#330000;stop-opacity:1" />
88
+ <stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
89
+ </linearGradient>
90
+ <clipPath id="leeway-clip">
91
+ <rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" />
92
+ </clipPath>
93
+ <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
94
+ <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
95
+ </filter>
96
+ </defs>
97
+
98
+ <circle cx="200" cy="200" r="160" fill="#050505" />
99
+ <circle cx="200" cy="200" r="125" fill="#121212" />
100
+
101
+ <path d="M 82.0 101.0 A 154 154 0 0 1 142.3 57.2" fill="none" stroke="#ff0000" stroke-width="12" />
102
+ <path d="M 257.7 57.2 A 154 154 0 0 1 318.0 101.0" fill="none" stroke="#00ff00" stroke-width="12" />
103
+ <path d="M 265.1 339.6 A 154 154 0 0 1 134.9 339.6" fill="none" stroke="#ff8800" stroke-width="12" />
104
+
105
+ <g id="ticks"></g>
106
+
107
+ <g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
108
+ <text font-size="16" transform="translate(200, 74)">0</text>
109
+ <text font-size="16" transform="translate(326, 200) rotate(90)">90</text>
110
+ <text font-size="16" transform="translate(74, 200) rotate(-90)">90</text>
111
+ <text font-size="16" transform="translate(200, 326) rotate(180)">180</text>
112
+ <text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
113
+ <text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
114
+ <text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
115
+ <text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
116
+ <text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
117
+ <text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
118
+ <text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
119
+ <text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
120
+ </g>
121
+
122
+ <g id="track-pointer" transform="rotate(0, 200, 200)">
123
+ <path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" />
124
+ </g>
125
+
126
+ <circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
127
+
128
+ <path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
129
+ fill="url(#axiom-grad)" transform="translate(0, 5)" clip-path="url(#boat-clip)" style="pointer-events: none;" />
130
+
131
+ <g id="aws-display-group" transform="translate(200, 265)">
132
+ <text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
133
+ <text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
134
+ </g>
135
+
136
+ <g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
137
+ <path d="M 200,70 L 213,95 L 200,145 L 187,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" />
138
+ <text x="200" y="90" fill="#000" font-size="10" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
139
+ </g>
140
+
141
+ <g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
142
+ <path d="M 200,92 A 8,8 0 0 1 208,100 C 208,108 200,128 200,128 C 200,128 192,108 192,100 A 8,8 0 0 1 200,92 Z"
143
+ fill="#ffff00" stroke="#000" stroke-width="0.8" />
144
+ <text x="200" y="106" fill="#000" font-size="9" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
145
+ <circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#bbb" />
146
+ <circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#bbb" />
147
+ </g>
148
+
149
+ <g transform="translate(75, 395)">
150
+ <text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0&deg;</text>
151
+ <rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
152
+ <rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
153
+ <g stroke="#555" stroke-width="1">
154
+ <line x1="0" y1="-2" x2="0" y2="14" /><line x1="62.5" y1="2" x2="62.5" y2="10" />
155
+ <line x1="125" y1="-2" x2="125" y2="14" /><line x1="187.5" y1="2" x2="187.5" y2="10" />
156
+ <line x1="250" y1="-2" x2="250" y2="14" />
157
+ </g>
158
+ <g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
159
+ <text x="0" y="24">-20&deg;</text><text x="62.5" y="24">-10</text>
160
+ <text x="125" y="24">0&deg;</text><text x="187.5" y="24">10</text>
161
+ <text x="250" y="24">20&deg;</text>
162
+ </g>
163
+ </g>
164
+ </svg>
165
+
166
+ <!-- BUSSOLA 2: RADAR HISTORICAL (Nascosto all'avvio) -->
167
+ <svg id="wind-radar" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet" style="display: none;">
168
+ <defs id="radar-gradients">
169
+ <clipPath id="radar-boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
170
+ <filter id="radar-center-glow" x="-20%" y="-20%" width="140%" height="140%">
171
+ <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
172
+ </filter>
173
+ </defs>
174
+
175
+ <circle cx="200" cy="200" r="160" fill="rgb(252, 252, 252)" />
176
+ <circle cx="200" cy="200" r="125" fill="rgb(240, 240, 240)" />
177
+
178
+ <g id="radar-ticks"></g>
179
+
180
+ <g id="radar-tick-labels" fill="#000000" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
181
+ <text font-size="16" transform="translate(200, 74)">N</text>
182
+ <text font-size="16" transform="translate(326, 200) rotate(90)">E</text>
183
+ <text font-size="16" transform="translate(74, 200) rotate(-90)">W</text>
184
+ <text font-size="16" transform="translate(200, 326) rotate(180)">S</text>
185
+
186
+ <text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
187
+ <text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
188
+ <text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
189
+ <text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
190
+ <text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">330</text>
191
+ <text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">300</text>
192
+ <text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">240</text>
193
+ <text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">210</text>
194
+ </g>
195
+
196
+ <circle id="ora-orbit" cx="200" cy="200" r="67.2" fill="none" stroke="#cfd8dc" stroke-width="1.2" stroke-dasharray="4, 4" />
197
+ <g id="radar-rings"></g>
198
+
199
+ <circle id="radar-hotspot" cx="200" cy="200" r="55" fill="rgb(238, 238, 238)" stroke="#e0e0e0" stroke-width="1" filter="url(#radar-center-glow)" cursor="pointer" />
200
+ <path id="radar-boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
201
+ fill="#000000" transform="translate(0, 5)" clip-path="url(#radar-boat-clip)" style="pointer-events: none;" />
202
+ </svg>
203
+ </div>
164
204
 
165
205
  <!-- COLONNA DESTRA -->
166
206
  <!-- NOTA: Etichetta a SX e Unità a DX nel codice, il CSS usa row-reverse per specchiarli sul bordo -->
@@ -218,6 +258,7 @@
218
258
  <script src="utils.js"></script>
219
259
  <script src="charts.js"></script>
220
260
  <script src="gauge.js"></script>
261
+ <script src="weather-radar.js"></script>
221
262
  <script src="app.js?v=6.0"></script>
222
263
  </body>
223
264
  </html>
package/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * ==========================================================================
5
5
  * Definisce l'interfaccia di configurazione in Signal K Admin e crea
6
6
  * gli endpoint pubblici per la Dashboard, mantenendo lo storico in RAM.
7
+ * file index.js
7
8
  */
8
9
  const https = require('https'); // Importazione del modulo HTTPS nativo di Node.js
9
10
 
@@ -23,11 +24,12 @@ module.exports = function (app) {
23
24
  const CALM_THRESHOLD_KTS = 1.5; // Soglia di calma piatta (anello a 360°)
24
25
  const PRESSURE_FILTER_RATIO = 0.40; // Filtro di pressione dinamico (40% del picco per ignorare i cali)
25
26
 
26
- // Database dello storico in RAM sul server (Sintonizzato Pro v6.0)
27
+ // Database dello storico in RAM sul server (Sintonizzato Pro v6.0)
27
28
  let histories = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [], twd: [] };
28
29
  let graphTempBuf = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [], twd: [] };
29
30
  let lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0, twd: 0 };
30
31
  let raw = {};
32
+ let lastPathProcessTimes = {}; // Registro dei timestamp per limitazione di frequenza a 1Hz
31
33
 
32
34
  // Nuovo database dedicato per gli archi storici della bussola (6 ore = 12 slot)
33
35
  let windRadarSlots = [];
@@ -135,93 +137,125 @@ module.exports = function (app) {
135
137
  app.debug(msg);
136
138
  };
137
139
 
138
- /**
139
- * processIncomingDelta: Decodifica i dati dei sensori in Knots/Meters ed esegue l'aggregazione
140
- * Gestisce l'architettura "Nativo Prima, Fallback Dopo" per il vento reale.
141
- */
142
- function processIncomingDelta(path, val) {
143
- if (val === null || val === undefined) return;
144
- raw[path] = val;
140
+ /**
141
+ * processIncomingDelta: Decodifica i dati dei sensori in Knots/Meters ed esegue l'aggregazione
142
+ * Applica un filtro passa-basso continuo a ogni pacchetto e storicizza a 1Hz con dati stabilizzati.
143
+ */
144
+ function processIncomingDelta(path, val) {
145
+ if (val === null || val === undefined) return;
146
+
147
+ const now = Date.now();
148
+ const alpha = 0.20; // Coefficiente di smoothing (Filtro passa-basso: reattività ~2 secondi)
149
+
150
+ // FILTRO PASSA-BASSO CONTINUO IN TEMPO REALE (Previene gli Spike prima della storicizzazione)
151
+ if (path === 'navigation.position') {
152
+ raw[path] = val; // Le coordinate GPS non sono soggette a filtri di smorzamento o ritardo
153
+
154
+ // Chiamata periodica Open-Meteo
155
+ if (val.latitude !== undefined && val.longitude !== undefined) {
156
+ const current30mSlot = Math.floor(now / 1800000) * 1800000;
157
+ if (current30mSlot > lastForecast30mSlot) {
158
+ fetchOpenMeteoForecast(val, current30mSlot);
159
+ }
160
+ }
161
+ return; // Esce subito
162
+ }
145
163
 
146
- const now = Date.now();
164
+ if (path.includes('angle') || path.includes('heading') || path.includes('course') || path.includes('direction') || path.includes('twd')) {
165
+ // 1. Caso Angolare (Radianti): Calcolo differenziale circolare per gestire l'oltrepasso dello 0/360 gradi
166
+ if (raw[path] !== undefined) {
167
+ let diff = Math.atan2(Math.sin(val - raw[path]), Math.cos(val - raw[path]));
168
+ raw[path] = (raw[path] + diff * alpha + Math.PI * 2) % (Math.PI * 2);
169
+ } else {
170
+ raw[path] = val;
171
+ }
172
+ } else {
173
+ // 2. Caso Lineare (Velocità e Profondità): Smorzamento continuo
174
+ if (raw[path] !== undefined) {
175
+ raw[path] = (val * alpha) + (raw[path] * (1 - alpha));
176
+ } else {
177
+ raw[path] = val;
178
+ }
179
+ }
147
180
 
148
- // 1. Cattura dei dati nativi (Se presenti, li scrive direttamente nello storico)
149
- if (path === 'navigation.position') {
150
- // Trigger allineato all'orologio: calcoliamo il confine della mezz'ora corrente dell'orologio
151
- if (val && val.latitude !== undefined && val.longitude !== undefined) {
152
- const current30mSlot = Math.floor(Date.now() / 1800000) * 1800000;
153
- // Se siamo entrati in una nuova mezz'ora di orologio dall'ultimo download, avviamo il fetch
154
- if (current30mSlot > lastForecast30mSlot) {
155
- fetchOpenMeteoForecast(val, current30mSlot);
181
+ // LIMITATORE DI FREQUENZA (RATE LIMITER) A 1HZ PER PERCORSO ATTIVO:
182
+ // La scrittura nello storico e i calcoli derivati vengono eseguiti al massimo una volta al secondo,
183
+ // leggendo il valore "raw[path]" stabilizzato continuamente dal filtro passa-basso superiore.
184
+ if (!lastPathProcessTimes[path]) lastPathProcessTimes[path] = 0;
185
+ if (now - lastPathProcessTimes[path] < 1000) {
186
+ return; // Esce subito risparmiando la CPU se il sensore ha già aggiornato nell'ultimo secondo
156
187
  }
157
- }
158
- }
159
- else if (path === 'navigation.speedThroughWater') {
160
- manageHistory('stw', val * 1.94384);
161
- }
162
- else if (path === 'navigation.speedOverGround') {
163
- manageHistory('sog', val * 1.94384);
164
- }
165
- else if (path === 'environment.depth.belowTransducer') {
166
- manageHistory('depth', val);
167
- }
168
- else if (path === 'environment.wind.speedApparent') {
169
- manageHistory('aws', val * 1.94384);
170
- }
171
- else if (path === 'environment.wind.angleApparent') {
172
- raw[path] = val; // BUG RISOLTO: Acquisizione dell'AWA mancante inserita!
173
- }
174
- else if (path === 'environment.wind.speedTrue') {
175
- lastNativeTwsTime = now; // Rilevato TWS nativo della centralina!
176
- manageHistory('tws', val * 1.94384);
177
- }
178
- // --- DECODIFICA PRUA MAGNETICA SERVER-SIDE ---
179
- else if (path === 'navigation.headingMagnetic') {
180
- const hasTrueHdg = raw['navigation.headingTrue'] !== undefined;
181
- if (!hasTrueHdg) {
182
- const variation = raw['navigation.magneticVariation'] || 0;
183
- raw['navigation.headingTrue'] = (val + variation + 2 * Math.PI) % (2 * Math.PI);
184
- }
185
- }
186
- else if (path === 'environment.wind.directionTrue') {
187
- lastNativeTwdTime = now; // Rilevato TWD nativo della centralina!
188
- manageHistory('twd', val);
189
- }
188
+ lastPathProcessTimes[path] = now;
190
189
 
191
- // 2. Calcolo combinato di FALLBACK (Si attiva solo se la centralina non invia TWS/TWD nativi)
192
- const aws = raw["environment.wind.speedApparent"];
193
- const awa = raw["environment.wind.angleApparent"];
194
- const stw = raw["navigation.speedThroughWater"] || 0;
195
- const sog = raw["navigation.speedOverGround"] || 0;
196
- const hdg = raw["navigation.headingTrue"];
197
- const cog = raw["navigation.courseOverGroundTrue"] || 0;
198
-
199
- if (aws !== undefined && awa !== undefined) {
200
- const awsKts = aws * 1.94384;
201
- const stwKts = stw * 1.94384;
202
- const tw_water_x = awsKts * Math.cos(awa) - stwKts;
203
- const tw_water_y = awsKts * Math.sin(awa);
204
-
205
- // Calcoliamo il TWS di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
206
- if (now - lastNativeTwsTime > 5000) {
207
- const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
208
- manageHistory('tws', tws);
209
- }
190
+ // Da qui in poi, l'esecuzione della storia e del vento reale avviene rigorosamente a 1Hz con dati puliti:
191
+ const smoothedVal = raw[path];
210
192
 
211
- const twa = Math.atan2(tw_water_y, tw_water_x);
212
-
213
- // La VMG viene sempre calcolata a livello server poiché raramente è nativa
214
- const vmg = Math.abs(stwKts * Math.cos(twa));
215
- manageHistory('vmg', vmg);
216
-
217
- // Calcoliamo il TWD di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
218
- if (hdg !== undefined && (now - lastNativeTwdTime > 5000)) {
219
- const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
220
- manageHistory('twd', twd); // BUG RISOLTO: Rimossa la riassegnazione di "const" che mandava in crash il server
221
- }
222
- }
223
- }
193
+ if (path === 'navigation.speedThroughWater') {
194
+ manageHistory('stw', smoothedVal * 1.94384);
195
+ }
196
+ else if (path === 'navigation.speedOverGround') {
197
+ manageHistory('sog', smoothedVal * 1.94384);
198
+ }
199
+ else if (path === 'environment.depth.belowTransducer') {
200
+ manageHistory('depth', smoothedVal);
201
+ }
202
+ else if (path === 'environment.wind.speedApparent') {
203
+ manageHistory('aws', smoothedVal * 1.94384);
204
+ }
205
+ else if (path === 'environment.wind.angleApparent') {
206
+ // Già gestito e normalizzato dal filtro passa-basso superiore
207
+ }
208
+ else if (path === 'environment.wind.speedTrue') {
209
+ lastNativeTwsTime = now; // Rilevato TWS nativo della centralina!
210
+ manageHistory('tws', smoothedVal * 1.94384);
211
+ }
212
+ // --- DECODIFICA PRUA MAGNETICA SERVER-SIDE ---
213
+ else if (path === 'navigation.headingMagnetic') {
214
+ const hasTrueHdg = raw['navigation.headingTrue'] !== undefined;
215
+ if (!hasTrueHdg) {
216
+ const variation = raw['navigation.magneticVariation'] || 0;
217
+ raw['navigation.headingTrue'] = (smoothedVal + variation + 2 * Math.PI) % (2 * Math.PI);
218
+ }
219
+ }
220
+ else if (path === 'environment.wind.directionTrue') {
221
+ lastNativeTwdTime = now; // Rilevato TWD nativo della centralina!
222
+ manageHistory('twd', smoothedVal);
223
+ }
224
+
225
+ // 2. Calcolo combinato di FALLBACK (Si attiva solo se la centralina non invia TWS/TWD nativi)
226
+ const aws = raw["environment.wind.speedApparent"];
227
+ const awa = raw["environment.wind.angleApparent"];
228
+ const stw = raw["navigation.speedThroughWater"] || 0;
229
+ const sog = raw["navigation.speedOverGround"] || 0;
230
+ const hdg = raw["navigation.headingTrue"];
231
+ const cog = raw["navigation.courseOverGroundTrue"] || 0;
232
+
233
+ if (aws !== undefined && awa !== undefined) {
234
+ const awsKts = aws * 1.94384;
235
+ const stwKts = stw * 1.94384;
236
+ const tw_water_x = awsKts * Math.cos(awa) - stwKts;
237
+ const tw_water_y = awsKts * Math.sin(awa);
238
+
239
+ // Calcoliamo il TWS di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
240
+ if (now - lastNativeTwsTime > 5000) {
241
+ const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
242
+ manageHistory('tws', tws);
243
+ }
224
244
 
245
+ const twa = Math.atan2(tw_water_y, tw_water_x);
246
+
247
+ // La VMG viene sempre calcolata a livello server poiché raramente è nativa
248
+ const vmg = Math.abs(stwKts * Math.cos(twa));
249
+ manageHistory('vmg', vmg);
250
+
251
+ // Calcoliamo il TWD di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
252
+ if (hdg !== undefined && (now - lastNativeTwdTime > 5000)) {
253
+ const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
254
+ manageHistory('twd', twd);
255
+ }
256
+ }
257
+ }
258
+
225
259
  /**
226
260
  * manageHistory: Versione Server-side dell'aggregatore matematico tattico
227
261
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "7.0.1",
3
+ "version": "7.0.3",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/style.css CHANGED
@@ -520,3 +520,40 @@ body.night-mode {
520
520
  .night-mode .graph-wrapper polygon[style*="#0088cc"] {
521
521
  fill: #33aadd !important;
522
522
  }
523
+
524
+ /* ==========================================================================
525
+ 12. INTEGRAZIONE DUAL-SVG COMPASS & RADAR
526
+ ========================================================================== */
527
+ #wind-gauge, #wind-radar {
528
+ width: 100%;
529
+ height: 100%;
530
+ max-height: 100%;
531
+ object-fit: contain;
532
+ transition: opacity 0.2s ease-in-out;
533
+ }
534
+
535
+ /* --- OVERRIDE NIGHT MODE PER IL WIND RADAR --- */
536
+ .night-mode #wind-radar circle {
537
+ fill: #000000 !important;
538
+ stroke: #220000 !important;
539
+ }
540
+ .night-mode #wind-radar circle:nth-of-type(2),
541
+ .night-mode #radar-hotspot {
542
+ fill: #050000 !important;
543
+ stroke: #330000 !important;
544
+ }
545
+ .night-mode #radar-tick-labels {
546
+ fill: #800000 !important;
547
+ }
548
+ .night-mode #radar-boat-icon {
549
+ fill: #220000 !important;
550
+ stroke: #ff0000 !important;
551
+ stroke-width: 0.8px !important;
552
+ opacity: 1 !important;
553
+ }
554
+ .night-mode #ora-orbit {
555
+ stroke: #330000 !important;
556
+ }
557
+ .night-mode #wind-radar circle[stroke="#b0bec5"] {
558
+ stroke: #440000 !important;
559
+ }
package/utils.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Sailing Dashboard Pro - Math, Conversions & Audio Utilities
4
4
  * ==========================================================================
5
5
  * Raccoglie le funzioni pure di calcolo vettoriale e sintesi sonora.
6
+ *file utils.js
6
7
  */
7
8
 
8
9
  // --- 1. CONVERSIONI STANDARD ---
@@ -0,0 +1,281 @@
1
+ /**
2
+ * ==========================================================================
3
+ * Signal K Wind Radar - Vector Core and Compass Rendering Engine (Modular v6.0)
4
+ * ==========================================================================
5
+ * Autore: Sailing Rotevista
6
+ * Libreria grafica pura per il disegno della bussola radar storica (TWD).
7
+ * Condivide le risorse, lo stato e i flussi di dati generati da app.js.
8
+ */
9
+
10
+ // --- 1. PARAMETRI DI CALCOLO INTERNI ---
11
+ const CALM_THRESHOLD_KTS = 1.5;
12
+ const PRESSURE_FILTER_RATIO = 0.40;
13
+ const ringRadii = [59.0, 67.2, 75.4, 83.6, 91.8, 100.0, 108.2, 116.4];
14
+ const ARC_STROKE_WIDTH = 5.0;
15
+ const BORDER_STROKE_WIDTH = ARC_STROKE_WIDTH + 2; // 7px
16
+
17
+ // --- 2. FUNZIONI GEOMETRICHE DI SUPPORTO ---
18
+ function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
19
+ const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
20
+ return {
21
+ x: centerX + (radius * Math.cos(angleInRadians)),
22
+ y: centerY + (radius * Math.sin(angleInRadians))
23
+ };
24
+ }
25
+
26
+ function describeArc(centerX, centerY, radius, startAngle, endAngle) {
27
+ const start = polarToCartesian(centerX, centerY, radius, endAngle);
28
+ const end = polarToCartesian(centerX, centerY, radius, startAngle);
29
+
30
+ let arcSweep = endAngle - startAngle;
31
+ if (arcSweep < 0) arcSweep += 360;
32
+
33
+ const largeArcFlag = arcSweep <= 180 ? "0" : "1";
34
+
35
+ return [
36
+ "M", start.x, start.y,
37
+ "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
38
+ ].join(" ");
39
+ }
40
+
41
+ // Genera dinamicamente le tacche dei gradi bussola per l'SVG del radar
42
+ function initRadarTicks() {
43
+ const c = document.getElementById('radar-ticks');
44
+ if (c) {
45
+ c.innerHTML = "";
46
+ for (let i = 0; i < 360; i += 10) {
47
+ const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
48
+ const m = i % 30 === 0;
49
+ l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50));
50
+ l.setAttribute("stroke", m ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1");
51
+ l.setAttribute("transform", `rotate(${i}, 200, 200)`);
52
+ c.appendChild(l);
53
+ }
54
+ }
55
+ }
56
+
57
+ // --- 3. MOTORE DI CALCOLO DELL'ANELLO 1 (Presente Mobile) ---
58
+ function calculateActive30mRing() {
59
+ const now = Date.now();
60
+ const start30m = now - 1800000;
61
+
62
+ const twdRecent = (store.twdMinuteBuffer || []).filter(p => p.time >= start30m);
63
+ const twsRecent = (store.twsMinuteBuffer || []).filter(p => p.time >= start30m);
64
+
65
+ if (twdRecent.length === 0) return null;
66
+
67
+ const twsVals = twsRecent.map(p => p.val).filter(v => isFinite(v));
68
+ const maxTws = twsVals.length > 0 ? Math.max(...twsVals) : 0;
69
+ const minTws = twsVals.length > 0 ? Math.min(...twsVals) : 0;
70
+
71
+ if (maxTws < CALM_THRESHOLD_KTS) {
72
+ return { twsPeak: maxTws, twsMin: minTws, twdMin: 0, twdMax: 360, isCalm: true };
73
+ }
74
+
75
+ let allAngles = [];
76
+ twdRecent.forEach(p => {
77
+ allAngles.push(p.val);
78
+ allAngles.push(p.min);
79
+ allAngles.push(p.max);
80
+ });
81
+
82
+ let sumSin = 0; let sumCos = 0;
83
+ allAngles.forEach(a => { sumSin += Math.sin(a); sumCos += Math.cos(a); });
84
+ const avgAngle = Math.atan2(sumSin, sumCos);
85
+ const finalAvg = (avgAngle + Math.PI * 2) % (Math.PI * 2);
86
+
87
+ let diffs = allAngles.map(a => {
88
+ let diff = a - finalAvg;
89
+ return Math.atan2(Math.sin(diff), Math.cos(diff));
90
+ });
91
+
92
+ diffs.sort((a, b) => a - b);
93
+ const trimCount = Math.floor(diffs.length * 0.05);
94
+ const activeDiffs = diffs.slice(trimCount, diffs.length - trimCount);
95
+ const finalDiffs = activeDiffs.length > 0 ? activeDiffs : diffs;
96
+
97
+ const minDiff = Math.min(...finalDiffs);
98
+ const maxDiff = Math.max(...finalDiffs);
99
+
100
+ const finalMinDeg = Math.round(radToDeg((finalAvg + minDiff + Math.PI * 2) % (Math.PI * 2)));
101
+ const finalMaxDeg = Math.round(radToDeg((finalAvg + maxDiff + Math.PI * 2) % (Math.PI * 2)));
102
+
103
+ return {
104
+ twdMin: finalMinDeg,
105
+ twdMax: finalMaxDeg,
106
+ twsPeak: maxTws,
107
+ twsMin: minTws,
108
+ isCalm: false
109
+ };
110
+ }
111
+
112
+ // --- 4. MOTORE GRAFICO DI DISEGNO DEL RADAR ---
113
+ function renderRadar() {
114
+ const ringsContainer = document.getElementById('radar-rings');
115
+ const defsContainer = document.getElementById('radar-gradients');
116
+
117
+ if (!ringsContainer || !defsContainer) return; // Protezione se l'SVG del radar non è presente nel DOM
118
+
119
+ defsContainer.innerHTML = `
120
+ <clipPath id="radar-boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
121
+ <filter id="radar-center-glow" x="-20%" y="-20%" width="140%" height="140%">
122
+ <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
123
+ </filter>
124
+ `;
125
+ ringsContainer.innerHTML = '';
126
+
127
+ const oraOrbit = document.getElementById('ora-orbit');
128
+ if (oraOrbit) oraOrbit.setAttribute("r", ringRadii[1]);
129
+
130
+ const now = Date.now();
131
+ const current30mSlot = Math.floor(now / 1800000) * 1800000;
132
+
133
+ const radarDataList = [];
134
+
135
+ // 1. ANELLO 0: Previsione Futura Open-Meteo
136
+ if (store.futureForecast) {
137
+ const twdDeg = Math.round(radToDeg(store.futureForecast.twd));
138
+ radarDataList.push({
139
+ twdMin: (twdDeg - 20 + 360) % 360,
140
+ twdMax: (twdDeg + 20 + 360) % 360,
141
+ twsPeak: store.futureForecast.tws,
142
+ isFuture: true
143
+ });
144
+ } else {
145
+ radarDataList.push(null);
146
+ }
147
+
148
+ // 2. ANELLO 1: Presente Mobile (Real-Time Client-Side)
149
+ const activeRing = calculateActive30mRing();
150
+ radarDataList.push(activeRing);
151
+
152
+ // 3. ANELLI 2-7: Storico consolidato dal server
153
+ const slots = store.windRadarSlots || [];
154
+ for (let i = 1; i <= 6; i++) {
155
+ const targetTimestamp = current30mSlot - (i * 1800000);
156
+ const matchedSlot = slots.find(s => s.timestamp === targetTimestamp);
157
+
158
+ if (matchedSlot) {
159
+ radarDataList.push({
160
+ twdMin: Math.round(radToDeg(matchedSlot.twdMin)),
161
+ twdMax: Math.round(radToDeg(matchedSlot.twdMax)),
162
+ twsPeak: matchedSlot.twsPeak,
163
+ twsMin: matchedSlot.twsMin !== undefined ? matchedSlot.twsMin : matchedSlot.twsPeak,
164
+ isCalm: matchedSlot.twsPeak < CALM_THRESHOLD_KTS
165
+ });
166
+ } else {
167
+ radarDataList.push(null);
168
+ }
169
+ }
170
+
171
+ // Disegno degli archi
172
+ radarDataList.forEach((data, index) => {
173
+ if (!data) return;
174
+
175
+ const radius = ringRadii[index];
176
+ const gradId = `chord-gradient-${index}`;
177
+ const opacityValue = 1;
178
+
179
+ if (data.isCalm || data.twsPeak < CALM_THRESHOLD_KTS) {
180
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
181
+ circle.setAttribute("cx", "200");
182
+ circle.setAttribute("cy", "200");
183
+ circle.setAttribute("r", radius);
184
+ circle.setAttribute("fill", "none");
185
+ circle.setAttribute("stroke", document.body.classList.contains('night-mode') ? "#440000" : "#b0bec5");
186
+ circle.setAttribute("stroke-width", "1.2");
187
+ circle.setAttribute("stroke-dasharray", "4, 4");
188
+ ringsContainer.appendChild(circle);
189
+ return;
190
+ }
191
+
192
+ let strokeColor = '';
193
+
194
+ const getColorForSpeed = (tws) => {
195
+ const R1 = CONFIG.graphs.reef1 || 15;
196
+ const R2 = CONFIG.graphs.reef2 || 20;
197
+ const R3 = R2 + (R2 - R1);
198
+ if (tws < R1 * 0.4) return '#ffffff';
199
+ if (tws < R1 * 0.75) return '#00C851';
200
+ if (tws < R1) return '#ff9800';
201
+ if (tws < R2) return '#ffaa00';
202
+ if (tws < R2 + (R3 - R2) * 0.5) return '#ff3b30';
203
+ return '#9c27b0';
204
+ };
205
+
206
+ const baseTws = data.isFuture && store.futureForecast ? store.futureForecast.tws : (data.twsMin !== undefined ? data.twsMin : data.twsPeak);
207
+ const peakTws = data.isFuture && store.futureForecast ? store.futureForecast.gust : data.twsPeak;
208
+
209
+ const baseColor = getColorForSpeed(baseTws);
210
+ const peakColor = getColorForSpeed(peakTws);
211
+
212
+ if (baseColor !== peakColor) {
213
+ const startPt = polarToCartesian(200, 200, radius, data.twdMax);
214
+ const endPt = polarToCartesian(200, 200, radius, data.twdMin);
215
+
216
+ const xml = `
217
+ <linearGradient id="${gradId}" x1="${startPt.x.toFixed(1)}" y1="${startPt.y.toFixed(1)}" x2="${endPt.x.toFixed(1)}" y2="${endPt.y.toFixed(1)}" gradientUnits="userSpaceOnUse">
218
+ <stop offset="0%" stop-color="${baseColor}" />
219
+ <stop offset="50%" stop-color="${peakColor}" />
220
+ <stop offset="100%" stop-color="${baseColor}" />
221
+ </linearGradient>
222
+ `;
223
+ defsContainer.innerHTML += xml;
224
+ strokeColor = `url(#${gradId})`;
225
+ } else {
226
+ strokeColor = baseColor;
227
+ }
228
+
229
+ const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
230
+
231
+ if (!data.isFuture) {
232
+ const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
233
+ borderPath.setAttribute("d", pathData);
234
+ borderPath.setAttribute("fill", "none");
235
+ borderPath.setAttribute("stroke", "#000000");
236
+ borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
237
+ borderPath.setAttribute("stroke-linecap", "round");
238
+ borderPath.setAttribute("opacity", opacityValue);
239
+ ringsContainer.appendChild(borderPath);
240
+ }
241
+
242
+ const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
243
+ mainPath.setAttribute("d", pathData);
244
+ mainPath.setAttribute("fill", "none");
245
+ mainPath.setAttribute("stroke", strokeColor);
246
+ mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
247
+ mainPath.setAttribute("stroke-linecap", "round");
248
+ mainPath.setAttribute("opacity", data.isFuture ? "0.5" : opacityValue);
249
+ mainPath.id = data.isFuture ? "" : (index === 1 ? "active-present-arc" : "");
250
+ ringsContainer.appendChild(mainPath);
251
+ });
252
+
253
+ // Disegno del LED lampeggiante del meteo-trend
254
+ if (activeRing && !activeRing.isCalm) {
255
+ const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
256
+ const strategicWindowMs = (isNavigating ? 15 : 60) * 60000;
257
+ const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
258
+
259
+ if (twdNow && twdRef) {
260
+ let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
261
+
262
+ if (Math.abs(deltaMeteo) > 6.0) {
263
+ const isSouth = store.raw["navigation.position"] && store.raw["navigation.position"].latitude < 0;
264
+ let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#00C851" : "#ff3b30") : (deltaMeteo > 0 ? "#00C851" : "#ff3b30");
265
+
266
+ const radiusAnello1 = ringRadii[1];
267
+ const angleTarget = deltaMeteo > 0 ? activeRing.twdMax : activeRing.twdMin;
268
+ const pt = polarToCartesian(200, 200, radiusAnello1, angleTarget);
269
+
270
+ const led = document.createElementNS("http://www.w3.org/2000/svg", "circle");
271
+ led.setAttribute("cx", pt.x.toFixed(1));
272
+ led.setAttribute("cy", pt.y.toFixed(1));
273
+ led.setAttribute("r", "5.5");
274
+ led.setAttribute("fill", meteoColor);
275
+ led.setAttribute("class", "is-trending");
276
+ led.setAttribute("filter", "url(#radar-center-glow)");
277
+ ringsContainer.appendChild(led);
278
+ }
279
+ }
280
+ }
281
+ }
File without changes
File without changes
File without changes