@sailingrotevista/rotevista-dash 7.0.1 → 7.0.2

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
  // ==========================================================================
@@ -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
 
@@ -172,20 +173,23 @@ module.exports = function (app) {
172
173
  raw[path] = val; // BUG RISOLTO: Acquisizione dell'AWA mancante inserita!
173
174
  }
174
175
  else if (path === 'environment.wind.speedTrue') {
175
- lastNativeTwsTime = now; // Rilevato TWS nativo della centralina!
176
- manageHistory('tws', val * 1.94384);
176
+ lastNativeTwsTime = now; // Rilevato TWS nativo della centralina!
177
+ const cleanVal = (val && typeof val === 'object' && val.value !== undefined) ? val.value : val;
178
+ manageHistory('tws', cleanVal * 1.94384);
177
179
  }
178
180
  // --- DECODIFICA PRUA MAGNETICA SERVER-SIDE ---
179
181
  else if (path === 'navigation.headingMagnetic') {
180
182
  const hasTrueHdg = raw['navigation.headingTrue'] !== undefined;
181
183
  if (!hasTrueHdg) {
182
184
  const variation = raw['navigation.magneticVariation'] || 0;
183
- raw['navigation.headingTrue'] = (val + variation + 2 * Math.PI) % (2 * Math.PI);
185
+ const cleanVal = (val && typeof val === 'object' && val.value !== undefined) ? val.value : val;
186
+ raw['navigation.headingTrue'] = (cleanVal + variation + 2 * Math.PI) % (2 * Math.PI);
184
187
  }
185
188
  }
186
189
  else if (path === 'environment.wind.directionTrue') {
187
190
  lastNativeTwdTime = now; // Rilevato TWD nativo della centralina!
188
- manageHistory('twd', val);
191
+ const cleanVal = (val && typeof val === 'object' && val.value !== undefined) ? val.value : val;
192
+ manageHistory('twd', cleanVal);
189
193
  }
190
194
 
191
195
  // 2. Calcolo combinato di FALLBACK (Si attiva solo se la centralina non invia TWS/TWD nativi)
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.2",
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