@sailingrotevista/rotevista-dash 6.2.3 → 6.2.5

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
@@ -22,7 +22,7 @@ let CONFIG = {
22
22
  minSpeed: 0.5,
23
23
  stabilityBreakout: 15
24
24
  },
25
- graphs: { reef1: 5, reef2: 10, historyMinutes: 10, samples: 60 },
25
+ graphs: { reef1: 15, reef2: 20, historyMinutes: 10, samples: 60 },
26
26
  scales: {
27
27
  stw: { stdMax: 4, hercSpan: 2, step: 1 },
28
28
  sog: { stdMax: 4, hercSpan: 2, step: 1 },
package/index.js CHANGED
@@ -75,6 +75,7 @@ module.exports = function (app) {
75
75
  ...histories,
76
76
  windRadarSlots: windRadarSlots,
77
77
  futureForecast: futureForecast
78
+ 'navigation.position': raw['navigation.position'] // Chirurgico: Espone le coordinate GPS correnti per la diagnostica e il radar
78
79
  };
79
80
  res.json(responseData);
80
81
  });
@@ -86,6 +87,7 @@ module.exports = function (app) {
86
87
  const localSubscription = {
87
88
  context: 'vessels.self',
88
89
  subscribe: [
90
+ { path: 'navigation.position' }, // Chirurgico: Aggiunto l'ascolto della posizione GPS per abilitare le previsioni
89
91
  { path: 'navigation.speedThroughWater' },
90
92
  { path: 'navigation.speedOverGround' },
91
93
  { path: 'environment.depth.belowTransducer' },
@@ -191,7 +193,7 @@ module.exports = function (app) {
191
193
  const awa = raw["environment.wind.angleApparent"];
192
194
  const stw = raw["navigation.speedThroughWater"] || 0;
193
195
  const sog = raw["navigation.speedOverGround"] || 0;
194
- const hdg = raw["navigation.headingTrue"] || 0;
196
+ const hdg = raw["navigation.headingTrue"];
195
197
  const cog = raw["navigation.courseOverGroundTrue"] || 0;
196
198
 
197
199
  if (aws !== undefined && awa !== undefined) {
@@ -212,12 +214,11 @@ module.exports = function (app) {
212
214
  const vmg = Math.abs(stwKts * Math.cos(twa));
213
215
  manageHistory('vmg', vmg);
214
216
 
215
- // Calcoliamo il TWD di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
216
- if (hdg !== undefined && (now - lastNativeTwdTime > 5000)) {
217
- const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
218
- twd = (twd + 2 * Math.PI) % (2 * Math.PI); // Sicurezza extra
219
- manageHistory('twd', twd);
220
- }
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
+ }
221
222
  }
222
223
  }
223
224
 
@@ -359,11 +360,8 @@ module.exports = function (app) {
359
360
 
360
361
  // EMISSIONE DEL DELTA: Se abbiamo calcolato il TWD di fallback, lo trasmettiamo a Signal K
361
362
  if (now - lastNativeTwdTime > 5000) {
362
- emitDelta('environment.wind.directionTrue', {
363
- val: finalValue.val,
364
- min: finalValue.min,
365
- max: finalValue.max
366
- });
363
+ // Standard Signal K: trasmettiamo solo il valore medio (float numerico in radianti)
364
+ emitDelta('environment.wind.directionTrue', finalValue.val);
367
365
  }
368
366
 
369
367
  // --- TRIGGER DI CONGELAMENTO ARCO (Ogni :00 e :30 dell'orologio) ---
@@ -647,6 +645,7 @@ module.exports = function (app) {
647
645
  https.get(url, (res) => {
648
646
  if (res.statusCode !== 200) {
649
647
  app.error(`[Open-Meteo] HTTP Error: ${res.statusCode}`);
648
+ res.resume();
650
649
  lastForecast30mSlot = 0; // Reset in caso di errore per permettere un tentativo al prossimo pacchetto GPS
651
650
  return;
652
651
  }
@@ -744,6 +743,7 @@ module.exports = function (app) {
744
743
  app.error('[Open-Meteo] Forecast matching slots not found for target time');
745
744
  }
746
745
  }
746
+
747
747
  /**
748
748
  * emitDelta: Scrive ed emette un aggiornamento di rotta direttamente nel
749
749
  * server principale di Signal K per renderlo disponibile a tutti i client WebSocket.
@@ -751,6 +751,7 @@ module.exports = function (app) {
751
751
  function emitDelta(path, value) {
752
752
  if (typeof app.handleMessage === 'function') {
753
753
  app.handleMessage(plugin.id, {
754
+ context: 'vessels.self', // BUG RISOLTO: Inserito il contesto Signal K per evitare lo scarto del delta
754
755
  updates: [
755
756
  {
756
757
  source: { label: 'rotevista-dash-plugin' },
@@ -767,6 +768,5 @@ module.exports = function (app) {
767
768
  }
768
769
  }
769
770
 
770
-
771
771
  return plugin;
772
772
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "6.2.3",
3
+ "version": "6.2.5",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
@@ -0,0 +1,233 @@
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="it">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Signal K Wind Radar - Server RAM Diagnostic Panel</title>
8
+ <style>
9
+ body {
10
+ background-color: #0c0c0c;
11
+ color: #00ff66;
12
+ font-family: 'Courier New', Courier, monospace;
13
+ padding: 20px;
14
+ margin: 0;
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ h1 {
19
+ color: #ffffff;
20
+ font-size: 1.4rem;
21
+ border-bottom: 2px solid #00ff66;
22
+ padding-bottom: 10px;
23
+ margin-top: 0;
24
+ }
25
+
26
+ .controls {
27
+ background: #111;
28
+ border: 1px solid #333;
29
+ padding: 15px;
30
+ border-radius: 8px;
31
+ margin-bottom: 20px;
32
+ display: flex;
33
+ flex-wrap: wrap;
34
+ gap: 15px;
35
+ align-items: center;
36
+ }
37
+
38
+ input[type="text"] {
39
+ background: #000;
40
+ color: #00ff66;
41
+ border: 1px solid #00ff66;
42
+ padding: 8px 12px;
43
+ font-family: monospace;
44
+ border-radius: 4px;
45
+ width: 250px;
46
+ }
47
+
48
+ button {
49
+ background: #00ff66;
50
+ color: #000000;
51
+ border: none;
52
+ padding: 8px 15px;
53
+ font-weight: bold;
54
+ font-family: monospace;
55
+ cursor: pointer;
56
+ border-radius: 4px;
57
+ }
58
+
59
+ button:hover {
60
+ background: #ffffff;
61
+ }
62
+
63
+ .status-box {
64
+ font-size: 13px;
65
+ line-height: 1.5;
66
+ margin-bottom: 20px;
67
+ padding: 15px;
68
+ background: #141414;
69
+ border: 1px solid #222;
70
+ border-radius: 8px;
71
+ }
72
+
73
+ .grid {
74
+ display: grid;
75
+ grid-template-columns: repeat(auto-fit, minmax(45%, 1fr));
76
+ gap: 20px;
77
+ }
78
+
79
+ .panel {
80
+ background: #111;
81
+ border: 1px solid #333;
82
+ border-radius: 8px;
83
+ padding: 15px;
84
+ box-sizing: border-box;
85
+ display: flex;
86
+ flex-direction: column;
87
+ }
88
+
89
+ .panel-title {
90
+ color: #ffffff;
91
+ font-weight: bold;
92
+ font-size: 1.1rem;
93
+ margin-bottom: 10px;
94
+ border-bottom: 1px solid #222;
95
+ padding-bottom: 5px;
96
+ text-transform: uppercase;
97
+ }
98
+
99
+ pre {
100
+ background: #000000;
101
+ border: 1px solid #222;
102
+ padding: 10px;
103
+ border-radius: 4px;
104
+ overflow: auto;
105
+ max-height: 400px;
106
+ font-size: 11px;
107
+ color: #00ff66;
108
+ margin: 0;
109
+ box-sizing: border-box;
110
+ flex-grow: 1;
111
+ }
112
+
113
+ .success { color: #00ff66; }
114
+ .danger { color: #ff3333; }
115
+ .warning { color: #ffcc00; }
116
+ </style>
117
+ </head>
118
+ <body>
119
+
120
+ <h1>Signal K Wind Radar - Server RAM Diagnostic Panel (v6.0)</h1>
121
+
122
+ <!-- PANNELLO DI CONTROLLO IP -->
123
+ <div class="controls">
124
+ <label>Indirizzo IP Server SK:</label>
125
+ <input type="text" id="server-ip" value="192.168.111.240:3000">
126
+ <button onclick="runManualDiagnostic()">Esegui Scansione Ora</button>
127
+ <span id="diagnostic-running" style="display: none; color: #ffcc00;">SCANSIONE IN CORSO...</span>
128
+ </div>
129
+
130
+ <!-- STATO DELLE RISPOSTE DEL SERVER -->
131
+ <div class="status-box">
132
+ <div style="font-weight: bold; font-size: 15px; margin-bottom: 8px; color: #fff;">Sintesi Connessione REST:</div>
133
+ <div>• Endpoint Configurazione (/rotevista-config): <span id="status-config" class="warning">IN ATTESA...</span></div>
134
+ <div>• Endpoint Storico RAM (/rotevista-history): <span id="status-history" class="warning">IN ATTESA...</span></div>
135
+ <div style="margin-top: 10px; border-top: 1px dashed #222; padding-top: 10px;">
136
+ • Picchi Vento in RAM (histories.tws): <span id="stat-tws" class="warning">--</span> |
137
+ • Direzioni Vento in RAM (histories.twd): <span id="stat-twd" class="warning">--</span> |
138
+ • Archi Bussola in RAM (windRadarSlots): <span id="stat-slots" class="warning">--</span>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- GRIGLIA DUMP MEMORIA GREZZA -->
143
+ <div class="grid">
144
+
145
+ <!-- PANNELLO SINISTRO: DUMP CONFIG -->
146
+ <div class="panel">
147
+ <div class="panel-title">Raw Config Dump (/rotevista-config)</div>
148
+ <pre id="dump-config">In attesa di scansione...</pre>
149
+ </div>
150
+
151
+ <!-- PANNELLO DESTRO: DUMP COSTRUZIONE STORICO -->
152
+ <div class="panel">
153
+ <div class="panel-title">Raw History & Radar Dump (/rotevista-history)</div>
154
+ <pre id="dump-history">In attesa di scansione...</pre>
155
+ </div>
156
+
157
+ </div>
158
+
159
+ <script>
160
+ // Estrapola l'indirizzo dinamico dall'URL della barra indirizzi se caricato dal server
161
+ window.onload = function() {
162
+ let addr = window.location.host || "192.168.111.240:3000";
163
+ document.getElementById('server-ip').value = addr;
164
+ runManualDiagnostic();
165
+ // Esegue un poll diagnostico automatico ogni 5 secondi
166
+ setInterval(runManualDiagnostic, 5000);
167
+ };
168
+
169
+ function getTargetUrl(path) {
170
+ const ip = document.getElementById('server-ip').value;
171
+ return `http://${ip}${path}`;
172
+ }
173
+
174
+ async function runManualDiagnostic() {
175
+ document.getElementById('diagnostic-running').style.display = 'inline';
176
+
177
+ const configPre = document.getElementById('dump-config');
178
+ const historyPre = document.getElementById('dump-history');
179
+
180
+ const statusConfig = document.getElementById('status-config');
181
+ const statusHistory = document.getElementById('status-history');
182
+
183
+ // 1. SCANSIONE /rotevista-config
184
+ try {
185
+ const resConfig = await fetch(getTargetUrl('/rotevista-config'));
186
+ if (resConfig.ok) {
187
+ const data = await resConfig.json();
188
+ configPre.innerText = JSON.stringify(data, null, 2);
189
+ statusConfig.innerHTML = `<span class="success">RISPOSTA OK (200) - Configurazione Caricata</span>`;
190
+ } else {
191
+ statusConfig.innerHTML = `<span class="danger">ERRORE SERVER (${resConfig.status})</span>`;
192
+ configPre.innerText = "Il server ha risposto con un codice di errore.";
193
+ }
194
+ } catch (err) {
195
+ statusConfig.innerHTML = `<span class="danger">ERRORE DI RETE (Impossibile raggiungere il server)</span>`;
196
+ configPre.innerText = `Errore di connessione:\n${err.message}\n\nConsiglio:\n1. Verifica che l'IP del Cerbo sia corretto.\n2. Verifica che il tablet sia connesso alla rete Wi-Fi della barca.`;
197
+ }
198
+
199
+ // 2. SCANSIONE /rotevista-history
200
+ try {
201
+ const resHistory = await fetch(getTargetUrl('/rotevista-history'));
202
+ if (resHistory.ok) {
203
+ const data = await resHistory.json();
204
+ historyPre.innerText = JSON.stringify(data, null, 2);
205
+ statusHistory.innerHTML = `<span class="success">RISPOSTA OK (200) - Storico Caricato</span>`;
206
+
207
+ // Mostra statistiche dei record in RAM sul server
208
+ const countTws = data.tws ? data.tws.length : 0;
209
+ const countTwd = data.twd ? data.twd.length : 0;
210
+ const countSlots = data.windRadarSlots ? data.windRadarSlots.length : 0;
211
+
212
+ document.getElementById('stat-tws').innerHTML = `<span class="success">${countTws} punti</span>`;
213
+ document.getElementById('stat-twd').innerHTML = `<span class="success">${countTwd} punti</span>`;
214
+ document.getElementById('stat-slots').innerHTML = `<span class="success">${countSlots}/12 congelati</span>`;
215
+
216
+ } else {
217
+ statusHistory.innerHTML = `<span class="danger">ERRORE SERVER (${resHistory.status})</span>`;
218
+ historyPre.innerText = "Il server ha risposto con un codice di errore.";
219
+ }
220
+ } catch (err) {
221
+ statusHistory.innerHTML = `<span class="danger">ERRORE DI RETE (Impossibile raggiungere lo storico)</span>`;
222
+ historyPre.innerText = `Errore di connessione:\n${err.message}`;
223
+
224
+ document.getElementById('stat-tws').innerHTML = `<span class="danger">--</span>`;
225
+ document.getElementById('stat-twd').innerHTML = `<span class="danger">--</span>`;
226
+ document.getElementById('stat-slots').innerHTML = `<span class="danger">--</span>`;
227
+ }
228
+
229
+ document.getElementById('diagnostic-running').style.display = 'none';
230
+ }
231
+ </script>
232
+ </body>
233
+ </html>
package/radar.html CHANGED
@@ -311,6 +311,16 @@
311
311
  document.getElementById('debug-tws').innerText = tws.toFixed(1);
312
312
  document.getElementById('debug-twa').innerText = Math.round(radToDeg(twa)) + '°';
313
313
  document.getElementById('debug-twd').innerText = Math.round(radToDeg(twd)) + '°';
314
+
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
319
+
320
+ store.twsMinuteBuffer.push({ val: tws, time: now });
321
+ while(store.twsMinuteBuffer.length > 1800) store.twsMinuteBuffer.shift();
322
+
323
+ renderRadar(); // Rinfresco grafico istantaneo reattivo
314
324
  }
315
325
  }
316
326
 
@@ -352,7 +362,7 @@
352
362
  document.getElementById('debug-gps').innerText = val.latitude.toFixed(4) + '; ' + val.longitude.toFixed(4);
353
363
  }
354
364
 
355
- // BUG RISOLTO CHIRURGICAMENTE: Allineamento chiavi REST storiche twd e tws sintonizzate!
365
+ // Sincronizzazione automatica allineata alle chiavi REST storiche reali del server
356
366
  if (data.twd) store.twdMinuteBuffer = data.twd;
357
367
  if (data.tws) store.twsMinuteBuffer = data.tws;
358
368
  }
@@ -386,32 +396,8 @@
386
396
  let sourceLabel = u.$source || (u.source ? (u.source.label || "N2K") : "NMEA");
387
397
  if (u.values) {
388
398
  u.values.forEach(v => {
399
+ // Mantiene l'aggiornamento dei testi di debug istantanei a 1Hz
389
400
  processIncomingDelta(v.path, v.value, sourceLabel, timeMs);
390
-
391
- // COMPATIBILITÀ TOTALE DEL COPRIMENTO WEB-SOCKET:
392
- if (v.path === 'environment.wind.directionTrue') {
393
- let val, min, max;
394
- if (v.value && typeof v.value === 'object' && v.value.val !== undefined) {
395
- val = v.value.val;
396
- min = v.value.min;
397
- max = v.value.max;
398
- } else {
399
- val = v.value; min = v.value; max = v.value;
400
- }
401
- store.twdMinuteBuffer.push({ val: val, min: min, max: max, time: timeMs });
402
- while(store.twdMinuteBuffer.length > 120) store.twdMinuteBuffer.shift();
403
- renderRadar();
404
- }
405
- if (v.path === 'environment.wind.speedTrue') {
406
- let val;
407
- if (v.value && typeof v.value === 'object' && v.value.val !== undefined) {
408
- val = v.value.val;
409
- } else {
410
- val = v.value;
411
- }
412
- store.twsMinuteBuffer.push({ val: val, time: timeMs });
413
- while(store.twsMinuteBuffer.length > 120) store.twsMinuteBuffer.shift();
414
- }
415
401
  });
416
402
  }
417
403
  });
@@ -678,7 +664,7 @@
678
664
  const radius = ringRadii[index];
679
665
  const gradId = `chord-gradient-${index}`;
680
666
 
681
- // BUG RISOLTO CHIRURGICAMENTE: Rimosso il sbiadimento grigio (opacità sempre fissa a 1 per colore solido e brillante)
667
+ // Opacità sempre fissa a 1 per colore solido e brillante (BUG RISOLTO)
682
668
  const opacityValue = 1;
683
669
 
684
670
  // Caso A: Calma Piatta
@@ -737,11 +723,17 @@
737
723
  function init() {
738
724
  drawCompassTicks();
739
725
  connect();
740
- // Rinfresco periodico del radar e del pannello di debug ogni 5 secondi
741
- setInterval(renderRadar, 5000);
726
+
727
+ // Sincronizzazione strategica: scarica lo storico consolidato dal server ogni 30 secondi e ridisegna
728
+ setInterval(async () => {
729
+ await fetchConfigAndHistory();
730
+ renderRadar();
731
+ }, 30000);
742
732
  }
743
733
 
744
- window.onload = init;
734
+ window.onload = init;
735
+
736
+ window.onload = init;
745
737
  </script>
746
738
  </body>
747
739
  </html>
@@ -0,0 +1,181 @@
1
+ <!DOCTYPE html>
2
+ <html lang="it">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sailing Dashboard - Open-Meteo Diagnostic Tool</title>
7
+ <style>
8
+ body {
9
+ background-color: #0f172a;
10
+ color: #e2e8f0;
11
+ font-family: monospace;
12
+ padding: 20px;
13
+ margin: 0;
14
+ }
15
+ h1 {
16
+ color: #38bdf8;
17
+ border-bottom: 2px solid #38bdf8;
18
+ padding-bottom: 10px;
19
+ }
20
+ .card {
21
+ background: #1e293b;
22
+ border: 1px solid #334155;
23
+ border-radius: 8px;
24
+ padding: 15px;
25
+ margin-bottom: 20px;
26
+ }
27
+ button {
28
+ background: #0ea5e9;
29
+ color: #fff;
30
+ border: none;
31
+ padding: 10px 20px;
32
+ font-size: 14px;
33
+ font-weight: bold;
34
+ cursor: pointer;
35
+ border-radius: 4px;
36
+ }
37
+ button:hover { background: #0284c7; }
38
+ pre {
39
+ background: #020617;
40
+ padding: 15px;
41
+ border-radius: 6px;
42
+ overflow: auto;
43
+ max-height: 250px;
44
+ font-size: 12px;
45
+ color: #34d399;
46
+ border: 1px solid #1e293b;
47
+ }
48
+ .success { color: #4ade80; font-weight: bold; }
49
+ .error { color: #f87171; font-weight: bold; }
50
+ .pending { color: #fbbf24; font-weight: bold; }
51
+ </style>
52
+ </head>
53
+ <body>
54
+
55
+ <h1>Diagnostic Tool: Open-Meteo & GPS</h1>
56
+ <p>Usa questo pannello per verificare se la barca riesce a scaricare le previsioni meteo.</p>
57
+
58
+ <!-- STEP 1: VERIFICA GPS INTERNO DI BORDO -->
59
+ <div class="card">
60
+ <h3>1. Coordinate GPS correnti dal Cerbo GX (Signal K)</h3>
61
+ <button onclick="checkBoatGPS()">Leggi GPS Barca</button>
62
+ <p>Stato: <span id="gps-status" class="pending">In attesa...</span></p>
63
+ <pre id="gps-data">Nessun dato letto.</pre>
64
+ </div>
65
+
66
+ <!-- STEP 2: TEST DI CONNESSIONE DIRETTO DAL TABLET -->
67
+ <div class="card">
68
+ <h3>2. Test connessione Open-Meteo diretto dal Tablet (Questo Browser)</h3>
69
+ <p>Verifica se il tuo tablet ha accesso a internet in questo momento.</p>
70
+ <button onclick="testTabletConnection()">Esegui Test Tablet</button>
71
+ <p>Risultato: <span id="tablet-status" class="pending">In attesa...</span></p>
72
+ <pre id="tablet-data">Nessun tentativo eseguito.</pre>
73
+ </div>
74
+
75
+ <!-- STEP 3: ANALISI STORICO DEL SERVER -->
76
+ <div class="card">
77
+ <h3>3. Verifica Previsione salvata sul Server (/rotevista-history)</h3>
78
+ <p>Verifica se il Cerbo ha memorizzato la previsione oraria.</p>
79
+ <button onclick="checkServerHistory()">Interroga Server</button>
80
+ <p>Stato Previsione: <span id="server-status" class="pending">In attesa...</span></p>
81
+ <pre id="server-data">Nessun tentativo eseguito.</pre>
82
+ </div>
83
+
84
+ <script>
85
+ const fallbackIp = "192.168.111.240:3000";
86
+
87
+ function getApiUrl(path) {
88
+ let addr = window.location.host || fallbackIp;
89
+ if (window.location.protocol === 'file:') {
90
+ return `http://${fallbackIp}${path}`;
91
+ }
92
+ return `${window.location.protocol}//${addr}${path}`;
93
+ }
94
+
95
+ // 1. Legge il GPS dal server di bordo
96
+ async function checkBoatGPS() {
97
+ const status = document.getElementById('gps-status');
98
+ const dataPre = document.getElementById('gps-data');
99
+ status.innerHTML = "Interrogazione in corso...";
100
+
101
+ try {
102
+ const res = await fetch(getApiUrl('/rotevista-history'));
103
+ if (res.ok) {
104
+ const data = await res.json();
105
+ if (data['navigation.position']) {
106
+ const pos = data['navigation.position'];
107
+ status.className = "success";
108
+ status.innerText = "GPS ATTIVO DI BORDO";
109
+ dataPre.innerText = JSON.stringify(pos, null, 2);
110
+ } else {
111
+ status.className = "error";
112
+ status.innerText = "GPS RAGGIUNGIBILE MA SENZA COORDINATE";
113
+ dataPre.innerText = "La chiave 'navigation.position' non è presente nello storico del server.";
114
+ }
115
+ } else {
116
+ throw new Error(`Risposta server non OK: ${res.status}`);
117
+ }
118
+ } catch (err) {
119
+ status.className = "error";
120
+ status.innerText = "ERRORE DI RETE";
121
+ dataPre.innerText = err.message;
122
+ }
123
+ }
124
+
125
+ // 2. Test connessione Open-Meteo dal Tablet
126
+ async function testTabletConnection() {
127
+ const status = document.getElementById('tablet-status');
128
+ const dataPre = document.getElementById('tablet-data');
129
+ status.innerHTML = "Tentativo di connessione a Open-Meteo...";
130
+
131
+ // Coordinate di test (La Spezia)
132
+ const url = "https://api.open-meteo.com/v1/forecast?latitude=44.11&longitude=9.83&hourly=wind_speed_10m&wind_speed_unit=kn&forecast_days=1";
133
+
134
+ try {
135
+ const res = await fetch(url);
136
+ if (res.ok) {
137
+ const json = await res.json();
138
+ status.className = "success";
139
+ status.innerText = "CONNESSIONE OK - Il Tablet ha internet";
140
+ dataPre.innerText = JSON.stringify(json.hourly.time.slice(0, 3).map((t, i) => `${t}: ${json.hourly.wind_speed_10m[i]} nodi`), null, 2);
141
+ } else {
142
+ throw new Error(`Errore API: ${res.status}`);
143
+ }
144
+ } catch (err) {
145
+ status.className = "error";
146
+ status.innerText = "ERRORE - Il tablet NON ha internet o Open-Meteo è offline";
147
+ dataPre.innerText = err.message + "\n\nNota: Se sei connesso al Wi-Fi locale della barca privo di SIM, è normale che il test fallisca.";
148
+ }
149
+ }
150
+
151
+ // 3. Verifica se il server ha la previsione
152
+ async function checkServerHistory() {
153
+ const status = document.getElementById('server-status');
154
+ const dataPre = document.getElementById('server-data');
155
+ status.innerHTML = "Lettura storico dal Cerbo GX...";
156
+
157
+ try {
158
+ const res = await fetch(getApiUrl('/rotevista-history'));
159
+ if (res.ok) {
160
+ const data = await res.json();
161
+ if (data.futureForecast) {
162
+ status.className = "success";
163
+ status.innerText = "METEO PRESENTE NEL SERVER";
164
+ dataPre.innerText = JSON.stringify(data.futureForecast, null, 2);
165
+ } else {
166
+ status.className = "error";
167
+ status.innerText = "PREVISIONE ASSENTE (futureForecast è null)";
168
+ dataPre.innerText = "Il server è attivo ma non ha ancora scaricato o elaborato dati di previsione da Open-Meteo.";
169
+ }
170
+ } else {
171
+ throw new Error(`Risposta server non OK: ${res.status}`);
172
+ }
173
+ } catch (err) {
174
+ status.className = "error";
175
+ status.innerText = "ERRORE LETTURA STORICO";
176
+ dataPre.innerText = err.message;
177
+ }
178
+ }
179
+ </script>
180
+ </body>
181
+ </html>
@@ -1,490 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="it">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Wind Radar - Twin Light Compass Calibrata (v6.0)</title>
7
- <style>
8
- :root {
9
- /* Dimensione dinamica reattiva del widget quadrato */
10
- --box-size: 400px;
11
- }
12
-
13
- body {
14
- background-color: #ffffff;
15
- color: #000000;
16
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
17
- display: flex;
18
- flex-direction: column;
19
- align-items: center;
20
- justify-content: center;
21
- min-height: 100vh;
22
- margin: 0;
23
- padding: 20px;
24
- box-sizing: border-box;
25
- }
26
-
27
- h1 {
28
- color: #000;
29
- font-size: 1.3rem;
30
- margin-bottom: 5px;
31
- letter-spacing: 1px;
32
- text-transform: uppercase;
33
- font-weight: 700;
34
- }
35
-
36
- p {
37
- color: #666;
38
- font-size: 0.85rem;
39
- margin-top: 0;
40
- margin-bottom: 25px;
41
- text-align: center;
42
- }
43
-
44
- .container {
45
- display: flex;
46
- flex-wrap: wrap;
47
- gap: 30px;
48
- align-items: flex-start;
49
- justify-content: center;
50
- }
51
-
52
- .controls-panel {
53
- background: #fdfdfd;
54
- border: 1px solid #e5e5e5;
55
- padding: 20px;
56
- border-radius: 12px;
57
- width: 320px;
58
- box-sizing: border-box;
59
- box-shadow: 0 4px 12px rgba(0,0,0,0.05);
60
- }
61
-
62
- .control-group {
63
- margin-bottom: 15px;
64
- }
65
-
66
- .control-group label {
67
- display: block;
68
- font-size: 0.75rem;
69
- font-weight: bold;
70
- color: #666;
71
- text-transform: uppercase;
72
- margin-bottom: 8px;
73
- letter-spacing: 0.5px;
74
- }
75
-
76
- .control-group input[type="range"] {
77
- width: 100%;
78
- accent-color: #00C851;
79
- background: #ddd;
80
- height: 4px;
81
- border-radius: 2px;
82
- outline: none;
83
- cursor: pointer;
84
- }
85
-
86
- .val-display {
87
- float: right;
88
- color: #000;
89
- font-weight: bold;
90
- }
91
-
92
- /* BOX STRUTTURALE SCALABILE */
93
- .data-box-square {
94
- position: relative;
95
- width: var(--box-size);
96
- height: var(--box-size);
97
- background: rgb(255, 255, 255);
98
- border: 1px solid #eee;
99
- border-radius: 12px;
100
- box-sizing: border-box;
101
- display: flex;
102
- align-items: center;
103
- justify-content: center;
104
- overflow: hidden;
105
- transition: width 0.1s ease, height 0.1s ease;
106
- }
107
-
108
- .label-row {
109
- position: absolute;
110
- top: 10px;
111
- left: 12px;
112
- right: 12px;
113
- display: flex;
114
- justify-content: space-between;
115
- align-items: baseline;
116
- pointer-events: none;
117
- z-index: 10;
118
- }
119
-
120
- .label {
121
- color: #888;
122
- font-size: 0.65rem;
123
- font-weight: bold;
124
- text-transform: uppercase;
125
- }
126
-
127
- .unit {
128
- color: #aaa;
129
- font-size: 0.6rem;
130
- font-weight: bold;
131
- }
132
-
133
- /* Forza l'SVG a riempire lo spazio a disposizione mantenendo le proporzioni */
134
- svg {
135
- display: block;
136
- width: 100%;
137
- height: 100%;
138
- max-height: 100%;
139
- object-fit: contain;
140
- }
141
- </style>
142
- </head>
143
- <body>
144
-
145
- <h1>Tactical Wind Radar (v6.0)</h1>
146
- <p>Calibrazione Spaziatura: Compressione proporzionale degli anelli con luce di 5px dal dial</p>
147
-
148
- <div class="container">
149
-
150
- <div class="controls-panel">
151
-
152
- <!-- SLIDER DI ZOOM SCALABILITÀ -->
153
- <div class="control-group" style="padding-bottom: 15px; border-bottom: 1px solid #eee; margin-bottom: 20px;">
154
- <label style="color: #0088cc;">Zoom Box / Scalabilità <span class="val-display" id="scale-val" style="color: #0088cc;">400 px</span></label>
155
- <input type="range" id="slider-scale" min="200" max="800" value="400" step="10" oninput="resizeBox()">
156
- </div>
157
-
158
- <div class="control-group">
159
- <label>Reef 1 (Limite 1) <span class="val-display" id="r1-val">18 kts</span></label>
160
- <input type="range" id="slider-r1" min="10" max="22" value="18" step="1" oninput="updateReefs()">
161
- </div>
162
-
163
- <div class="control-group">
164
- <label>Reef 2 (Limite 2) <span class="val-display" id="r2-val">24 kts</span></label>
165
- <input type="range" id="slider-r2" min="16" max="32" value="24" step="1" oninput="updateReefs()">
166
- </div>
167
-
168
- <div class="control-group" style="border-top: 1px solid #eee; padding-top: 15px; margin-bottom: 0;">
169
- <label>Reef 3 (Storm) <span class="val-display" id="r3-val" style="color: #9c27b0;">30 kts</span></label>
170
- <span style="font-size: 0.7rem; color: #888; display: block; margin-top: -3px; line-height: 1.2;">
171
- Calcolato dinamicamente: R2 + (R2 - R1)
172
- </span>
173
- </div>
174
- </div>
175
-
176
- <div class="data-box-square">
177
- <div class="label-row">
178
- <span class="label">TWD (HISTORICAL)</span>
179
- <span class="unit">ECMWF + N2K</span>
180
- </div>
181
-
182
- <!-- BUSSOLA RADAR SVG (Esatto ViewBox dell'originale, nativamente scalabile) -->
183
- <svg id="wind-radar" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
184
- <defs id="radar-gradients">
185
- <clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
186
- <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
187
- <stop offset="0%" style="stop-color:#333333;stop-opacity:1" />
188
- <stop offset="100%" style="stop-color:#999999;stop-opacity:1" />
189
- </linearGradient>
190
-
191
- <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
192
- <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
193
- </filter>
194
- </defs>
195
-
196
- <!-- I tre sfondi speculari originali diurni con coordinate RGB esatte -->
197
- <circle cx="200" cy="200" r="160" fill="rgb(252, 252, 252)" />
198
- <circle cx="200" cy="200" r="125" fill="rgb(240, 240, 240)" />
199
-
200
- <!-- Ticks originali generati dinamicamente -->
201
- <g id="ticks"></g>
202
-
203
- <!-- Etichette cardinali N/S/E/W -->
204
- <g id="tick-labels" fill="#000000" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
205
- <text font-size="16" transform="translate(200, 74)">N</text>
206
- <text font-size="16" transform="translate(326, 200) rotate(90)">E</text>
207
- <text font-size="16" transform="translate(74, 200) rotate(-90)">W</text>
208
- <text font-size="16" transform="translate(200, 326) rotate(180)">S</text>
209
-
210
- <text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
211
- <text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
212
- <text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
213
- <text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
214
- <text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
215
- <text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
216
- <text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
217
- <text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
218
- </g>
219
-
220
- <!-- 1. ORBITA DEL PRESENTE "ORA" SUL LIVELLO DI SFONDO (Sotto gli archi) -->
221
- <!-- Posizionata esattamente sulla mezzeria del raggio dell'Anello 1 (67.2px) -->
222
- <circle id="ora-orbit" cx="200" cy="200" r="67.2" fill="none" stroke="#cfd8dc" stroke-width="1.2" stroke-dasharray="4, 4" />
223
-
224
- <!-- Gli anelli radar degli archi verranno stampati qui -->
225
- <g id="radar-rings"></g>
226
-
227
- <!-- Cerchio centrale interattivo originale e Barca NERA SOLIDA -->
228
- <circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="rgb(238, 238, 238)" stroke="#e0e0e0" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
229
- <path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
230
- fill="#000000" transform="translate(0, 5)" clip-path="url(#boat-clip)" style="pointer-events: none;" />
231
- </svg>
232
- </div>
233
- </div>
234
-
235
- <script>
236
- // Funzione per testare dinamicamente il ridimensionamento del div
237
- function resizeBox() {
238
- const size = document.getElementById('slider-scale').value;
239
- // Modifichiamo la variabile CSS, l'SVG seguirà nativamente
240
- document.documentElement.style.setProperty('--box-size', size + 'px');
241
- document.getElementById('scale-val').innerText = size + " px";
242
- }
243
-
244
- const CALM_THRESHOLD_KTS = 1.5;
245
-
246
- // Default Reef settings
247
- let REEF1 = 18;
248
- let REEF2 = 24;
249
- let REEF3 = 30;
250
-
251
- // Dati di simulazione calibrati su 8 anelli reali con la correzione ripristinata dei diametri
252
- const mockData = [
253
- { isFuture: true, twdMin: 235, twdMax: 245, twsPeak: 5.0 }, // Anello 0 (r:59.0px): Previsione futura (Bianco Solido - 100% OPACITÀ CON TRATTEGGIO)
254
- { isActive: true, twdMin: 220, twdMax: 260, twsPeak: 11.5 }, // Anello 1 (r:67.2px): Presente Mobile (Sfumatura Bianco -> Verde -> Bianco)
255
- { twdMin: 225, twdMax: 255, twsPeak: 13.5 }, // Anello 2 (r:75.4px): Ora -0.5 (Verde Solido)
256
- { twdMin: 230, twdMax: 250, twsPeak: 16.0 }, // Anello 3 (r:83.6px): Ora -1.0 (Sfumatura Verde -> Arancio -> Verde)
257
- { twdMin: 235, twdMax: 245, twsPeak: 20.0 }, // Anello 4 (r:91.8px): Ora -1.5 (Arancio Solido)
258
- { twdMin: 220, twdMax: 260, twsPeak: 22.5 }, // Anello 5 (r:100.0px): Ora -2.0 (Sfumatura Arancio -> Rosso -> Arancio)
259
- { twdMin: 230, twdMax: 250, twsPeak: 25.5 }, // Anello 6 (r:108.2px): Ora -2.5 (Rosso Solido)
260
- { twdMin: 215, twdMax: 265, twsPeak: 28.5 } // Anello 7 (r:116.4px): Ora -3.0 (Sfumatura Rosso -> Viola -> Rosso)
261
- ];
262
-
263
- // --- MAPPA RADIALE SPECULARE COMPRESSA AL MILLIMETRO SU 8 ANELLI ---
264
- // r_i = 59.0px + 8.2px * i (Incastonati in sicurezza, massimo raggio a 116.4px)
265
- const ringRadii = [59.0, 67.2, 75.4, 83.6, 91.8, 100.0, 108.2, 116.4];
266
- const chronologicalMapping = [0, 1, 2, 3, 4, 5, 6, 7];
267
-
268
- const ARC_STROKE_WIDTH = 5.0; // Spessore arco ridotto a 5.0px per una visualizzazione affilatissima
269
- const BORDER_STROKE_WIDTH = ARC_STROKE_WIDTH + 2; // Spessore 7.0px (bordo da 1px per lato)
270
-
271
- function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
272
- const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
273
- return {
274
- x: centerX + (radius * Math.cos(angleInRadians)),
275
- y: centerY + (radius * Math.sin(angleInRadians))
276
- };
277
- }
278
-
279
- function describeArc(centerX, centerY, radius, startAngle, endAngle) {
280
- const start = polarToCartesian(centerX, centerY, radius, endAngle);
281
- const end = polarToCartesian(centerX, centerY, radius, startAngle);
282
-
283
- let arcSweep = endAngle - startAngle;
284
- if (arcSweep < 0) arcSweep += 360;
285
-
286
- const largeArcFlag = arcSweep <= 180 ? "0" : "1";
287
-
288
- return [
289
- "M", start.x, start.y,
290
- "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
291
- ].join(" ");
292
- }
293
-
294
- function drawCompassTicks() {
295
- const ticksGroup = document.getElementById('ticks');
296
- ticksGroup.innerHTML = '';
297
- for (let i = 0; i < 360; i += 10) {
298
- const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
299
- const isMajor = (i % 30 === 0);
300
-
301
- line.setAttribute("x1", "200");
302
- line.setAttribute("y1", "40");
303
- line.setAttribute("x2", "200");
304
- line.setAttribute("y2", isMajor ? "60" : "50");
305
-
306
- line.setAttribute("stroke", isMajor ? "#000000" : "#bbbbbb");
307
- line.setAttribute("stroke-width", isMajor ? "2" : "1");
308
- line.setAttribute("transform", `rotate(${i}, 200, 200)`);
309
- ticksGroup.appendChild(line);
310
- }
311
- }
312
-
313
- function updateReefs() {
314
- REEF1 = parseInt(document.getElementById('slider-r1').value);
315
- REEF2 = parseInt(document.getElementById('slider-r2').value);
316
- REEF3 = REEF2 + (REEF2 - REEF1);
317
-
318
- document.getElementById('r1-val').innerText = REEF1 + " kts";
319
- document.getElementById('r2-val').innerText = REEF2 + " kts";
320
- document.getElementById('r3-val').innerText = REEF3 + " kts";
321
-
322
- renderRadar();
323
- }
324
-
325
- // --- LA NUOVA SCALA A COLORE DI PROSSIMITÀ CON CONTRASTO RICALIBRATO ---
326
- function getChordAlignedGradient(id, radius, tws, startAngle, endAngle) {
327
- const startPt = polarToCartesian(200, 200, radius, endAngle);
328
- const endPt = polarToCartesian(200, 200, radius, startAngle);
329
-
330
- let stops = '';
331
- const R1 = REEF1;
332
- const R2 = REEF2;
333
- const R3 = REEF3;
334
- const colorEdge = 'rgb(240, 240, 240)';
335
-
336
- // 1. BIANCO SOLIDO (0 - 40% di R1)
337
- if (tws < R1 * 0.4) {
338
- return { type: 'solid', color: '#ffffff' };
339
- }
340
- // 2. TRANSIZIONE BIANCO -> VERDE (40% - 60% di R1)
341
- else if (tws >= R1 * 0.4 && tws < R1 * 0.6) {
342
- stops = `
343
- <stop offset="0%" stop-color="#ffffff" />
344
- <stop offset="50%" stop-color="#00C851" />
345
- <stop offset="100%" stop-color="#ffffff" />
346
- `;
347
- }
348
- // 3. VERDE SOLIDO (60% - 75% di R1) -> Brezza stabile
349
- else if (tws >= R1 * 0.6 && tws < R1 * 0.75) {
350
- return { type: 'solid', color: '#00C851' };
351
- }
352
- // 4. TRANSIZIONE VERDE -> ARANCIONE (75% R1 - R1) -> Raffiche in aumento
353
- else if (tws >= R1 * 0.75 && tws < R1) {
354
- stops = `
355
- <stop offset="0%" stop-color="#00C851" />
356
- <stop offset="50%" stop-color="#ff9800" />
357
- <stop offset="100%" stop-color="#00C851" />
358
- `;
359
- }
360
- // 5. ARANCIONE SOLIDO (R1 -> Metà di R2) -> Vento forte stabile
361
- else if (tws >= R1 && tws < R1 + (R2 - R1) * 0.5) {
362
- return { type: 'solid', color: '#ff9800' };
363
- }
364
- // 6. TRANSIZIONE ARANCIONE -> ROSSO (Metà R2 -> R2) -> RICALIBRATO AD ALTO CONTRASTO (Dorato -> Cremisi)
365
- else if (tws >= R1 + (R2 - R1) * 0.5 && tws < R2) {
366
- stops = `
367
- <stop offset="0%" stop-color="#ffaa00" /> <!-- Arancio Dorato Brillante sui bordi -->
368
- <stop offset="50%" stop-color="#d50000" /> <!-- Rosso Cremisi Intenso e saturo al centro -->
369
- <stop offset="100%" stop-color="#ffaa00" />
370
- `;
371
- }
372
- // 7. ROSSO SOLIDO (R2 -> Metà di R3) -> Burrasca stabile
373
- else if (tws >= R2 && tws < R2 + (R3 - R2) * 0.5) {
374
- return { type: 'solid', color: '#ff3b30' };
375
- }
376
- // 8. TRANSIZIONE ROSSO -> VIOLA (Metà R3 -> R3) -> Transizione Spettrale (Rosso vivo -> Viola)
377
- else if (tws >= R2 + (R3 - R2) * 0.5 && tws < R3) {
378
- stops = `
379
- <stop offset="0%" stop-color="#ff3b30" />
380
- <stop offset="50%" stop-color="#9c27b0" />
381
- <stop offset="100%" stop-color="#ff3b30" />
382
- `;
383
- }
384
- // 9. VIOLA SOLIDO (Oltre R3) -> Tempesta stabile
385
- else {
386
- return { type: 'solid', color: '#9c27b0' };
387
- }
388
-
389
- const xml = `
390
- <linearGradient id="${id}" x1="${startPt.x.toFixed(1)}" y1="${startPt.y.toFixed(1)}" x2="${endPt.x.toFixed(1)}" y2="${endPt.y.toFixed(1)}" gradientUnits="userSpaceOnUse">
391
- ${stops}
392
- </linearGradient>
393
- `;
394
-
395
- return { type: 'gradient', xml: xml, url: `url(#${id})` };
396
- }
397
-
398
- function renderRadar() {
399
- const ringsContainer = document.getElementById('radar-rings');
400
- const defsContainer = document.getElementById('radar-gradients');
401
-
402
- defsContainer.innerHTML = `
403
- <clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
404
- <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
405
- <stop offset="0%" style="stop-color:#333333;stop-opacity:1" />
406
- <stop offset="100%" style="stop-color:#999999;stop-opacity:1" />
407
- </linearGradient>
408
- <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
409
- <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
410
- </filter>
411
- `;
412
- ringsContainer.innerHTML = '';
413
-
414
- // Imposta dinamicamente i raggi in base a X per renderli scalabili
415
- const oraOrbit = document.getElementById('ora-orbit');
416
- if (oraOrbit) oraOrbit.setAttribute("r", 67.2);
417
-
418
- mockData.forEach((data, index) => {
419
- const radius = ringRadii[index];
420
- const gradId = `chord-gradient-${index}`;
421
-
422
- // IMPORTANTE: Rimosso il dimezzamento dell'opacità per la previsione.
423
- // Ora l'opacità è sempre 1 (100% solida) per tutti gli anelli,
424
- // garantendo che il Bianco sia puro e brillante, senza sbiadire in grigio!
425
- const opacityValue = 1;
426
-
427
- // --- 1. CASO CALMA PIATTA (Cerchio grigio-azzurro discreto sottile) ---
428
- if (data.twsPeak < CALM_THRESHOLD_KTS) {
429
- const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
430
- circle.setAttribute("cx", "200");
431
- circle.setAttribute("cy", "200");
432
- circle.setAttribute("r", radius);
433
- circle.setAttribute("fill", "none");
434
- circle.setAttribute("stroke", "#b0bec5");
435
- circle.setAttribute("stroke-width", "1.2");
436
- circle.setAttribute("stroke-dasharray", "4, 4");
437
- ringsContainer.appendChild(circle);
438
- return;
439
- }
440
-
441
- // --- 2. CASO ARCO DIREZIONALE ---
442
- const grad = getChordAlignedGradient(gradId, radius, data.twsPeak, data.twdMin, data.twdMax);
443
- let strokeColor = '';
444
-
445
- if (grad.type === 'gradient') {
446
- defsContainer.innerHTML += grad.xml;
447
- strokeColor = grad.url;
448
- } else {
449
- strokeColor = grad.color;
450
- }
451
-
452
- const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
453
-
454
- const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
455
- borderPath.setAttribute("d", pathData);
456
- borderPath.setAttribute("fill", "none");
457
- borderPath.setAttribute("stroke", "#000000");
458
- borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
459
- borderPath.setAttribute("stroke-linecap", "round");
460
- borderPath.setAttribute("opacity", opacityValue);
461
-
462
- // Tratteggio per l'Anello Previsione (Anello 0)
463
- if (data.isFuture) {
464
- borderPath.setAttribute("stroke-dasharray", "10, 6");
465
- }
466
- ringsContainer.appendChild(borderPath);
467
-
468
- const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
469
- mainPath.setAttribute("d", pathData);
470
- mainPath.setAttribute("fill", "none");
471
- mainPath.setAttribute("stroke", strokeColor);
472
- mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
473
- mainPath.setAttribute("stroke-linecap", "round");
474
- mainPath.setAttribute("opacity", opacityValue);
475
-
476
- // Tratteggio per l'Anello Previsione (Anello 0)
477
- if (data.isFuture) {
478
- mainPath.setAttribute("stroke-dasharray", "10, 6");
479
- }
480
- ringsContainer.appendChild(mainPath);
481
- });
482
- }
483
-
484
- window.onload = function() {
485
- drawCompassTicks();
486
- renderRadar();
487
- };
488
- </script>
489
- </body>
490
- </html>