@sailingrotevista/rotevista-dash 6.1.4 → 6.2.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/radar.html ADDED
@@ -0,0 +1,747 @@
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, maximum-scale=1.0, user-scalable=no">
6
+ <title>Wind Radar Live - Sailing Dashboard Pro</title>
7
+ <style>
8
+ body {
9
+ background-color: #ffffff;
10
+ color: #000000;
11
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
12
+ margin: 0;
13
+ padding: 0;
14
+ height: 100vh;
15
+ width: 100vw;
16
+ display: flex;
17
+ flex-direction: column;
18
+ overflow: hidden;
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ /* AREA GRAFICA PRINCIPALE (75% ALTEZZA SCHERMO) */
23
+ .radar-viewport {
24
+ height: 75vh;
25
+ width: 100%;
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ background: #ffffff;
30
+ border-bottom: 1px solid #eee;
31
+ }
32
+
33
+ /* BOX STRUTTURALE DEL WIDGET REATTIVO AL 75% DI ALTEZZA */
34
+ .data-box-square {
35
+ position: relative;
36
+ width: 73vh;
37
+ height: 73vh;
38
+ background: #ffffff;
39
+ box-sizing: border-box;
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ }
44
+
45
+ .label-row {
46
+ position: absolute;
47
+ top: 10px;
48
+ left: 12px;
49
+ right: 12px;
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: baseline;
53
+ pointer-events: none;
54
+ z-index: 10;
55
+ }
56
+
57
+ .label {
58
+ color: #888;
59
+ font-size: 0.65rem;
60
+ font-weight: bold;
61
+ text-transform: uppercase;
62
+ }
63
+
64
+ .unit {
65
+ color: #aaa;
66
+ font-size: 0.6rem;
67
+ font-weight: bold;
68
+ }
69
+
70
+ svg {
71
+ display: block;
72
+ width: 100%;
73
+ height: 100%;
74
+ max-height: 100%;
75
+ object-fit: contain;
76
+ }
77
+
78
+ /* AREA TROUBLESHOOTING E DEBUG (25% ALTEZZA SCHERMO) */
79
+ .debug-panel {
80
+ height: 25vh;
81
+ width: 100%;
82
+ background: #f9f9f9;
83
+ border-top: 1px solid #ddd;
84
+ padding: 10px 15px;
85
+ box-sizing: border-box;
86
+ font-family: monospace;
87
+ font-size: 11px;
88
+ color: #333;
89
+ overflow-y: auto;
90
+ display: grid;
91
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
92
+ gap: 15px;
93
+ }
94
+
95
+ .debug-col {
96
+ background: #ffffff;
97
+ border: 1px solid #e0e0e0;
98
+ border-radius: 6px;
99
+ padding: 8px;
100
+ box-sizing: border-box;
101
+ max-height: 100%;
102
+ overflow-y: auto;
103
+ }
104
+
105
+ .debug-title {
106
+ font-weight: bold;
107
+ color: #00C851;
108
+ margin-bottom: 5px;
109
+ border-bottom: 1px solid #eee;
110
+ padding-bottom: 3px;
111
+ text-transform: uppercase;
112
+ }
113
+
114
+ .status-badge {
115
+ display: inline-block;
116
+ padding: 2px 6px;
117
+ border-radius: 3px;
118
+ font-weight: bold;
119
+ color: #fff;
120
+ text-transform: uppercase;
121
+ font-size: 9px;
122
+ }
123
+ .status-online { background: #00C851; }
124
+ .status-offline { background: #ff3b30; }
125
+ </style>
126
+ </head>
127
+ <body>
128
+
129
+ <!-- AREA SUPERIORE: BUSSOLA RADAR TATTICA -->
130
+ <div class="radar-viewport">
131
+ <div class="data-box-square">
132
+ <div class="label-row">
133
+ <span class="label">TWD (HISTORICAL)</span>
134
+ <span id="network-status-text" class="unit">CONNESSIONE...</span>
135
+ </div>
136
+
137
+ <!-- BUSSOLA RADAR SVG (Esatto ViewBox dell'originale, nativamente scalabile) -->
138
+ <svg id="wind-radar" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
139
+ <defs id="radar-gradients">
140
+ <clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
141
+ <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
142
+ <stop offset="0%" style="stop-color:#333333;stop-opacity:1" />
143
+ <stop offset="100%" style="stop-color:#999999;stop-opacity:1" />
144
+ </linearGradient>
145
+
146
+ <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
147
+ <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
148
+ </filter>
149
+ </defs>
150
+
151
+ <!-- I tre sfondi speculari originali diurni con coordinate RGB esatte -->
152
+ <circle cx="200" cy="200" r="160" fill="rgb(252, 252, 252)" />
153
+ <circle cx="200" cy="200" r="125" fill="rgb(240, 240, 240)" />
154
+
155
+ <!-- Ticks originali generati dinamicamente -->
156
+ <g id="ticks"></g>
157
+
158
+ <!-- Etichette cardinali N/S/E/W -->
159
+ <g id="tick-labels" fill="#000000" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
160
+ <text font-size="16" transform="translate(200, 74)">N</text>
161
+ <text font-size="16" transform="translate(326, 200) rotate(90)">E</text>
162
+ <text font-size="16" transform="translate(74, 200) rotate(-90)">W</text>
163
+ <text font-size="16" transform="translate(200, 326) rotate(180)">S</text>
164
+
165
+ <text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
166
+ <text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
167
+ <text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
168
+ <text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
169
+ <text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
170
+ <text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
171
+ <text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
172
+ <text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
173
+ </g>
174
+
175
+ <!-- 1. ORBITA DEL PRESENTE "ORA" SUL LIVELLO DI SFONDO (Sotto gli archi) -->
176
+ <!-- Sintonizzata dinamicamente sul raggio dell'Anello 1 (67.2px) per evitare la doppia linea -->
177
+ <circle id="ora-orbit" cx="200" cy="200" r="67.2" fill="none" stroke="#cfd8dc" stroke-width="1.2" stroke-dasharray="4, 4" />
178
+
179
+ <!-- Gli anelli radar degli archi verranno stampati qui -->
180
+ <g id="radar-rings"></g>
181
+
182
+ <!-- Cerchio centrale interattivo originale e Barca NERA SOLIDA -->
183
+ <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" />
184
+ <path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
185
+ fill="#000000" transform="translate(0, 5)" clip-path="url(#boat-clip)" style="pointer-events: none;" />
186
+ </svg>
187
+ </div>
188
+ </div>
189
+
190
+ <!-- AREA INFERIORE: DEBUG & TROUBLESHOOTING PANEL (Ottimizzato per lo Store) -->
191
+ <div class="debug-panel">
192
+ <!-- Colonna 1: Stato Generale e Previsioni -->
193
+ <div class="debug-col">
194
+ <div class="debug-title">System & Forecast</div>
195
+ <div>Status: <span id="debug-status" class="status-badge status-offline">DISCONNECTED</span></div>
196
+ <div>Server IP: <span id="debug-ip">---</span></div>
197
+ <div>Reef 1: <span id="debug-r1">--</span> kts | Reef 2: <span id="debug-r2">--</span> kts</div>
198
+ <div>Reef 3 (Storm): <span id="debug-r3" style="color: #9c27b0; font-weight: bold;">--</span> kts</div>
199
+ <div>GPS Position: <span id="debug-gps">---</span></div>
200
+ <div style="margin-top: 5px; border-top: 1px dashed #eee; padding-top: 5px;">
201
+ <strong>Future Wind (Open-Meteo):</strong>
202
+ <div id="debug-future" style="color: #0088cc;">In attesa di coordinate GPS...</div>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- Colonna 2: Database Storico in RAM ( windRadarSlots ) -->
207
+ <div class="debug-col">
208
+ <div class="debug-title">Bussola Radar Store (Past Slots)</div>
209
+ <div>Rings loaded: <span id="debug-rings-count" style="font-weight: bold; color: #ff9800;">0/8</span></div>
210
+ <div id="debug-slots-list" style="margin-top: 5px; font-size: 10px; line-height: 1.3;">
211
+ <!-- Popolato via JS -->
212
+ </div>
213
+ </div>
214
+
215
+ <!-- Colonna 3: Buffer Minuto per Minuto ( Presente Tattico ) -->
216
+ <div class="debug-col">
217
+ <div class="debug-title">Presente Mobile (Active Buffer)</div>
218
+ <div id="debug-buffer-list" style="font-size: 10px; line-height: 1.3;">
219
+ <!-- Popolato via JS -->
220
+ </div>
221
+ </div>
222
+ </div>
223
+
224
+ <script>
225
+ // --- 1. CONFIGURAZIONI E STATO DI DEFAULT ---
226
+ let CONFIG = {
227
+ alarms: { depthDanger: 2.5, depthWarning: 3.5 },
228
+ graphs: { reef1: 18, reef2: 24, historyMinutes: 10 },
229
+ averaging: { minSpeed: 0.5, stabilityThreshold: 0.95 }
230
+ };
231
+ let REEF1 = 18; let REEF2 = 24; let REEF3 = 30;
232
+ const CALM_THRESHOLD_KTS = 1.5;
233
+ const PRESSURE_FILTER_RATIO = 0.40;
234
+
235
+ let socket = null;
236
+ let reconnectDelay = 1000;
237
+ let futureForecast = null;
238
+ let windRadarSlots = [];
239
+
240
+ const store = {
241
+ raw: {},
242
+ timestamps: {},
243
+ twdMinuteBuffer: [],
244
+ twsMinuteBuffer: []
245
+ };
246
+
247
+ // Raggi geometrici speculari compressi di sicurezza (Massimo raggio 116.4px)
248
+ const ringRadii = [59.0, 67.2, 75.4, 83.6, 91.8, 100.0, 108.2, 116.4];
249
+ const chronologicalMapping = [0, 1, 2, 3, 4, 5, 6, 7];
250
+ const ARC_STROKE_WIDTH = 5.0;
251
+ const BORDER_STROKE_WIDTH = ARC_STROKE_WIDTH + 2; // 7px
252
+
253
+ const radToDeg = (rad) => (rad * 180 / Math.PI);
254
+ const degToRad = (deg) => (deg * Math.PI / 180);
255
+ const msToKts = (ms) => ms * 1.94384;
256
+
257
+ function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
258
+ const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
259
+ return {
260
+ x: centerX + (radius * Math.cos(angleInRadians)),
261
+ y: centerY + (radius * Math.sin(angleInRadians))
262
+ };
263
+ }
264
+
265
+ function describeArc(centerX, centerY, radius, startAngle, endAngle) {
266
+ const start = polarToCartesian(centerX, centerY, radius, endAngle);
267
+ const end = polarToCartesian(centerX, centerY, radius, startAngle);
268
+
269
+ let arcSweep = endAngle - startAngle;
270
+ if (arcSweep < 0) arcSweep += 360;
271
+
272
+ const largeArcFlag = arcSweep <= 180 ? "0" : "1";
273
+
274
+ return [
275
+ "M", start.x, start.y,
276
+ "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
277
+ ].join(" ");
278
+ }
279
+
280
+ // --- 2. GESTIONE SEGNALI IN TEMPO REALE ---
281
+ function processIncomingDelta(path, val, source, timeMs) {
282
+ const now = timeMs || Date.now();
283
+ store.timestamps[path] = now;
284
+ store.raw[path] = val;
285
+
286
+ if (path === 'navigation.speedThroughWater') document.getElementById('debug-stw').innerText = msToKts(val).toFixed(1);
287
+ else if (path === 'navigation.speedOverGround') document.getElementById('debug-sog').innerText = msToKts(val).toFixed(1);
288
+ else if (path === 'navigation.headingTrue') document.getElementById('debug-hdg').innerText = Math.round(radToDeg(val)) + '°';
289
+ else if (path === 'navigation.courseOverGroundTrue') document.getElementById('debug-cog').innerText = Math.round(radToDeg(val)) + '°';
290
+ else if (path === 'environment.wind.speedApparent') document.getElementById('debug-aws').innerText = msToKts(val).toFixed(1);
291
+ else if (path === 'environment.wind.angleApparent') document.getElementById('debug-awa').innerText = Math.round(radToDeg(val)) + '°';
292
+ else if (path === 'navigation.position') {
293
+ document.getElementById('debug-gps').innerText = val.latitude.toFixed(4) + '; ' + val.longitude.toFixed(4);
294
+ }
295
+
296
+ const aws = store.raw["environment.wind.speedApparent"];
297
+ const awa = store.raw["environment.wind.angleApparent"];
298
+ if (aws !== undefined && awa !== undefined) {
299
+ const stw = store.raw["navigation.speedThroughWater"] || 0;
300
+ const awsKts = msToKts(aws);
301
+ const stwKts = msToKts(stw);
302
+
303
+ const tw_water_x = awsKts * Math.cos(awa) - stwKts;
304
+ const tw_water_y = awsKts * Math.sin(awa);
305
+
306
+ const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
307
+ const twa = Math.atan2(tw_water_y, tw_water_x);
308
+ const hdg = store.raw["navigation.headingTrue"] || 0;
309
+ const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
310
+
311
+ document.getElementById('debug-tws').innerText = tws.toFixed(1);
312
+ document.getElementById('debug-twa').innerText = Math.round(radToDeg(twa)) + '°';
313
+ document.getElementById('debug-twd').innerText = Math.round(radToDeg(twd)) + '°';
314
+ }
315
+ }
316
+
317
+ // --- 3. RETE E SINCRONIZZAZIONE REST ---
318
+ function getApiUrl(path) {
319
+ if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
320
+ return `http://192.168.111.240:3000${path}`;
321
+ }
322
+ return path;
323
+ }
324
+
325
+ async function fetchConfigAndHistory() {
326
+ try {
327
+ const configRes = await fetch(getApiUrl('/rotevista-config'));
328
+ if (configRes.ok) {
329
+ const cfg = await configRes.json();
330
+ if (cfg.graphs) {
331
+ CONFIG.graphs = cfg.graphs;
332
+ REEF1 = cfg.graphs.reef1 || 18;
333
+ REEF2 = cfg.graphs.reef2 || 24;
334
+ REEF3 = REEF2 + (REEF2 - REEF1);
335
+
336
+ document.getElementById('debug-r1').innerText = REEF1;
337
+ document.getElementById('debug-r2').innerText = REEF2;
338
+ document.getElementById('debug-r3').innerText = REEF3;
339
+ }
340
+ }
341
+
342
+ const historyRes = await fetch(getApiUrl('/rotevista-history'));
343
+ if (historyRes.ok) {
344
+ const data = await historyRes.json();
345
+ if (data.windRadarSlots) windRadarSlots = data.windRadarSlots;
346
+ if (data.futureForecast) {
347
+ futureForecast = data.futureForecast;
348
+ document.getElementById('debug-future').innerText = `TWD: ${Math.round(radToDeg(futureForecast.twd))}°, TWS: ${futureForecast.tws.toFixed(1)} kts`;
349
+ }
350
+ if (data['navigation.position']) {
351
+ const val = data['navigation.position'];
352
+ document.getElementById('debug-gps').innerText = val.latitude.toFixed(4) + '; ' + val.longitude.toFixed(4);
353
+ }
354
+
355
+ // BUG RISOLTO CHIRURGICAMENTE: Allineamento chiavi REST storiche twd e tws sintonizzate!
356
+ if (data.twd) store.twdMinuteBuffer = data.twd;
357
+ if (data.tws) store.twsMinuteBuffer = data.tws;
358
+ }
359
+ } catch (err) {
360
+ console.warn("⚠️ Errore sincronizzazione storico:", err);
361
+ }
362
+ }
363
+
364
+ function connect() {
365
+ let addr = window.location.host || "192.168.111.240:3000";
366
+ document.getElementById('debug-ip').innerText = addr;
367
+
368
+ socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
369
+
370
+ socket.onopen = async () => {
371
+ document.getElementById('network-status-text').innerText = "ONLINE";
372
+ const badge = document.getElementById('debug-status');
373
+ badge.innerText = "ONLINE";
374
+ badge.className = "status-badge status-online";
375
+ reconnectDelay = 1000;
376
+
377
+ await fetchConfigAndHistory();
378
+ renderRadar();
379
+ };
380
+
381
+ socket.onmessage = (e) => {
382
+ const d = JSON.parse(e.data);
383
+ if (d.updates) {
384
+ d.updates.forEach(u => {
385
+ const timeMs = u.timestamp ? new Date(u.timestamp).getTime() : Date.now();
386
+ let sourceLabel = u.$source || (u.source ? (u.source.label || "N2K") : "NMEA");
387
+ if (u.values) {
388
+ u.values.forEach(v => {
389
+ 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
+ });
416
+ }
417
+ });
418
+ }
419
+ };
420
+
421
+ socket.onclose = () => {
422
+ document.getElementById('network-status-text').innerText = "DISCONNESSO";
423
+ const badge = document.getElementById('debug-status');
424
+ badge.innerText = "OFFLINE";
425
+ badge.className = "status-badge status-offline";
426
+ setTimeout(connect, reconnectDelay);
427
+ reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
428
+ };
429
+ }
430
+
431
+ // --- 4. MOTORE DI CALCOLO DELL'ANELLO 1 (Presente Mobile) ---
432
+ function calculateActive30mRing() {
433
+ const now = Date.now();
434
+ const start30m = now - 1800000;
435
+
436
+ const twdRecent = store.twdMinuteBuffer.filter(p => p.time >= start30m);
437
+ const twsRecent = store.twsMinuteBuffer.filter(p => p.time >= start30m);
438
+
439
+ if (twdRecent.length === 0) return null;
440
+
441
+ const twsVals = twsRecent.map(p => p.val).filter(v => isFinite(v));
442
+ const maxTws = twsVals.length > 0 ? Math.max(...twsVals) : 0;
443
+
444
+ if (maxTws < CALM_THRESHOLD_KTS) {
445
+ return { twsPeak: maxTws, twdMin: 0, twdMax: 360, isCalm: true };
446
+ }
447
+
448
+ let allAngles = [];
449
+ twdRecent.forEach(p => {
450
+ allAngles.push(p.val);
451
+ allAngles.push(p.min);
452
+ allAngles.push(p.max);
453
+ });
454
+
455
+ let sumSin = 0; let sumCos = 0;
456
+ allAngles.forEach(a => { sumSin += Math.sin(a); sumCos += Math.cos(a); });
457
+ const avgAngle = Math.atan2(sumSin, sumCos);
458
+ const finalAvg = (avgAngle + Math.PI * 2) % (Math.PI * 2);
459
+
460
+ let diffs = allAngles.map(a => {
461
+ let diff = a - finalAvg;
462
+ return Math.atan2(Math.sin(diff), Math.cos(diff));
463
+ });
464
+
465
+ diffs.sort((a, b) => a - b);
466
+ const trimCount = Math.floor(diffs.length * 0.05);
467
+ const activeDiffs = diffs.slice(trimCount, diffs.length - trimCount);
468
+ const finalDiffs = activeDiffs.length > 0 ? activeDiffs : diffs;
469
+
470
+ const minDiff = Math.min(...finalDiffs);
471
+ const maxDiff = Math.max(...finalDiffs);
472
+
473
+ const finalMinDeg = Math.round(radToDeg((finalAvg + minDiff + Math.PI * 2) % (Math.PI * 2)));
474
+ const finalMaxDeg = Math.round(radToDeg((finalAvg + maxDiff + Math.PI * 2) % (Math.PI * 2)));
475
+
476
+ return {
477
+ twdMin: finalMinDeg,
478
+ twdMax: finalMaxDeg,
479
+ twsPeak: maxTws,
480
+ isCalm: false
481
+ };
482
+ }
483
+
484
+ // --- 5. MOTORE GRAFICO DI DISEGNO ---
485
+ function drawCompassTicks() {
486
+ const ticksGroup = document.getElementById('ticks');
487
+ ticksGroup.innerHTML = '';
488
+ for (let i = 0; i < 360; i += 10) {
489
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
490
+ const isMajor = (i % 30 === 0);
491
+
492
+ line.setAttribute("x1", "200");
493
+ line.setAttribute("y1", "40");
494
+ line.setAttribute("x2", "200");
495
+ line.setAttribute("y2", isMajor ? "60" : "50");
496
+
497
+ line.setAttribute("stroke", isMajor ? "#000000" : "#bbbbbb");
498
+ line.setAttribute("stroke-width", isMajor ? "2" : "1");
499
+ line.setAttribute("transform", `rotate(${i}, 200, 200)`);
500
+ ticksGroup.appendChild(line);
501
+ }
502
+ }
503
+
504
+ // --- SCALA COLORI PROSSIMITÀ DIURNA SPECIALE (8 STEP) ---
505
+ function getChordAlignedGradient(id, radius, tws, startAngle, endAngle) {
506
+ const startPt = polarToCartesian(200, 200, radius, endAngle);
507
+ const endPt = polarToCartesian(200, 200, radius, startAngle);
508
+
509
+ let stops = '';
510
+ const R1 = REEF1; const R2 = REEF2; const R3 = REEF3;
511
+ const colorEdge = 'rgb(240, 240, 240)';
512
+
513
+ // 1. BIANCO SOLIDO (0 - 40% di R1)
514
+ if (tws < R1 * 0.4) {
515
+ return { type: 'solid', color: '#ffffff' };
516
+ }
517
+ // 2. TRANSIZIONE BIANCO -> VERDE (40% - 60% di R1)
518
+ else if (tws >= R1 * 0.4 && tws < R1 * 0.6) {
519
+ stops = `
520
+ <stop offset="0%" stop-color="#ffffff" />
521
+ <stop offset="50%" stop-color="#00C851" />
522
+ <stop offset="100%" stop-color="#ffffff" />
523
+ `;
524
+ }
525
+ // 3. VERDE SOLIDO (60% - 75% di R1)
526
+ else if (tws >= R1 * 0.6 && tws < R1 * 0.75) {
527
+ return { type: 'solid', color: '#00C851' };
528
+ }
529
+ // 4. TRANSIZIONE VERDE -> ARANCIONE (75% R1 - R1)
530
+ else if (tws >= R1 * 0.75 && tws < R1) {
531
+ stops = `
532
+ <stop offset="0%" stop-color="#00C851" />
533
+ <stop offset="50%" stop-color="#ff9800" />
534
+ <stop offset="100%" stop-color="#00C851" />
535
+ `;
536
+ }
537
+ // 5. ARANCIONE SOLIDO (R1 -> Metà di R2)
538
+ else if (tws >= R1 && tws < R1 + (R2 - R1) * 0.5) {
539
+ return { type: 'solid', color: '#ff9800' };
540
+ }
541
+ // 6. TRANSIZIONE ARANCIONE -> ROSSO (Metà R2 -> R2) (Arancio Dorato -> Cremisi)
542
+ else if (tws >= R1 + (R2 - R1) * 0.5 && tws < R2) {
543
+ stops = `
544
+ <stop offset="0%" stop-color="#ffaa00" /> <!-- Arancio Dorato Brillante sui bordi -->
545
+ <stop offset="50%" stop-color="#d50000" /> <!-- Rosso Cremisi Intenso e saturo al centro -->
546
+ <stop offset="100%" stop-color="#ffaa00" />
547
+ `;
548
+ }
549
+ // 7. ROSSO SOLIDO (R2 -> Metà di R3)
550
+ else if (tws >= R2 && tws < R2 + (R3 - R2) * 0.5) {
551
+ return { type: 'solid', color: '#ff3b30' };
552
+ }
553
+ // 8. TRANSIZIONE ROSSO -> VIOLA (Metà R3 -> R3)
554
+ else if (tws >= R2 + (R3 - R2) * 0.5 && tws < R3) {
555
+ stops = `
556
+ <stop offset="0%" stop-color="#ff3b30" />
557
+ <stop offset="50%" stop-color="#9c27b0" />
558
+ <stop offset="100%" stop-color="#ff3b30" />
559
+ `;
560
+ }
561
+ // 9. VIOLA SOLIDO (Oltre R3)
562
+ else {
563
+ return { type: 'solid', color: '#9c27b0' };
564
+ }
565
+
566
+ const xml = `
567
+ <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">
568
+ ${stops}
569
+ </linearGradient>
570
+ `;
571
+
572
+ return { type: 'gradient', xml: xml, url: `url(#${id})` };
573
+ }
574
+
575
+ // --- SISTEMA DI AGGIORNAMENTO DEL RADAR DI DIAGNOSTICA IN TEMPO REALE ---
576
+ function updateDebugPanel() {
577
+ // 1. Aggiorna la lista degli Slot storici precalcolati dal server ( windRadarSlots )
578
+ const slotsCol = document.getElementById('debug-slots-list');
579
+ if (slotsCol) {
580
+ if (windRadarSlots.length === 0) {
581
+ slotsCol.innerHTML = "<span style='color:#999'>RAM vuota sul server. In attesa del primo scatto orologio a :00 o :30...</span>";
582
+ } else {
583
+ slotsCol.innerHTML = windRadarSlots.map(s => {
584
+ const timeStr = new Date(s.timestamp).toLocaleTimeString();
585
+ const minDeg = Math.round(radToDeg(s.twdMin));
586
+ const maxDeg = Math.round(radToDeg(s.twdMax));
587
+ return `<div>• ${timeStr}: TWD ${minDeg}°-${maxDeg}°, TWS Peak ${s.twsPeak.toFixed(1)}kn</div>`;
588
+ }).join('');
589
+ }
590
+ }
591
+
592
+ // 2. Aggiorna lo stato dei minuti del presente accumulati in tempo reale ( per l'Anello 1 )
593
+ const bufferCol = document.getElementById('debug-buffer-list');
594
+ if (bufferCol) {
595
+ if (store.twdMinuteBuffer.length === 0) {
596
+ bufferCol.innerHTML = "<span style='color:#999'>In attesa di dati in streaming da Signal K...</span>";
597
+ } else {
598
+ const start30m = Date.now() - 1800000;
599
+ const twdRecent = store.twdMinuteBuffer.filter(p => p.time >= start30m);
600
+
601
+ bufferCol.innerHTML = `<div>Buffer size: <strong>${twdRecent.length}/30 min</strong> attivi</div>` +
602
+ twdRecent.slice(-5).map(p => {
603
+ const timeStr = new Date(p.time).toLocaleTimeString();
604
+ const val = Math.round(radToDeg(p.val));
605
+ const min = Math.round(radToDeg(p.min));
606
+ const max = Math.round(radToDeg(p.max));
607
+ return `<div>• ${timeStr}: Avg ${val}°, Range ${min}°-${max}°</div>`;
608
+ }).join('');
609
+ }
610
+ }
611
+ }
612
+
613
+ function renderRadar() {
614
+ const ringsContainer = document.getElementById('radar-rings');
615
+ const defsContainer = document.getElementById('radar-gradients');
616
+
617
+ defsContainer.innerHTML = `
618
+ <clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
619
+ <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
620
+ <stop offset="0%" style="stop-color:#333333;stop-opacity:1" />
621
+ <stop offset="100%" style="stop-color:#999999;stop-opacity:1" />
622
+ </linearGradient>
623
+ <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
624
+ <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
625
+ </filter>
626
+ `;
627
+ ringsContainer.innerHTML = '';
628
+
629
+ // Imposta dinamicamente i raggi in base a X per renderli scalabili
630
+ const oraOrbit = document.getElementById('ora-orbit');
631
+ if (oraOrbit) oraOrbit.setAttribute("r", ringRadii[1]);
632
+
633
+ const now = Date.now();
634
+ const current30mSlot = Math.floor(now / 1800000) * 1800000;
635
+
636
+ const radarDataList = [];
637
+
638
+ // 1. ANELLO 0 (Centro - Previsione Futura Open-Meteo)
639
+ if (futureForecast) {
640
+ const twdDeg = Math.round(radToDeg(futureForecast.twd));
641
+ radarDataList.push({
642
+ twdMin: (twdDeg - 5 + 360) % 360,
643
+ twdMax: (twdDeg + 5 + 360) % 360,
644
+ twsPeak: futureForecast.tws,
645
+ isFuture: true
646
+ });
647
+ } else {
648
+ radarDataList.push(null);
649
+ }
650
+
651
+ // 2. ANELLO 1 (Presente Mobile - Calcolato dal client in tempo reale)
652
+ const activeRing = calculateActive30mRing();
653
+ radarDataList.push(activeRing);
654
+
655
+ // 3. ANELLI da 2 a 7 (I 6 slot storici da 30 min passati salvati dal server, pari a 3 ore)
656
+ for (let i = 1; i <= 6; i++) {
657
+ const targetTimestamp = current30mSlot - (i * 1800000);
658
+ const matchedSlot = windRadarSlots.find(s => s.timestamp === targetTimestamp);
659
+
660
+ if (matchedSlot) {
661
+ radarDataList.push({
662
+ twdMin: Math.round(radToDeg(matchedSlot.twdMin)),
663
+ twdMax: Math.round(radToDeg(matchedSlot.twdMax)),
664
+ twsPeak: matchedSlot.twsPeak,
665
+ isCalm: matchedSlot.twsPeak < CALM_THRESHOLD_KTS
666
+ });
667
+ } else {
668
+ radarDataList.push(null);
669
+ }
670
+ }
671
+
672
+ document.getElementById('debug-rings-count').innerText = radarDataList.filter(p => p !== null).length + "/8";
673
+
674
+ // Disegno effettivo degli archi
675
+ radarDataList.forEach((data, index) => {
676
+ if (!data) return;
677
+
678
+ const radius = ringRadii[index];
679
+ const gradId = `chord-gradient-${index}`;
680
+
681
+ // BUG RISOLTO CHIRURGICAMENTE: Rimosso il sbiadimento grigio (opacità sempre fissa a 1 per colore solido e brillante)
682
+ const opacityValue = 1;
683
+
684
+ // Caso A: Calma Piatta
685
+ if (data.isCalm || data.twsPeak < CALM_THRESHOLD_KTS) {
686
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
687
+ circle.setAttribute("cx", "200");
688
+ circle.setAttribute("cy", "200");
689
+ circle.setAttribute("r", radius);
690
+ circle.setAttribute("fill", "none");
691
+ circle.setAttribute("stroke", "#b0bec5");
692
+ circle.setAttribute("stroke-width", "1.2");
693
+ circle.setAttribute("stroke-dasharray", "4, 4");
694
+ ringsContainer.appendChild(circle);
695
+ return;
696
+ }
697
+
698
+ // Caso B: Arco Direzionale
699
+ const grad = getChordAlignedGradient(gradId, radius, data.twsPeak, data.twdMin, data.twdMax);
700
+ let strokeColor = '';
701
+
702
+ if (grad.type === 'gradient') {
703
+ defsContainer.innerHTML += grad.xml;
704
+ strokeColor = grad.url;
705
+ } else {
706
+ strokeColor = grad.color;
707
+ }
708
+
709
+ const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
710
+
711
+ const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
712
+ borderPath.setAttribute("d", pathData);
713
+ borderPath.setAttribute("fill", "none");
714
+ borderPath.setAttribute("stroke", "#000000");
715
+ borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
716
+ borderPath.setAttribute("stroke-linecap", "round");
717
+ borderPath.setAttribute("opacity", opacityValue);
718
+ if (data.isFuture) borderPath.setAttribute("stroke-dasharray", "10, 6");
719
+ ringsContainer.appendChild(borderPath);
720
+
721
+ const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
722
+ mainPath.setAttribute("d", pathData);
723
+ mainPath.setAttribute("fill", "none");
724
+ mainPath.setAttribute("stroke", strokeColor);
725
+ mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
726
+ mainPath.setAttribute("stroke-linecap", "round");
727
+ mainPath.setAttribute("opacity", opacityValue);
728
+ if (data.isFuture) mainPath.setAttribute("stroke-dasharray", "10, 6");
729
+ ringsContainer.appendChild(mainPath);
730
+ });
731
+
732
+ // Aggiorna il pannello di debug ad ogni ridisegno del radar
733
+ updateDebugPanel();
734
+ }
735
+
736
+ // --- 6. CICLO DI VITA E AVVIO ---
737
+ function init() {
738
+ drawCompassTicks();
739
+ connect();
740
+ // Rinfresco periodico del radar e del pannello di debug ogni 5 secondi
741
+ setInterval(renderRadar, 5000);
742
+ }
743
+
744
+ window.onload = init;
745
+ </script>
746
+ </body>
747
+ </html>