@sailingrotevista/rotevista-dash 6.2.4 → 6.2.6

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
@@ -74,7 +74,8 @@ module.exports = function (app) {
74
74
  const responseData = {
75
75
  ...histories,
76
76
  windRadarSlots: windRadarSlots,
77
- futureForecast: futureForecast
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) {
@@ -358,11 +360,8 @@ module.exports = function (app) {
358
360
 
359
361
  // EMISSIONE DEL DELTA: Se abbiamo calcolato il TWD di fallback, lo trasmettiamo a Signal K
360
362
  if (now - lastNativeTwdTime > 5000) {
361
- emitDelta('environment.wind.directionTrue', {
362
- val: finalValue.val,
363
- min: finalValue.min,
364
- max: finalValue.max
365
- });
363
+ // Standard Signal K: trasmettiamo solo il valore medio (float numerico in radianti)
364
+ emitDelta('environment.wind.directionTrue', finalValue.val);
366
365
  }
367
366
 
368
367
  // --- TRIGGER DI CONGELAMENTO ARCO (Ogni :00 e :30 dell'orologio) ---
@@ -646,6 +645,7 @@ module.exports = function (app) {
646
645
  https.get(url, (res) => {
647
646
  if (res.statusCode !== 200) {
648
647
  app.error(`[Open-Meteo] HTTP Error: ${res.statusCode}`);
648
+ res.resume();
649
649
  lastForecast30mSlot = 0; // Reset in caso di errore per permettere un tentativo al prossimo pacchetto GPS
650
650
  return;
651
651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "6.2.4",
3
+ "version": "6.2.6",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/radar.html CHANGED
@@ -396,32 +396,8 @@
396
396
  let sourceLabel = u.$source || (u.source ? (u.source.label || "N2K") : "NMEA");
397
397
  if (u.values) {
398
398
  u.values.forEach(v => {
399
+ // Mantiene l'aggiornamento dei testi di debug istantanei a 1Hz
399
400
  processIncomingDelta(v.path, v.value, sourceLabel, timeMs);
400
-
401
- // Ricezione live delle emissioni delta del nostro plugin server
402
- if (v.path === 'environment.wind.directionTrue') {
403
- let val, min, max;
404
- if (v.value && typeof v.value === 'object' && v.value.val !== undefined) {
405
- val = v.value.val;
406
- min = v.value.min;
407
- max = v.value.max;
408
- } else {
409
- val = v.value; min = v.value; max = v.value;
410
- }
411
- store.twdMinuteBuffer.push({ val: val, min: min, max: max, time: timeMs });
412
- while(store.twdMinuteBuffer.length > 120) store.twdMinuteBuffer.shift();
413
- renderRadar();
414
- }
415
- if (v.path === 'environment.wind.speedTrue') {
416
- let val;
417
- if (v.value && typeof v.value === 'object' && v.value.val !== undefined) {
418
- val = v.value.val;
419
- } else {
420
- val = v.value;
421
- }
422
- store.twsMinuteBuffer.push({ val: val, time: timeMs });
423
- while(store.twsMinuteBuffer.length > 120) store.twsMinuteBuffer.shift();
424
- }
425
401
  });
426
402
  }
427
403
  });
@@ -747,11 +723,17 @@
747
723
  function init() {
748
724
  drawCompassTicks();
749
725
  connect();
750
- // Rinfresco periodico del radar e del pannello di debug ogni 5 secondi
751
- 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);
752
732
  }
753
733
 
754
- window.onload = init;
734
+ window.onload = init;
735
+
736
+ window.onload = init;
755
737
  </script>
756
738
  </body>
757
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>
File without changes