@sailingrotevista/rotevista-dash 6.2.5 → 6.2.7
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 +68 -62
- package/package.json +1 -1
- package/radar.html +41 -36
package/index.js
CHANGED
|
@@ -74,7 +74,7 @@ module.exports = function (app) {
|
|
|
74
74
|
const responseData = {
|
|
75
75
|
...histories,
|
|
76
76
|
windRadarSlots: windRadarSlots,
|
|
77
|
-
futureForecast: futureForecast
|
|
77
|
+
futureForecast: futureForecast,
|
|
78
78
|
'navigation.position': raw['navigation.position'] // Chirurgico: Espone le coordinate GPS correnti per la diagnostica e il radar
|
|
79
79
|
};
|
|
80
80
|
res.json(responseData);
|
|
@@ -631,56 +631,58 @@ module.exports = function (app) {
|
|
|
631
631
|
* fetchOpenMeteoForecast: Recupera le previsioni orarie accoppiate (Seamless) da Open-Meteo.
|
|
632
632
|
* Utilizza il modello integrato /forecast per evitare zone d'ombra in rada, con tempo forzato in UTC.
|
|
633
633
|
*/
|
|
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;
|
|
634
|
+
function fetchOpenMeteoForecast(position, current30mSlot) {
|
|
635
|
+
if (!position || position.latitude === undefined || position.longitude === undefined) return;
|
|
636
|
+
|
|
637
|
+
const lat = position.latitude;
|
|
638
|
+
const lon = position.longitude;
|
|
639
|
+
|
|
640
|
+
// Chirurgico: Aggiunto wind_gusts_10m alla chiamata per ottenere l'intensità delle raffiche previste
|
|
641
|
+
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`;
|
|
642
|
+
|
|
643
|
+
lastForecast30mSlot = current30mSlot; // Aggiorna preventivamente lo slot per evitare chiamate simultanee in caso di rallentamento di rete
|
|
644
|
+
|
|
645
|
+
https.get(url, (res) => {
|
|
646
|
+
if (res.statusCode !== 200) {
|
|
647
|
+
app.error(`[Open-Meteo] HTTP Error: ${res.statusCode}`);
|
|
648
|
+
res.resume();
|
|
649
|
+
lastForecast30mSlot = 0; // Reset in caso di errore per permettere un tentativo al prossimo pacchetto GPS
|
|
661
650
|
return;
|
|
662
651
|
}
|
|
663
652
|
|
|
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;
|
|
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;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
677
663
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
664
|
+
const times = parsed.hourly.time;
|
|
665
|
+
const speeds = parsed.hourly.wind_speed_10m;
|
|
666
|
+
const directions = parsed.hourly.wind_direction_10m;
|
|
667
|
+
const gusts = parsed.hourly.wind_gusts_10m || []; // Chirurgico: Catturiamo le raffiche dal payload JSON
|
|
668
|
+
|
|
669
|
+
// Costruiamo la serie storica delle previsioni in formato UTC
|
|
670
|
+
const forecastList = [];
|
|
671
|
+
for (let i = 0; i < times.length; i++) {
|
|
672
|
+
// Forziamo il parsing UTC aggiungendo la dicitura 'Z' alla stringa ISO prodotta da Open-Meteo
|
|
673
|
+
const epoch = Date.parse(times[i] + "Z");
|
|
674
|
+
if (isNaN(epoch)) continue;
|
|
675
|
+
|
|
676
|
+
// Convertiamo la direzione del vento da gradi (0-360) a radianti (0-2PI)
|
|
677
|
+
const twdRad = (directions[i] * Math.PI) / 180;
|
|
678
|
+
|
|
679
|
+
forecastList.push({
|
|
680
|
+
time: epoch,
|
|
681
|
+
tws: speeds[i], // Già in nodi grazie ai parametri della chiamata
|
|
682
|
+
twd: twdRad,
|
|
683
|
+
gust: gusts[i] !== undefined ? gusts[i] : speeds[i] // Chirurgico: Memorizziamo la raffica oraria (con fallback sulla velocità media)
|
|
684
|
+
});
|
|
685
|
+
}
|
|
684
686
|
|
|
685
687
|
// Calcola l'interpolazione per la mezz'ora futura basandosi sullo slot corrente dell'orologio
|
|
686
688
|
calculateInterpolatedFuture(forecastList);
|
|
@@ -721,25 +723,29 @@ module.exports = function (app) {
|
|
|
721
723
|
}
|
|
722
724
|
}
|
|
723
725
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
+
if (s1 && s2) {
|
|
727
|
+
const ratio = (targetTime - s1.time) / (s2.time - s1.time);
|
|
728
|
+
|
|
729
|
+
// 1. Interpolazione Lineare Velocità (TWS)
|
|
730
|
+
const interpolatedTws = s1.tws + (s2.tws - s1.tws) * ratio;
|
|
726
731
|
|
|
727
|
-
|
|
728
|
-
|
|
732
|
+
// 2. Interpolazione Circolare Vettoriale Direzione (TWD) per evitare l'effetto sfasamento a 0/360°
|
|
733
|
+
const diff = Math.atan2(Math.sin(s2.twd - s1.twd), Math.cos(s2.twd - s1.twd));
|
|
734
|
+
const interpolatedTwd = (s1.twd + diff * ratio + Math.PI * 2) % (Math.PI * 2);
|
|
729
735
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const interpolatedTwd = (s1.twd + diff * ratio + Math.PI * 2) % (Math.PI * 2);
|
|
736
|
+
// 3. Interpolazione Lineare Raffiche (Gust)
|
|
737
|
+
const interpolatedGust = s1.gust + (s2.gust - s1.gust) * ratio; // Chirurgico: Calcolo interpolato della raffica futura
|
|
733
738
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
739
|
+
// Salviamo le previsioni future nel server
|
|
740
|
+
futureForecast = {
|
|
741
|
+
timestamp: targetTime,
|
|
742
|
+
tws: interpolatedTws,
|
|
743
|
+
twd: interpolatedTwd,
|
|
744
|
+
gust: interpolatedGust // Chirurgico: Aggiunta la raffica nel pacchetto dati della previsione
|
|
745
|
+
};
|
|
740
746
|
|
|
741
|
-
|
|
742
|
-
|
|
747
|
+
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)`);
|
|
748
|
+
} else {
|
|
743
749
|
app.error('[Open-Meteo] Forecast matching slots not found for target time');
|
|
744
750
|
}
|
|
745
751
|
}
|
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
|
}
|
|
@@ -622,15 +624,16 @@
|
|
|
622
624
|
const radarDataList = [];
|
|
623
625
|
|
|
624
626
|
// 1. ANELLO 0 (Centro - Previsione Futura Open-Meteo)
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
627
|
+
if (futureForecast) {
|
|
628
|
+
const twdDeg = Math.round(radToDeg(futureForecast.twd));
|
|
629
|
+
radarDataList.push({
|
|
630
|
+
// Chirurgico: Impostata una finta ampiezza di 40 gradi (+-20°) per rappresentare l'area di incertezza meteo
|
|
631
|
+
twdMin: (twdDeg - 20 + 360) % 360,
|
|
632
|
+
twdMax: (twdDeg + 20 + 360) % 360,
|
|
633
|
+
twsPeak: futureForecast.tws,
|
|
634
|
+
isFuture: true
|
|
635
|
+
});
|
|
636
|
+
} else {
|
|
634
637
|
radarDataList.push(null);
|
|
635
638
|
}
|
|
636
639
|
|
|
@@ -694,25 +697,27 @@
|
|
|
694
697
|
|
|
695
698
|
const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
|
|
696
699
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
700
|
+
// Chirurgico: Se è l'anello futuro (isFuture), non disegniamo il bordo nero per evitare artefatti "a doppia bolla"
|
|
701
|
+
if (!data.isFuture) {
|
|
702
|
+
const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
703
|
+
borderPath.setAttribute("d", pathData);
|
|
704
|
+
borderPath.setAttribute("fill", "none");
|
|
705
|
+
borderPath.setAttribute("stroke", "#000000");
|
|
706
|
+
borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
|
|
707
|
+
borderPath.setAttribute("stroke-linecap", "round");
|
|
708
|
+
borderPath.setAttribute("opacity", opacityValue);
|
|
709
|
+
ringsContainer.appendChild(borderPath);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
713
|
+
mainPath.setAttribute("d", pathData);
|
|
714
|
+
mainPath.setAttribute("fill", "none");
|
|
715
|
+
mainPath.setAttribute("stroke", strokeColor);
|
|
716
|
+
mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
|
|
717
|
+
mainPath.setAttribute("stroke-linecap", "round");
|
|
718
|
+
// Chirurgico: Per l'anello futuro, usiamo un'opacità morbida al 50% (0.5) continua (senza tratteggio) per un effetto "glowing" pulitissimo
|
|
719
|
+
mainPath.setAttribute("opacity", data.isFuture ? "0.5" : opacityValue);
|
|
720
|
+
ringsContainer.appendChild(mainPath);
|
|
716
721
|
});
|
|
717
722
|
|
|
718
723
|
// Aggiorna il pannello di debug ad ogni ridisegno del radar
|