@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.
Files changed (3) hide show
  1. package/index.js +68 -62
  2. package/package.json +1 -1
  3. 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
- 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
- // Modello accoppiato Seamless basato su forecast, con vento espresso in nodi e orario in UTC (GMT)
641
- const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&hourly=wind_speed_10m,wind_direction_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
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
- const times = parsed.hourly.time;
665
- const speeds = parsed.hourly.wind_speed_10m;
666
- const directions = parsed.hourly.wind_direction_10m;
667
-
668
- // Costruiamo la serie storica delle previsioni in formato UTC
669
- const forecastList = [];
670
- for (let i = 0; i < times.length; i++) {
671
- // Forziamo il parsing UTC aggiungendo la dicitura 'Z' alla stringa ISO prodotta da Open-Meteo
672
- const epoch = Date.parse(times[i] + "Z");
673
- if (isNaN(epoch)) continue;
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
- forecastList.push({
679
- time: epoch,
680
- tws: speeds[i], // Già in nodi grazie ai parametri della chiamata
681
- twd: twdRad
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
- if (s1 && s2) {
725
- const ratio = (targetTime - s1.time) / (s2.time - s1.time);
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
- // 1. Interpolazione Lineare Velocità (TWS)
728
- const interpolatedTws = s1.tws + (s2.tws - s1.tws) * ratio;
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
- // 2. Interpolazione Circolare Vettoriale Direzione (TWD) per evitare l'effetto sfasamento a 0/360°
731
- const diff = Math.atan2(Math.sin(s2.twd - s1.twd), Math.cos(s2.twd - s1.twd));
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
- // Salviamo le previsioni future nel server
735
- futureForecast = {
736
- timestamp: targetTime,
737
- tws: interpolatedTws,
738
- twd: interpolatedTwd
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
- app.debug(`🔮 [Open-Meteo] Target forecast interpolated for ${new Date(targetTime).toLocaleTimeString()}: TWD ${Math.round(interpolatedTwd * 180 / Math.PI)}°, TWS ${interpolatedTws.toFixed(1)} kts`);
742
- } else {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "6.2.5",
3
+ "version": "6.2.7",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/radar.html CHANGED
@@ -350,14 +350,16 @@
350
350
  }
351
351
 
352
352
  const historyRes = await fetch(getApiUrl('/rotevista-history'));
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
- document.getElementById('debug-future').innerText = `TWD: ${Math.round(radToDeg(futureForecast.twd))}°, TWS: ${futureForecast.tws.toFixed(1)} kts`;
359
- }
360
- if (data['navigation.position']) {
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
- if (futureForecast) {
626
- const twdDeg = Math.round(radToDeg(futureForecast.twd));
627
- radarDataList.push({
628
- twdMin: (twdDeg - 5 + 360) % 360,
629
- twdMax: (twdDeg + 5 + 360) % 360,
630
- twsPeak: futureForecast.tws,
631
- isFuture: true
632
- });
633
- } else {
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
- const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
698
- borderPath.setAttribute("d", pathData);
699
- borderPath.setAttribute("fill", "none");
700
- borderPath.setAttribute("stroke", "#000000");
701
- borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
702
- borderPath.setAttribute("stroke-linecap", "round");
703
- borderPath.setAttribute("opacity", opacityValue);
704
- if (data.isFuture) borderPath.setAttribute("stroke-dasharray", "10, 6");
705
- ringsContainer.appendChild(borderPath);
706
-
707
- const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
708
- mainPath.setAttribute("d", pathData);
709
- mainPath.setAttribute("fill", "none");
710
- mainPath.setAttribute("stroke", strokeColor);
711
- mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
712
- mainPath.setAttribute("stroke-linecap", "round");
713
- mainPath.setAttribute("opacity", opacityValue);
714
- if (data.isFuture) mainPath.setAttribute("stroke-dasharray", "10, 6");
715
- ringsContainer.appendChild(mainPath);
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