@sailingrotevista/rotevista-dash 6.2.6 → 6.2.8
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/index.js +88 -80
- package/package.json +1 -1
- package/radar.html +122 -83
package/index.js
CHANGED
|
@@ -559,21 +559,22 @@ module.exports = function (app) {
|
|
|
559
559
|
* Raccoglie i 30 record da 1 minuto, estrae i picchi, unisce gli angoli
|
|
560
560
|
* e applica la scrematura del percentile al 5% prima di salvare lo slot.
|
|
561
561
|
*/
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
562
|
+
function freeze30mSlot(slotTimestamp) {
|
|
563
|
+
const startTime = slotTimestamp;
|
|
564
|
+
const endTime = slotTimestamp + 1800000; // 30 minuti in millisecondi
|
|
565
565
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
566
|
+
// 1. Estrae i record storici del TWD e del TWS che ricadono in quella mezz'ora
|
|
567
|
+
const twdPoints = histories['twd'].filter(p => p.time >= startTime && p.time < endTime);
|
|
568
|
+
const twsPoints = histories['tws'].filter(p => p.time >= startTime && p.time < endTime);
|
|
569
569
|
|
|
570
|
-
|
|
570
|
+
if (twdPoints.length === 0) return; // Se non ci sono dati, salta lo slot
|
|
571
571
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
572
|
+
// 2. Calcola il vento massimo sostenuto (in nodi) registrato nel periodo
|
|
573
|
+
const twsVals = twsPoints.map(p => p.val).filter(v => isFinite(v));
|
|
574
|
+
const maxTws = twsVals.length > 0 ? Math.max(...twsVals) : 0;
|
|
575
|
+
const minTws = twsVals.length > 0 ? Math.min(...twsVals) : 0; // Chirurgico: Calcoliamo il vento minimo del periodo
|
|
575
576
|
|
|
576
|
-
|
|
577
|
+
// 3. Estrae tutti gli estremi angolari catturati minuto per minuto
|
|
577
578
|
let allAngles = [];
|
|
578
579
|
twdPoints.forEach(p => {
|
|
579
580
|
allAngles.push(p.val);
|
|
@@ -611,15 +612,16 @@ module.exports = function (app) {
|
|
|
611
612
|
const finalMin = (finalAvg + minDiff + Math.PI * 2) % (Math.PI * 2);
|
|
612
613
|
const finalMax = (finalAvg + maxDiff + Math.PI * 2) % (Math.PI * 2);
|
|
613
614
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
615
|
+
// 7. Salva l'arco compresso e pulito nello store dedicato
|
|
616
|
+
windRadarSlots.push({
|
|
617
|
+
timestamp: startTime,
|
|
618
|
+
twdMin: finalMin,
|
|
619
|
+
twdMax: finalMax,
|
|
620
|
+
twsPeak: maxTws,
|
|
621
|
+
twsMin: minTws // Chirurgico: Salviamo il vento minimo per poter tracciare la variabilità (Gust Factor)
|
|
622
|
+
});
|
|
621
623
|
|
|
622
|
-
|
|
624
|
+
// Pruning: manteniamo in RAM solo le ultime 6 ore (12 slot)
|
|
623
625
|
while (windRadarSlots.length > 12) {
|
|
624
626
|
windRadarSlots.shift();
|
|
625
627
|
}
|
|
@@ -631,56 +633,58 @@ module.exports = function (app) {
|
|
|
631
633
|
* fetchOpenMeteoForecast: Recupera le previsioni orarie accoppiate (Seamless) da Open-Meteo.
|
|
632
634
|
* Utilizza il modello integrato /forecast per evitare zone d'ombra in rada, con tempo forzato in UTC.
|
|
633
635
|
*/
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
let data = '';
|
|
654
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
655
|
-
res.on('end', () => {
|
|
656
|
-
try {
|
|
657
|
-
const parsed = JSON.parse(data);
|
|
658
|
-
if (!parsed.hourly || !parsed.hourly.time) {
|
|
659
|
-
app.error('[Open-Meteo] Invalid API response format');
|
|
660
|
-
lastForecast30mSlot = 0;
|
|
636
|
+
function fetchOpenMeteoForecast(position, current30mSlot) {
|
|
637
|
+
if (!position || position.latitude === undefined || position.longitude === undefined) return;
|
|
638
|
+
|
|
639
|
+
const lat = position.latitude;
|
|
640
|
+
const lon = position.longitude;
|
|
641
|
+
|
|
642
|
+
// Chirurgico: Aggiunto wind_gusts_10m alla chiamata per ottenere l'intensità delle raffiche previste
|
|
643
|
+
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&hourly=wind_speed_10m,wind_direction_10m,wind_gusts_10m&wind_speed_unit=kn&timezone=GMT&forecast_days=2`;
|
|
644
|
+
|
|
645
|
+
lastForecast30mSlot = current30mSlot; // Aggiorna preventivamente lo slot per evitare chiamate simultanee in caso di rallentamento di rete
|
|
646
|
+
|
|
647
|
+
https.get(url, (res) => {
|
|
648
|
+
if (res.statusCode !== 200) {
|
|
649
|
+
app.error(`[Open-Meteo] HTTP Error: ${res.statusCode}`);
|
|
650
|
+
res.resume();
|
|
651
|
+
lastForecast30mSlot = 0; // Reset in caso di errore per permettere un tentativo al prossimo pacchetto GPS
|
|
661
652
|
return;
|
|
662
653
|
}
|
|
663
654
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
// Convertiamo la direzione del vento da gradi (0-360) a radianti (0-2PI)
|
|
676
|
-
const twdRad = (directions[i] * Math.PI) / 180;
|
|
655
|
+
let data = '';
|
|
656
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
657
|
+
res.on('end', () => {
|
|
658
|
+
try {
|
|
659
|
+
const parsed = JSON.parse(data);
|
|
660
|
+
if (!parsed.hourly || !parsed.hourly.time) {
|
|
661
|
+
app.error('[Open-Meteo] Invalid API response format');
|
|
662
|
+
lastForecast30mSlot = 0;
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
677
665
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
666
|
+
const times = parsed.hourly.time;
|
|
667
|
+
const speeds = parsed.hourly.wind_speed_10m;
|
|
668
|
+
const directions = parsed.hourly.wind_direction_10m;
|
|
669
|
+
const gusts = parsed.hourly.wind_gusts_10m || []; // Chirurgico: Catturiamo le raffiche dal payload JSON
|
|
670
|
+
|
|
671
|
+
// Costruiamo la serie storica delle previsioni in formato UTC
|
|
672
|
+
const forecastList = [];
|
|
673
|
+
for (let i = 0; i < times.length; i++) {
|
|
674
|
+
// Forziamo il parsing UTC aggiungendo la dicitura 'Z' alla stringa ISO prodotta da Open-Meteo
|
|
675
|
+
const epoch = Date.parse(times[i] + "Z");
|
|
676
|
+
if (isNaN(epoch)) continue;
|
|
677
|
+
|
|
678
|
+
// Convertiamo la direzione del vento da gradi (0-360) a radianti (0-2PI)
|
|
679
|
+
const twdRad = (directions[i] * Math.PI) / 180;
|
|
680
|
+
|
|
681
|
+
forecastList.push({
|
|
682
|
+
time: epoch,
|
|
683
|
+
tws: speeds[i], // Già in nodi grazie ai parametri della chiamata
|
|
684
|
+
twd: twdRad,
|
|
685
|
+
gust: gusts[i] !== undefined ? gusts[i] : speeds[i] // Chirurgico: Memorizziamo la raffica oraria (con fallback sulla velocità media)
|
|
686
|
+
});
|
|
687
|
+
}
|
|
684
688
|
|
|
685
689
|
// Calcola l'interpolazione per la mezz'ora futura basandosi sullo slot corrente dell'orologio
|
|
686
690
|
calculateInterpolatedFuture(forecastList);
|
|
@@ -721,25 +725,29 @@ module.exports = function (app) {
|
|
|
721
725
|
}
|
|
722
726
|
}
|
|
723
727
|
|
|
724
|
-
|
|
725
|
-
|
|
728
|
+
if (s1 && s2) {
|
|
729
|
+
const ratio = (targetTime - s1.time) / (s2.time - s1.time);
|
|
726
730
|
|
|
727
|
-
|
|
728
|
-
|
|
731
|
+
// 1. Interpolazione Lineare Velocità (TWS)
|
|
732
|
+
const interpolatedTws = s1.tws + (s2.tws - s1.tws) * ratio;
|
|
729
733
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
734
|
+
// 2. Interpolazione Circolare Vettoriale Direzione (TWD) per evitare l'effetto sfasamento a 0/360°
|
|
735
|
+
const diff = Math.atan2(Math.sin(s2.twd - s1.twd), Math.cos(s2.twd - s1.twd));
|
|
736
|
+
const interpolatedTwd = (s1.twd + diff * ratio + Math.PI * 2) % (Math.PI * 2);
|
|
733
737
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
738
|
+
// 3. Interpolazione Lineare Raffiche (Gust)
|
|
739
|
+
const interpolatedGust = s1.gust + (s2.gust - s1.gust) * ratio; // Chirurgico: Calcolo interpolato della raffica futura
|
|
740
|
+
|
|
741
|
+
// Salviamo le previsioni future nel server
|
|
742
|
+
futureForecast = {
|
|
743
|
+
timestamp: targetTime,
|
|
744
|
+
tws: interpolatedTws,
|
|
745
|
+
twd: interpolatedTwd,
|
|
746
|
+
gust: interpolatedGust // Chirurgico: Aggiunta la raffica nel pacchetto dati della previsione
|
|
747
|
+
};
|
|
740
748
|
|
|
741
|
-
|
|
742
|
-
|
|
749
|
+
app.debug(`🔮 [Open-Meteo] Target forecast interpolated for ${new Date(targetTime).toLocaleTimeString()}: TWD ${Math.round(interpolatedTwd * 180 / Math.PI)}°, TWS ${interpolatedTws.toFixed(1)} kts (Gust: ${interpolatedGust.toFixed(1)} kts)`);
|
|
750
|
+
} else {
|
|
743
751
|
app.error('[Open-Meteo] Forecast matching slots not found for target time');
|
|
744
752
|
}
|
|
745
753
|
}
|
package/package.json
CHANGED
package/radar.html
CHANGED
|
@@ -350,14 +350,16 @@
|
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
const historyRes = await fetch(getApiUrl('/rotevista-history'));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
353
|
+
if (historyRes.ok) {
|
|
354
|
+
const data = await historyRes.json();
|
|
355
|
+
if (data.windRadarSlots) windRadarSlots = data.windRadarSlots;
|
|
356
|
+
if (data.futureForecast) {
|
|
357
|
+
futureForecast = data.futureForecast;
|
|
358
|
+
// Chirurgico: Formattazione testuale avanzata che estrae ed espone le raffiche (gusts) previste
|
|
359
|
+
const gustVal = futureForecast.gust ? futureForecast.gust.toFixed(1) : futureForecast.tws.toFixed(1);
|
|
360
|
+
document.getElementById('debug-future').innerText = `TWD: ${Math.round(radToDeg(futureForecast.twd))}°, TWS: ${futureForecast.tws.toFixed(1)} kts (Gusts: ${gustVal} kts)`;
|
|
361
|
+
}
|
|
362
|
+
if (data['navigation.position']) {
|
|
361
363
|
const val = data['navigation.position'];
|
|
362
364
|
document.getElementById('debug-gps').innerText = val.latitude.toFixed(4) + '; ' + val.longitude.toFixed(4);
|
|
363
365
|
}
|
|
@@ -415,21 +417,22 @@
|
|
|
415
417
|
}
|
|
416
418
|
|
|
417
419
|
// --- 4. MOTORE DI CALCOLO DELL'ANELLO 1 (Presente Mobile) ---
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
420
|
+
function calculateActive30mRing() {
|
|
421
|
+
const now = Date.now();
|
|
422
|
+
const start30m = now - 1800000;
|
|
421
423
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
+
const twdRecent = store.twdMinuteBuffer.filter(p => p.time >= start30m);
|
|
425
|
+
const twsRecent = store.twsMinuteBuffer.filter(p => p.time >= start30m);
|
|
424
426
|
|
|
425
|
-
|
|
427
|
+
if (twdRecent.length === 0) return null;
|
|
426
428
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
+
const twsVals = twsRecent.map(p => p.val).filter(v => isFinite(v));
|
|
430
|
+
const maxTws = twsVals.length > 0 ? Math.max(...twsVals) : 0;
|
|
431
|
+
const minTws = twsVals.length > 0 ? Math.min(...twsVals) : 0; // Chirurgico: Troviamo il vento minimo reale degli ultimi 30 minuti
|
|
429
432
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
+
if (maxTws < CALM_THRESHOLD_KTS) {
|
|
434
|
+
return { twsPeak: maxTws, twsMin: minTws, twdMin: 0, twdMax: 360, isCalm: true };
|
|
435
|
+
}
|
|
433
436
|
|
|
434
437
|
let allAngles = [];
|
|
435
438
|
twdRecent.forEach(p => {
|
|
@@ -457,15 +460,16 @@
|
|
|
457
460
|
const maxDiff = Math.max(...finalDiffs);
|
|
458
461
|
|
|
459
462
|
const finalMinDeg = Math.round(radToDeg((finalAvg + minDiff + Math.PI * 2) % (Math.PI * 2)));
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
463
|
+
const finalMaxDeg = Math.round(radToDeg((finalAvg + maxDiff + Math.PI * 2) % (Math.PI * 2)));
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
twdMin: finalMinDeg,
|
|
467
|
+
twdMax: finalMaxDeg,
|
|
468
|
+
twsPeak: maxTws,
|
|
469
|
+
twsMin: minTws, // Chirurgico: Ritorniamo anche il valore minimo per abilitare la sfumatura sull'Anello 1
|
|
470
|
+
isCalm: false
|
|
471
|
+
};
|
|
472
|
+
}
|
|
469
473
|
|
|
470
474
|
// --- 5. MOTORE GRAFICO DI DISEGNO ---
|
|
471
475
|
function drawCompassTicks() {
|
|
@@ -622,15 +626,16 @@
|
|
|
622
626
|
const radarDataList = [];
|
|
623
627
|
|
|
624
628
|
// 1. ANELLO 0 (Centro - Previsione Futura Open-Meteo)
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
629
|
+
if (futureForecast) {
|
|
630
|
+
const twdDeg = Math.round(radToDeg(futureForecast.twd));
|
|
631
|
+
radarDataList.push({
|
|
632
|
+
// Chirurgico: Impostata una finta ampiezza di 40 gradi (+-20°) per rappresentare l'area di incertezza meteo
|
|
633
|
+
twdMin: (twdDeg - 20 + 360) % 360,
|
|
634
|
+
twdMax: (twdDeg + 20 + 360) % 360,
|
|
635
|
+
twsPeak: futureForecast.tws,
|
|
636
|
+
isFuture: true
|
|
637
|
+
});
|
|
638
|
+
} else {
|
|
634
639
|
radarDataList.push(null);
|
|
635
640
|
}
|
|
636
641
|
|
|
@@ -639,21 +644,23 @@
|
|
|
639
644
|
radarDataList.push(activeRing);
|
|
640
645
|
|
|
641
646
|
// 3. ANELLI da 2 a 7 (I 6 slot storici da 30 min passati salvati dal server, pari a 3 ore)
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
647
|
+
for (let i = 1; i <= 6; i++) {
|
|
648
|
+
const targetTimestamp = current30mSlot - (i * 1800000);
|
|
649
|
+
const matchedSlot = windRadarSlots.find(s => s.timestamp === targetTimestamp);
|
|
650
|
+
|
|
651
|
+
if (matchedSlot) {
|
|
652
|
+
radarDataList.push({
|
|
653
|
+
twdMin: Math.round(radToDeg(matchedSlot.twdMin)),
|
|
654
|
+
twdMax: Math.round(radToDeg(matchedSlot.twdMax)),
|
|
655
|
+
twsPeak: matchedSlot.twsPeak,
|
|
656
|
+
// Chirurgico: Estraiamo il minimo storico con protezione di compatibilità se assente
|
|
657
|
+
twsMin: matchedSlot.twsMin !== undefined ? matchedSlot.twsMin : matchedSlot.twsPeak,
|
|
658
|
+
isCalm: matchedSlot.twsPeak < CALM_THRESHOLD_KTS
|
|
659
|
+
});
|
|
660
|
+
} else {
|
|
661
|
+
radarDataList.push(null);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
657
664
|
|
|
658
665
|
document.getElementById('debug-rings-count').innerText = radarDataList.filter(p => p !== null).length + "/8";
|
|
659
666
|
|
|
@@ -682,37 +689,69 @@
|
|
|
682
689
|
}
|
|
683
690
|
|
|
684
691
|
// Caso B: Arco Direzionale
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
692
|
+
let strokeColor = '';
|
|
693
|
+
|
|
694
|
+
// Chirurgico: Definita la funzione di colorazione universale basata sulle soglie dei Reef di bordo
|
|
695
|
+
const getColorForSpeed = (tws) => {
|
|
696
|
+
const R1 = REEF1; const R2 = REEF2; const R3 = REEF3;
|
|
697
|
+
if (tws < R1 * 0.4) return '#ffffff'; // Bianco (Calma)
|
|
698
|
+
if (tws < R1 * 0.75) return '#00C851'; // Verde (Regolare)
|
|
699
|
+
if (tws < R1) return '#ff9800'; // Arancio (1° Reef Alert)
|
|
700
|
+
if (tws < R2) return '#ffaa00'; // Dorato (Transizione)
|
|
701
|
+
if (tws < R2 + (R3 - R2) * 0.5) return '#ff3b30'; // Rosso (2° Reef Alert)
|
|
702
|
+
return '#9c27b0'; // Viola (Storm / Temporale)
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// Chirurgico: Estraiamo i limiti minimi e massimi dell'arco corrente
|
|
706
|
+
// Se è la previsione, usiamo il vento medio e le raffiche di Open-Meteo, altrimenti i minimi e i picchi in RAM
|
|
707
|
+
const baseTws = data.isFuture && futureForecast ? futureForecast.tws : (data.twsMin !== undefined ? data.twsMin : data.twsPeak);
|
|
708
|
+
const peakTws = data.isFuture && futureForecast ? futureForecast.gust : data.twsPeak;
|
|
709
|
+
|
|
710
|
+
const baseColor = getColorForSpeed(baseTws);
|
|
711
|
+
const peakColor = getColorForSpeed(peakTws);
|
|
712
|
+
|
|
713
|
+
if (baseColor !== peakColor) {
|
|
714
|
+
// Se c'è variabilità d'intensità (raffica), disegnamo il gradiente termico dinamico lungo l'arco (Min -> Max -> Min)
|
|
715
|
+
const startPt = polarToCartesian(200, 200, radius, data.twdMax);
|
|
716
|
+
const endPt = polarToCartesian(200, 200, radius, data.twdMin);
|
|
717
|
+
|
|
718
|
+
const xml = `
|
|
719
|
+
<linearGradient id="${gradId}" x1="${startPt.x.toFixed(1)}" y1="${startPt.y.toFixed(1)}" x2="${endPt.x.toFixed(1)}" y2="${endPt.y.toFixed(1)}" gradientUnits="userSpaceOnUse">
|
|
720
|
+
<stop offset="0%" stop-color="${baseColor}" />
|
|
721
|
+
<stop offset="50%" stop-color="${peakColor}" />
|
|
722
|
+
<stop offset="100%" stop-color="${baseColor}" />
|
|
723
|
+
</linearGradient>
|
|
724
|
+
`;
|
|
725
|
+
defsContainer.innerHTML += xml;
|
|
726
|
+
strokeColor = `url(#${gradId})`;
|
|
727
|
+
} else {
|
|
728
|
+
// Se il vento è costante (base e picco ricadono nella stessa fascia), usiamo il colore solido puro
|
|
729
|
+
strokeColor = baseColor;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
|
|
733
|
+
|
|
734
|
+
// Chirurgico: Se è l'anello futuro (isFuture), non disegniamo il bordo nero per evitare artefatti "a doppia bolla"
|
|
735
|
+
if (!data.isFuture) {
|
|
736
|
+
const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
737
|
+
borderPath.setAttribute("d", pathData);
|
|
738
|
+
borderPath.setAttribute("fill", "none");
|
|
739
|
+
borderPath.setAttribute("stroke", "#000000");
|
|
740
|
+
borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
|
|
741
|
+
borderPath.setAttribute("stroke-linecap", "round");
|
|
742
|
+
borderPath.setAttribute("opacity", opacityValue);
|
|
743
|
+
ringsContainer.appendChild(borderPath);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
747
|
+
mainPath.setAttribute("d", pathData);
|
|
748
|
+
mainPath.setAttribute("fill", "none");
|
|
749
|
+
mainPath.setAttribute("stroke", strokeColor);
|
|
750
|
+
mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
|
|
751
|
+
mainPath.setAttribute("stroke-linecap", "round");
|
|
752
|
+
// Chirurgico: Per l'anello futuro, usiamo un'opacità morbida al 50% (0.5) continua (senza tratteggio) per un effetto "glowing" pulitissimo
|
|
753
|
+
mainPath.setAttribute("opacity", data.isFuture ? "0.5" : opacityValue);
|
|
754
|
+
ringsContainer.appendChild(mainPath);
|
|
716
755
|
});
|
|
717
756
|
|
|
718
757
|
// Aggiorna il pannello di debug ad ogni ridisegno del radar
|