@sailingrotevista/rotevista-dash 7.0.1 → 7.0.3
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 +96 -14
- package/charts.js +1 -0
- package/gauge.js +1 -0
- package/index.html +139 -98
- package/index.js +116 -82
- package/package.json +1 -1
- package/style.css +37 -0
- package/utils.js +1 -0
- package/weather-radar.js +281 -0
- /package/{radar.html → debug_sample_radar.html} +0 -0
- /package/{test.html → debug_signalk_connection.html} +0 -0
- /package/{radaar debug.html → debug_weather_radar.html} +0 -0
package/app.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
|
|
8
8
|
* Memoria UI persistente, Modalità Hercules, Focus Split Screen e
|
|
9
9
|
* Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
|
|
10
|
+
*file app.js
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
// ==========================================================================
|
|
@@ -29,7 +30,7 @@ let CONFIG = {
|
|
|
29
30
|
tws: { stdMax: 15, hercSpan: 2, step: 1 },
|
|
30
31
|
depth: { stdMax: 5, hercSpan: 2, step: 1 }
|
|
31
32
|
},
|
|
32
|
-
server: { fallbackIp: "
|
|
33
|
+
server: { fallbackIp: "venus.local:3000" }
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
const RENDER_INTERVAL_MS = 1000;
|
|
@@ -43,6 +44,7 @@ const DASH_VERSION = "6.0"; // Major Update: Server-Side History RAM Logging (Pr
|
|
|
43
44
|
let simulationMode = false;
|
|
44
45
|
let displayModeSog = 'SOG';
|
|
45
46
|
let displayModeTws = 'TWS';
|
|
47
|
+
let activeInstrument = 'gauge'; // Modalità di default all'avvio: 'gauge' (analogico) o 'radar' (storico)
|
|
46
48
|
let socket, renderInterval, simInterval;
|
|
47
49
|
let lastAvgUIUpdate = 0; // Chirurgico: Rimosse audioCtx e lastAlarmTime poiché sono già dichiarate in utils.js
|
|
48
50
|
|
|
@@ -663,13 +665,15 @@ function startDisplayLoop() {
|
|
|
663
665
|
}
|
|
664
666
|
}
|
|
665
667
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
668
|
+
// --- AGGIORNAMENTO DELLA BUSSOLA CENTRALE (BATTERY SAVER A 1Hz) ---
|
|
669
|
+
if (activeInstrument === 'gauge') {
|
|
670
|
+
updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
|
|
674
|
+
if (lastAvgUIUpdate++ % 10 === 0) {
|
|
675
|
+
saveDashboardState();
|
|
676
|
+
}
|
|
673
677
|
|
|
674
678
|
if (lastAvgUIUpdate % 3 === 0) {
|
|
675
679
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
|
|
@@ -801,7 +805,7 @@ async function watchConfigChanges() {
|
|
|
801
805
|
}
|
|
802
806
|
|
|
803
807
|
/**
|
|
804
|
-
* Recupera lo storico dei grafici pre-popolato dal server Signal K (Pro v6.0)
|
|
808
|
+
* Recupera lo storico dei grafici e dei radar pre-popolato dal server Signal K (Pro v6.0)
|
|
805
809
|
*/
|
|
806
810
|
async function fetchServerHistory() {
|
|
807
811
|
try {
|
|
@@ -815,6 +819,12 @@ async function fetchServerHistory() {
|
|
|
815
819
|
store.histories[key] = data[key];
|
|
816
820
|
}
|
|
817
821
|
}
|
|
822
|
+
// Sincronizza i dati specifici del radar storici, previsionali e i buffer minuto per minuto
|
|
823
|
+
if (data.windRadarSlots) store.windRadarSlots = data.windRadarSlots;
|
|
824
|
+
if (data.futureForecast) store.futureForecast = data.futureForecast;
|
|
825
|
+
if (data.twd) store.twdMinuteBuffer = data.twd;
|
|
826
|
+
if (data.tws) store.twsMinuteBuffer = data.tws;
|
|
827
|
+
|
|
818
828
|
// --- SILLABAZIONE STRATEGICA DELLA BUSSOLA METEO (TWD) ---
|
|
819
829
|
// Se il server ci invia lo storico del TWD, lo inseriamo calcolando i seni e coseni per i vettori
|
|
820
830
|
if (data.twd && data.twd.length > 0) {
|
|
@@ -825,7 +835,7 @@ async function fetchServerHistory() {
|
|
|
825
835
|
cos: Math.cos(p.val)
|
|
826
836
|
}));
|
|
827
837
|
console.log(`📈 Memoria strategica TWD sincronizzata dal server (${data.twd.length} punti).`);
|
|
828
|
-
|
|
838
|
+
}
|
|
829
839
|
console.log("📈 Storico dei grafici pre-popolato caricato dal server.");
|
|
830
840
|
}
|
|
831
841
|
} catch (err) {
|
|
@@ -1100,29 +1110,101 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
|
1100
1110
|
|
|
1101
1111
|
async function init() {
|
|
1102
1112
|
loadDashboardState();
|
|
1103
|
-
initCompassTicks(); // Genera i ticks sul quadrante usando il modulo gauge.js
|
|
1104
1113
|
|
|
1114
|
+
// Disegna la grafica statica delle tacche di calibrazione di entrambi gli strumenti
|
|
1115
|
+
initCompassTicks(); // Tacche del Wind Gauge analogico (gauge.js)
|
|
1116
|
+
initRadarTicks(); // Tacche del Wind Radar storico (weather-radar.js)
|
|
1117
|
+
|
|
1118
|
+
// 1. COMANDO TATTICO: Gestore Box TWD (Pressione prolungata -> Radar | Tocco rapido in modalità Radar -> Torna a Gauge)
|
|
1119
|
+
const twdBox = document.querySelector('.box-twd');
|
|
1120
|
+
if (twdBox) {
|
|
1121
|
+
let twdPressTimer = null;
|
|
1122
|
+
let longPressTriggered = false; // Flag di controllo della pressione prolungata
|
|
1123
|
+
|
|
1124
|
+
twdBox.addEventListener('pointerdown', (e) => {
|
|
1125
|
+
longPressTriggered = false;
|
|
1126
|
+
if (activeInstrument === 'gauge') {
|
|
1127
|
+
twdPressTimer = setTimeout(() => {
|
|
1128
|
+
activeInstrument = 'radar';
|
|
1129
|
+
document.getElementById('wind-gauge').style.display = 'none';
|
|
1130
|
+
document.getElementById('wind-radar').style.display = 'block';
|
|
1131
|
+
renderRadar(); // Disegna immediatamente il radar all'attivazione
|
|
1132
|
+
twdPressTimer = null;
|
|
1133
|
+
longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
|
|
1134
|
+
}, 1000);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
twdBox.addEventListener('pointerup', () => {
|
|
1138
|
+
if (activeInstrument === 'gauge') {
|
|
1139
|
+
if (twdPressTimer) {
|
|
1140
|
+
clearTimeout(twdPressTimer);
|
|
1141
|
+
twdPressTimer = null;
|
|
1142
|
+
}
|
|
1143
|
+
} else if (activeInstrument === 'radar') {
|
|
1144
|
+
if (longPressTriggered) {
|
|
1145
|
+
// Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
|
|
1146
|
+
longPressTriggered = false;
|
|
1147
|
+
} else {
|
|
1148
|
+
// Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
|
|
1149
|
+
activeInstrument = 'gauge';
|
|
1150
|
+
document.getElementById('wind-radar').style.display = 'none';
|
|
1151
|
+
document.getElementById('wind-gauge').style.display = 'block';
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
twdBox.addEventListener('pointerleave', () => {
|
|
1156
|
+
if (twdPressTimer) {
|
|
1157
|
+
clearTimeout(twdPressTimer);
|
|
1158
|
+
twdPressTimer = null;
|
|
1159
|
+
}
|
|
1160
|
+
longPressTriggered = false;
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// 2. COMANDO TATTICO: Click in qualsiasi punto del radar per tornare all'analogico
|
|
1165
|
+
const windRadarSvg = document.getElementById('wind-radar');
|
|
1166
|
+
if (windRadarSvg) {
|
|
1167
|
+
windRadarSvg.addEventListener('pointerup', () => {
|
|
1168
|
+
if (activeInstrument === 'radar') {
|
|
1169
|
+
activeInstrument = 'gauge';
|
|
1170
|
+
windRadarSvg.style.display = 'none';
|
|
1171
|
+
document.getElementById('wind-gauge').style.display = 'block';
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1105
1176
|
// Rileviamo se siamo sul Mac tramite file:// (Ambiente di sviluppo locale)
|
|
1106
1177
|
const isLocalFile = (window.location.protocol === 'file:');
|
|
1107
1178
|
|
|
1108
|
-
//
|
|
1179
|
+
// 3. CARICAMENTO STORICO GRAFICI E RADAR REALI DAL CERBO GX
|
|
1109
1180
|
try {
|
|
1110
1181
|
await fetchServerHistory();
|
|
1111
1182
|
} catch (err) {
|
|
1112
1183
|
console.warn("⚠️ Impossibile caricare lo storico reale dal server.");
|
|
1113
1184
|
}
|
|
1114
1185
|
|
|
1115
|
-
//
|
|
1186
|
+
// 4. CARICAMENTO CONFIGURAZIONI REALI (Bypassato su Mac per preservare i tuoi test!)
|
|
1116
1187
|
if (!isLocalFile) {
|
|
1117
1188
|
await fetchServerConfig();
|
|
1118
1189
|
} else {
|
|
1119
|
-
// Mantiene la CONFIG locale di app.js per farti fare le prove delle scale sul Mac
|
|
1120
1190
|
console.log("🎮 Esecuzione locale file://: utilizzo delle calibrazioni di CONFIG locali di debug.");
|
|
1121
1191
|
}
|
|
1122
1192
|
|
|
1123
1193
|
startDisplayLoop();
|
|
1124
1194
|
connect(); // Si collegherà in tempo reale al WebSocket reale della barca
|
|
1125
1195
|
|
|
1196
|
+
// 5. POLL LENTO (15 secondi): aggiorna i dati radar in background e, se attivo, li ridisegna
|
|
1197
|
+
setInterval(async () => {
|
|
1198
|
+
try {
|
|
1199
|
+
await fetchServerHistory();
|
|
1200
|
+
if (activeInstrument === 'radar') {
|
|
1201
|
+
renderRadar();
|
|
1202
|
+
}
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
console.warn("⚠️ Errore aggiornamento periodico storico:", err);
|
|
1205
|
+
}
|
|
1206
|
+
}, 15000);
|
|
1207
|
+
|
|
1126
1208
|
// Controlla le modifiche di configurazione sul Cerbo solo se non siamo sul Mac via file://
|
|
1127
1209
|
if (!isLocalFile) {
|
|
1128
1210
|
setInterval(watchConfigChanges, 10000);
|
package/charts.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* ==========================================================================
|
|
6
6
|
* Gestisce l'adattamento delle scale dei grafici, l'arrotondamento dei limiti
|
|
7
7
|
* (Snap a griglia) e la generazione dinamica delle curve SVG.
|
|
8
|
+
* file charts.js
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
// --- 1. MOTORE DI CALCOLO DELLE SCALE (Snap a griglia & Protezione Profondità) ---
|
package/gauge.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* ==========================================================================
|
|
5
5
|
* Gestisce l'aggiornamento grafico dei puntatori analogici (AWA, TWA),
|
|
6
6
|
* dello scarroccio (Leeway), della rotta (Track) e dei trend della bussola.
|
|
7
|
+
* file gauge,js
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
// 1. VARIABILI DI STATO DELLE ROTAZIONI (Estratte da app.js)
|
package/index.html
CHANGED
|
@@ -63,104 +63,144 @@
|
|
|
63
63
|
</div>
|
|
64
64
|
|
|
65
65
|
<!-- BUSSOLA CENTRALE -->
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
66
|
+
<div class="box-gauge">
|
|
67
|
+
<!-- STATO CONNESSIONE -->
|
|
68
|
+
<div id="status" class="offline">OFFLINE</div>
|
|
69
|
+
|
|
70
|
+
<!-- BUSSOLA 1: ANALOGICA STANDARD (Visibile all'avvio) -->
|
|
71
|
+
<svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
|
|
72
|
+
<defs>
|
|
73
|
+
<clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
74
|
+
<linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
75
|
+
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
|
76
|
+
<stop offset="100%" style="stop-color:#888888;stop-opacity:1" />
|
|
77
|
+
</linearGradient>
|
|
78
|
+
<linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
79
|
+
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
80
|
+
<stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" />
|
|
81
|
+
<stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" />
|
|
82
|
+
<stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" />
|
|
83
|
+
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
84
|
+
</linearGradient>
|
|
85
|
+
<linearGradient id="leeway-night-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
86
|
+
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
87
|
+
<stop offset="50%" style="stop-color:#330000;stop-opacity:1" />
|
|
88
|
+
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
89
|
+
</linearGradient>
|
|
90
|
+
<clipPath id="leeway-clip">
|
|
91
|
+
<rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" />
|
|
92
|
+
</clipPath>
|
|
93
|
+
<filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
94
|
+
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
95
|
+
</filter>
|
|
96
|
+
</defs>
|
|
97
|
+
|
|
98
|
+
<circle cx="200" cy="200" r="160" fill="#050505" />
|
|
99
|
+
<circle cx="200" cy="200" r="125" fill="#121212" />
|
|
100
|
+
|
|
101
|
+
<path d="M 82.0 101.0 A 154 154 0 0 1 142.3 57.2" fill="none" stroke="#ff0000" stroke-width="12" />
|
|
102
|
+
<path d="M 257.7 57.2 A 154 154 0 0 1 318.0 101.0" fill="none" stroke="#00ff00" stroke-width="12" />
|
|
103
|
+
<path d="M 265.1 339.6 A 154 154 0 0 1 134.9 339.6" fill="none" stroke="#ff8800" stroke-width="12" />
|
|
104
|
+
|
|
105
|
+
<g id="ticks"></g>
|
|
106
|
+
|
|
107
|
+
<g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
|
|
108
|
+
<text font-size="16" transform="translate(200, 74)">0</text>
|
|
109
|
+
<text font-size="16" transform="translate(326, 200) rotate(90)">90</text>
|
|
110
|
+
<text font-size="16" transform="translate(74, 200) rotate(-90)">90</text>
|
|
111
|
+
<text font-size="16" transform="translate(200, 326) rotate(180)">180</text>
|
|
112
|
+
<text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
|
|
113
|
+
<text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
|
|
114
|
+
<text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
|
|
115
|
+
<text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
|
|
116
|
+
<text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
|
|
117
|
+
<text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
|
|
118
|
+
<text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
|
|
119
|
+
<text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
|
|
120
|
+
</g>
|
|
121
|
+
|
|
122
|
+
<g id="track-pointer" transform="rotate(0, 200, 200)">
|
|
123
|
+
<path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" />
|
|
124
|
+
</g>
|
|
125
|
+
|
|
126
|
+
<circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
|
|
127
|
+
|
|
128
|
+
<path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
|
|
129
|
+
fill="url(#axiom-grad)" transform="translate(0, 5)" clip-path="url(#boat-clip)" style="pointer-events: none;" />
|
|
130
|
+
|
|
131
|
+
<g id="aws-display-group" transform="translate(200, 265)">
|
|
132
|
+
<text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
|
|
133
|
+
<text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
|
|
134
|
+
</g>
|
|
135
|
+
|
|
136
|
+
<g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
|
|
137
|
+
<path d="M 200,70 L 213,95 L 200,145 L 187,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" />
|
|
138
|
+
<text x="200" y="90" fill="#000" font-size="10" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
|
|
139
|
+
</g>
|
|
140
|
+
|
|
141
|
+
<g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
|
|
142
|
+
<path d="M 200,92 A 8,8 0 0 1 208,100 C 208,108 200,128 200,128 C 200,128 192,108 192,100 A 8,8 0 0 1 200,92 Z"
|
|
143
|
+
fill="#ffff00" stroke="#000" stroke-width="0.8" />
|
|
144
|
+
<text x="200" y="106" fill="#000" font-size="9" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
|
|
145
|
+
<circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#bbb" />
|
|
146
|
+
<circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#bbb" />
|
|
147
|
+
</g>
|
|
148
|
+
|
|
149
|
+
<g transform="translate(75, 395)">
|
|
150
|
+
<text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0°</text>
|
|
151
|
+
<rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
|
|
152
|
+
<rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
|
|
153
|
+
<g stroke="#555" stroke-width="1">
|
|
154
|
+
<line x1="0" y1="-2" x2="0" y2="14" /><line x1="62.5" y1="2" x2="62.5" y2="10" />
|
|
155
|
+
<line x1="125" y1="-2" x2="125" y2="14" /><line x1="187.5" y1="2" x2="187.5" y2="10" />
|
|
156
|
+
<line x1="250" y1="-2" x2="250" y2="14" />
|
|
157
|
+
</g>
|
|
158
|
+
<g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
|
|
159
|
+
<text x="0" y="24">-20°</text><text x="62.5" y="24">-10</text>
|
|
160
|
+
<text x="125" y="24">0°</text><text x="187.5" y="24">10</text>
|
|
161
|
+
<text x="250" y="24">20°</text>
|
|
162
|
+
</g>
|
|
163
|
+
</g>
|
|
164
|
+
</svg>
|
|
165
|
+
|
|
166
|
+
<!-- BUSSOLA 2: RADAR HISTORICAL (Nascosto all'avvio) -->
|
|
167
|
+
<svg id="wind-radar" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet" style="display: none;">
|
|
168
|
+
<defs id="radar-gradients">
|
|
169
|
+
<clipPath id="radar-boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
170
|
+
<filter id="radar-center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
171
|
+
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
172
|
+
</filter>
|
|
173
|
+
</defs>
|
|
174
|
+
|
|
175
|
+
<circle cx="200" cy="200" r="160" fill="rgb(252, 252, 252)" />
|
|
176
|
+
<circle cx="200" cy="200" r="125" fill="rgb(240, 240, 240)" />
|
|
177
|
+
|
|
178
|
+
<g id="radar-ticks"></g>
|
|
179
|
+
|
|
180
|
+
<g id="radar-tick-labels" fill="#000000" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
|
|
181
|
+
<text font-size="16" transform="translate(200, 74)">N</text>
|
|
182
|
+
<text font-size="16" transform="translate(326, 200) rotate(90)">E</text>
|
|
183
|
+
<text font-size="16" transform="translate(74, 200) rotate(-90)">W</text>
|
|
184
|
+
<text font-size="16" transform="translate(200, 326) rotate(180)">S</text>
|
|
185
|
+
|
|
186
|
+
<text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
|
|
187
|
+
<text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
|
|
188
|
+
<text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
|
|
189
|
+
<text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
|
|
190
|
+
<text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">330</text>
|
|
191
|
+
<text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">300</text>
|
|
192
|
+
<text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">240</text>
|
|
193
|
+
<text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">210</text>
|
|
194
|
+
</g>
|
|
195
|
+
|
|
196
|
+
<circle id="ora-orbit" cx="200" cy="200" r="67.2" fill="none" stroke="#cfd8dc" stroke-width="1.2" stroke-dasharray="4, 4" />
|
|
197
|
+
<g id="radar-rings"></g>
|
|
198
|
+
|
|
199
|
+
<circle id="radar-hotspot" cx="200" cy="200" r="55" fill="rgb(238, 238, 238)" stroke="#e0e0e0" stroke-width="1" filter="url(#radar-center-glow)" cursor="pointer" />
|
|
200
|
+
<path id="radar-boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
|
|
201
|
+
fill="#000000" transform="translate(0, 5)" clip-path="url(#radar-boat-clip)" style="pointer-events: none;" />
|
|
202
|
+
</svg>
|
|
203
|
+
</div>
|
|
164
204
|
|
|
165
205
|
<!-- COLONNA DESTRA -->
|
|
166
206
|
<!-- NOTA: Etichetta a SX e Unità a DX nel codice, il CSS usa row-reverse per specchiarli sul bordo -->
|
|
@@ -218,6 +258,7 @@
|
|
|
218
258
|
<script src="utils.js"></script>
|
|
219
259
|
<script src="charts.js"></script>
|
|
220
260
|
<script src="gauge.js"></script>
|
|
261
|
+
<script src="weather-radar.js"></script>
|
|
221
262
|
<script src="app.js?v=6.0"></script>
|
|
222
263
|
</body>
|
|
223
264
|
</html>
|
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* ==========================================================================
|
|
5
5
|
* Definisce l'interfaccia di configurazione in Signal K Admin e crea
|
|
6
6
|
* gli endpoint pubblici per la Dashboard, mantenendo lo storico in RAM.
|
|
7
|
+
* file index.js
|
|
7
8
|
*/
|
|
8
9
|
const https = require('https'); // Importazione del modulo HTTPS nativo di Node.js
|
|
9
10
|
|
|
@@ -23,11 +24,12 @@ module.exports = function (app) {
|
|
|
23
24
|
const CALM_THRESHOLD_KTS = 1.5; // Soglia di calma piatta (anello a 360°)
|
|
24
25
|
const PRESSURE_FILTER_RATIO = 0.40; // Filtro di pressione dinamico (40% del picco per ignorare i cali)
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
// Database dello storico in RAM sul server (Sintonizzato Pro v6.0)
|
|
27
28
|
let histories = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [], twd: [] };
|
|
28
29
|
let graphTempBuf = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [], twd: [] };
|
|
29
30
|
let lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0, twd: 0 };
|
|
30
31
|
let raw = {};
|
|
32
|
+
let lastPathProcessTimes = {}; // Registro dei timestamp per limitazione di frequenza a 1Hz
|
|
31
33
|
|
|
32
34
|
// Nuovo database dedicato per gli archi storici della bussola (6 ore = 12 slot)
|
|
33
35
|
let windRadarSlots = [];
|
|
@@ -135,93 +137,125 @@ module.exports = function (app) {
|
|
|
135
137
|
app.debug(msg);
|
|
136
138
|
};
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
/**
|
|
141
|
+
* processIncomingDelta: Decodifica i dati dei sensori in Knots/Meters ed esegue l'aggregazione
|
|
142
|
+
* Applica un filtro passa-basso continuo a ogni pacchetto e storicizza a 1Hz con dati stabilizzati.
|
|
143
|
+
*/
|
|
144
|
+
function processIncomingDelta(path, val) {
|
|
145
|
+
if (val === null || val === undefined) return;
|
|
146
|
+
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
const alpha = 0.20; // Coefficiente di smoothing (Filtro passa-basso: reattività ~2 secondi)
|
|
149
|
+
|
|
150
|
+
// FILTRO PASSA-BASSO CONTINUO IN TEMPO REALE (Previene gli Spike prima della storicizzazione)
|
|
151
|
+
if (path === 'navigation.position') {
|
|
152
|
+
raw[path] = val; // Le coordinate GPS non sono soggette a filtri di smorzamento o ritardo
|
|
153
|
+
|
|
154
|
+
// Chiamata periodica Open-Meteo
|
|
155
|
+
if (val.latitude !== undefined && val.longitude !== undefined) {
|
|
156
|
+
const current30mSlot = Math.floor(now / 1800000) * 1800000;
|
|
157
|
+
if (current30mSlot > lastForecast30mSlot) {
|
|
158
|
+
fetchOpenMeteoForecast(val, current30mSlot);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return; // Esce subito
|
|
162
|
+
}
|
|
145
163
|
|
|
146
|
-
|
|
164
|
+
if (path.includes('angle') || path.includes('heading') || path.includes('course') || path.includes('direction') || path.includes('twd')) {
|
|
165
|
+
// 1. Caso Angolare (Radianti): Calcolo differenziale circolare per gestire l'oltrepasso dello 0/360 gradi
|
|
166
|
+
if (raw[path] !== undefined) {
|
|
167
|
+
let diff = Math.atan2(Math.sin(val - raw[path]), Math.cos(val - raw[path]));
|
|
168
|
+
raw[path] = (raw[path] + diff * alpha + Math.PI * 2) % (Math.PI * 2);
|
|
169
|
+
} else {
|
|
170
|
+
raw[path] = val;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// 2. Caso Lineare (Velocità e Profondità): Smorzamento continuo
|
|
174
|
+
if (raw[path] !== undefined) {
|
|
175
|
+
raw[path] = (val * alpha) + (raw[path] * (1 - alpha));
|
|
176
|
+
} else {
|
|
177
|
+
raw[path] = val;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
147
180
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (current30mSlot > lastForecast30mSlot) {
|
|
155
|
-
fetchOpenMeteoForecast(val, current30mSlot);
|
|
181
|
+
// LIMITATORE DI FREQUENZA (RATE LIMITER) A 1HZ PER PERCORSO ATTIVO:
|
|
182
|
+
// La scrittura nello storico e i calcoli derivati vengono eseguiti al massimo una volta al secondo,
|
|
183
|
+
// leggendo il valore "raw[path]" stabilizzato continuamente dal filtro passa-basso superiore.
|
|
184
|
+
if (!lastPathProcessTimes[path]) lastPathProcessTimes[path] = 0;
|
|
185
|
+
if (now - lastPathProcessTimes[path] < 1000) {
|
|
186
|
+
return; // Esce subito risparmiando la CPU se il sensore ha già aggiornato nell'ultimo secondo
|
|
156
187
|
}
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
else if (path === 'navigation.speedThroughWater') {
|
|
160
|
-
manageHistory('stw', val * 1.94384);
|
|
161
|
-
}
|
|
162
|
-
else if (path === 'navigation.speedOverGround') {
|
|
163
|
-
manageHistory('sog', val * 1.94384);
|
|
164
|
-
}
|
|
165
|
-
else if (path === 'environment.depth.belowTransducer') {
|
|
166
|
-
manageHistory('depth', val);
|
|
167
|
-
}
|
|
168
|
-
else if (path === 'environment.wind.speedApparent') {
|
|
169
|
-
manageHistory('aws', val * 1.94384);
|
|
170
|
-
}
|
|
171
|
-
else if (path === 'environment.wind.angleApparent') {
|
|
172
|
-
raw[path] = val; // BUG RISOLTO: Acquisizione dell'AWA mancante inserita!
|
|
173
|
-
}
|
|
174
|
-
else if (path === 'environment.wind.speedTrue') {
|
|
175
|
-
lastNativeTwsTime = now; // Rilevato TWS nativo della centralina!
|
|
176
|
-
manageHistory('tws', val * 1.94384);
|
|
177
|
-
}
|
|
178
|
-
// --- DECODIFICA PRUA MAGNETICA SERVER-SIDE ---
|
|
179
|
-
else if (path === 'navigation.headingMagnetic') {
|
|
180
|
-
const hasTrueHdg = raw['navigation.headingTrue'] !== undefined;
|
|
181
|
-
if (!hasTrueHdg) {
|
|
182
|
-
const variation = raw['navigation.magneticVariation'] || 0;
|
|
183
|
-
raw['navigation.headingTrue'] = (val + variation + 2 * Math.PI) % (2 * Math.PI);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
else if (path === 'environment.wind.directionTrue') {
|
|
187
|
-
lastNativeTwdTime = now; // Rilevato TWD nativo della centralina!
|
|
188
|
-
manageHistory('twd', val);
|
|
189
|
-
}
|
|
188
|
+
lastPathProcessTimes[path] = now;
|
|
190
189
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const awa = raw["environment.wind.angleApparent"];
|
|
194
|
-
const stw = raw["navigation.speedThroughWater"] || 0;
|
|
195
|
-
const sog = raw["navigation.speedOverGround"] || 0;
|
|
196
|
-
const hdg = raw["navigation.headingTrue"];
|
|
197
|
-
const cog = raw["navigation.courseOverGroundTrue"] || 0;
|
|
198
|
-
|
|
199
|
-
if (aws !== undefined && awa !== undefined) {
|
|
200
|
-
const awsKts = aws * 1.94384;
|
|
201
|
-
const stwKts = stw * 1.94384;
|
|
202
|
-
const tw_water_x = awsKts * Math.cos(awa) - stwKts;
|
|
203
|
-
const tw_water_y = awsKts * Math.sin(awa);
|
|
204
|
-
|
|
205
|
-
// Calcoliamo il TWS di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
|
|
206
|
-
if (now - lastNativeTwsTime > 5000) {
|
|
207
|
-
const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
208
|
-
manageHistory('tws', tws);
|
|
209
|
-
}
|
|
190
|
+
// Da qui in poi, l'esecuzione della storia e del vento reale avviene rigorosamente a 1Hz con dati puliti:
|
|
191
|
+
const smoothedVal = raw[path];
|
|
210
192
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
193
|
+
if (path === 'navigation.speedThroughWater') {
|
|
194
|
+
manageHistory('stw', smoothedVal * 1.94384);
|
|
195
|
+
}
|
|
196
|
+
else if (path === 'navigation.speedOverGround') {
|
|
197
|
+
manageHistory('sog', smoothedVal * 1.94384);
|
|
198
|
+
}
|
|
199
|
+
else if (path === 'environment.depth.belowTransducer') {
|
|
200
|
+
manageHistory('depth', smoothedVal);
|
|
201
|
+
}
|
|
202
|
+
else if (path === 'environment.wind.speedApparent') {
|
|
203
|
+
manageHistory('aws', smoothedVal * 1.94384);
|
|
204
|
+
}
|
|
205
|
+
else if (path === 'environment.wind.angleApparent') {
|
|
206
|
+
// Già gestito e normalizzato dal filtro passa-basso superiore
|
|
207
|
+
}
|
|
208
|
+
else if (path === 'environment.wind.speedTrue') {
|
|
209
|
+
lastNativeTwsTime = now; // Rilevato TWS nativo della centralina!
|
|
210
|
+
manageHistory('tws', smoothedVal * 1.94384);
|
|
211
|
+
}
|
|
212
|
+
// --- DECODIFICA PRUA MAGNETICA SERVER-SIDE ---
|
|
213
|
+
else if (path === 'navigation.headingMagnetic') {
|
|
214
|
+
const hasTrueHdg = raw['navigation.headingTrue'] !== undefined;
|
|
215
|
+
if (!hasTrueHdg) {
|
|
216
|
+
const variation = raw['navigation.magneticVariation'] || 0;
|
|
217
|
+
raw['navigation.headingTrue'] = (smoothedVal + variation + 2 * Math.PI) % (2 * Math.PI);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (path === 'environment.wind.directionTrue') {
|
|
221
|
+
lastNativeTwdTime = now; // Rilevato TWD nativo della centralina!
|
|
222
|
+
manageHistory('twd', smoothedVal);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 2. Calcolo combinato di FALLBACK (Si attiva solo se la centralina non invia TWS/TWD nativi)
|
|
226
|
+
const aws = raw["environment.wind.speedApparent"];
|
|
227
|
+
const awa = raw["environment.wind.angleApparent"];
|
|
228
|
+
const stw = raw["navigation.speedThroughWater"] || 0;
|
|
229
|
+
const sog = raw["navigation.speedOverGround"] || 0;
|
|
230
|
+
const hdg = raw["navigation.headingTrue"];
|
|
231
|
+
const cog = raw["navigation.courseOverGroundTrue"] || 0;
|
|
232
|
+
|
|
233
|
+
if (aws !== undefined && awa !== undefined) {
|
|
234
|
+
const awsKts = aws * 1.94384;
|
|
235
|
+
const stwKts = stw * 1.94384;
|
|
236
|
+
const tw_water_x = awsKts * Math.cos(awa) - stwKts;
|
|
237
|
+
const tw_water_y = awsKts * Math.sin(awa);
|
|
238
|
+
|
|
239
|
+
// Calcoliamo il TWS di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
|
|
240
|
+
if (now - lastNativeTwsTime > 5000) {
|
|
241
|
+
const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
242
|
+
manageHistory('tws', tws);
|
|
243
|
+
}
|
|
224
244
|
|
|
245
|
+
const twa = Math.atan2(tw_water_y, tw_water_x);
|
|
246
|
+
|
|
247
|
+
// La VMG viene sempre calcolata a livello server poiché raramente è nativa
|
|
248
|
+
const vmg = Math.abs(stwKts * Math.cos(twa));
|
|
249
|
+
manageHistory('vmg', vmg);
|
|
250
|
+
|
|
251
|
+
// Calcoliamo il TWD di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
|
|
252
|
+
if (hdg !== undefined && (now - lastNativeTwdTime > 5000)) {
|
|
253
|
+
const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
|
|
254
|
+
manageHistory('twd', twd);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
225
259
|
/**
|
|
226
260
|
* manageHistory: Versione Server-side dell'aggregatore matematico tattico
|
|
227
261
|
*/
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -520,3 +520,40 @@ body.night-mode {
|
|
|
520
520
|
.night-mode .graph-wrapper polygon[style*="#0088cc"] {
|
|
521
521
|
fill: #33aadd !important;
|
|
522
522
|
}
|
|
523
|
+
|
|
524
|
+
/* ==========================================================================
|
|
525
|
+
12. INTEGRAZIONE DUAL-SVG COMPASS & RADAR
|
|
526
|
+
========================================================================== */
|
|
527
|
+
#wind-gauge, #wind-radar {
|
|
528
|
+
width: 100%;
|
|
529
|
+
height: 100%;
|
|
530
|
+
max-height: 100%;
|
|
531
|
+
object-fit: contain;
|
|
532
|
+
transition: opacity 0.2s ease-in-out;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* --- OVERRIDE NIGHT MODE PER IL WIND RADAR --- */
|
|
536
|
+
.night-mode #wind-radar circle {
|
|
537
|
+
fill: #000000 !important;
|
|
538
|
+
stroke: #220000 !important;
|
|
539
|
+
}
|
|
540
|
+
.night-mode #wind-radar circle:nth-of-type(2),
|
|
541
|
+
.night-mode #radar-hotspot {
|
|
542
|
+
fill: #050000 !important;
|
|
543
|
+
stroke: #330000 !important;
|
|
544
|
+
}
|
|
545
|
+
.night-mode #radar-tick-labels {
|
|
546
|
+
fill: #800000 !important;
|
|
547
|
+
}
|
|
548
|
+
.night-mode #radar-boat-icon {
|
|
549
|
+
fill: #220000 !important;
|
|
550
|
+
stroke: #ff0000 !important;
|
|
551
|
+
stroke-width: 0.8px !important;
|
|
552
|
+
opacity: 1 !important;
|
|
553
|
+
}
|
|
554
|
+
.night-mode #ora-orbit {
|
|
555
|
+
stroke: #330000 !important;
|
|
556
|
+
}
|
|
557
|
+
.night-mode #wind-radar circle[stroke="#b0bec5"] {
|
|
558
|
+
stroke: #440000 !important;
|
|
559
|
+
}
|
package/utils.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Sailing Dashboard Pro - Math, Conversions & Audio Utilities
|
|
4
4
|
* ==========================================================================
|
|
5
5
|
* Raccoglie le funzioni pure di calcolo vettoriale e sintesi sonora.
|
|
6
|
+
*file utils.js
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
// --- 1. CONVERSIONI STANDARD ---
|
package/weather-radar.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ==========================================================================
|
|
3
|
+
* Signal K Wind Radar - Vector Core and Compass Rendering Engine (Modular v6.0)
|
|
4
|
+
* ==========================================================================
|
|
5
|
+
* Autore: Sailing Rotevista
|
|
6
|
+
* Libreria grafica pura per il disegno della bussola radar storica (TWD).
|
|
7
|
+
* Condivide le risorse, lo stato e i flussi di dati generati da app.js.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// --- 1. PARAMETRI DI CALCOLO INTERNI ---
|
|
11
|
+
const CALM_THRESHOLD_KTS = 1.5;
|
|
12
|
+
const PRESSURE_FILTER_RATIO = 0.40;
|
|
13
|
+
const ringRadii = [59.0, 67.2, 75.4, 83.6, 91.8, 100.0, 108.2, 116.4];
|
|
14
|
+
const ARC_STROKE_WIDTH = 5.0;
|
|
15
|
+
const BORDER_STROKE_WIDTH = ARC_STROKE_WIDTH + 2; // 7px
|
|
16
|
+
|
|
17
|
+
// --- 2. FUNZIONI GEOMETRICHE DI SUPPORTO ---
|
|
18
|
+
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
|
|
19
|
+
const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
|
|
20
|
+
return {
|
|
21
|
+
x: centerX + (radius * Math.cos(angleInRadians)),
|
|
22
|
+
y: centerY + (radius * Math.sin(angleInRadians))
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function describeArc(centerX, centerY, radius, startAngle, endAngle) {
|
|
27
|
+
const start = polarToCartesian(centerX, centerY, radius, endAngle);
|
|
28
|
+
const end = polarToCartesian(centerX, centerY, radius, startAngle);
|
|
29
|
+
|
|
30
|
+
let arcSweep = endAngle - startAngle;
|
|
31
|
+
if (arcSweep < 0) arcSweep += 360;
|
|
32
|
+
|
|
33
|
+
const largeArcFlag = arcSweep <= 180 ? "0" : "1";
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
"M", start.x, start.y,
|
|
37
|
+
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
|
|
38
|
+
].join(" ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Genera dinamicamente le tacche dei gradi bussola per l'SVG del radar
|
|
42
|
+
function initRadarTicks() {
|
|
43
|
+
const c = document.getElementById('radar-ticks');
|
|
44
|
+
if (c) {
|
|
45
|
+
c.innerHTML = "";
|
|
46
|
+
for (let i = 0; i < 360; i += 10) {
|
|
47
|
+
const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
48
|
+
const m = i % 30 === 0;
|
|
49
|
+
l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50));
|
|
50
|
+
l.setAttribute("stroke", m ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1");
|
|
51
|
+
l.setAttribute("transform", `rotate(${i}, 200, 200)`);
|
|
52
|
+
c.appendChild(l);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- 3. MOTORE DI CALCOLO DELL'ANELLO 1 (Presente Mobile) ---
|
|
58
|
+
function calculateActive30mRing() {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const start30m = now - 1800000;
|
|
61
|
+
|
|
62
|
+
const twdRecent = (store.twdMinuteBuffer || []).filter(p => p.time >= start30m);
|
|
63
|
+
const twsRecent = (store.twsMinuteBuffer || []).filter(p => p.time >= start30m);
|
|
64
|
+
|
|
65
|
+
if (twdRecent.length === 0) return null;
|
|
66
|
+
|
|
67
|
+
const twsVals = twsRecent.map(p => p.val).filter(v => isFinite(v));
|
|
68
|
+
const maxTws = twsVals.length > 0 ? Math.max(...twsVals) : 0;
|
|
69
|
+
const minTws = twsVals.length > 0 ? Math.min(...twsVals) : 0;
|
|
70
|
+
|
|
71
|
+
if (maxTws < CALM_THRESHOLD_KTS) {
|
|
72
|
+
return { twsPeak: maxTws, twsMin: minTws, twdMin: 0, twdMax: 360, isCalm: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let allAngles = [];
|
|
76
|
+
twdRecent.forEach(p => {
|
|
77
|
+
allAngles.push(p.val);
|
|
78
|
+
allAngles.push(p.min);
|
|
79
|
+
allAngles.push(p.max);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let sumSin = 0; let sumCos = 0;
|
|
83
|
+
allAngles.forEach(a => { sumSin += Math.sin(a); sumCos += Math.cos(a); });
|
|
84
|
+
const avgAngle = Math.atan2(sumSin, sumCos);
|
|
85
|
+
const finalAvg = (avgAngle + Math.PI * 2) % (Math.PI * 2);
|
|
86
|
+
|
|
87
|
+
let diffs = allAngles.map(a => {
|
|
88
|
+
let diff = a - finalAvg;
|
|
89
|
+
return Math.atan2(Math.sin(diff), Math.cos(diff));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
diffs.sort((a, b) => a - b);
|
|
93
|
+
const trimCount = Math.floor(diffs.length * 0.05);
|
|
94
|
+
const activeDiffs = diffs.slice(trimCount, diffs.length - trimCount);
|
|
95
|
+
const finalDiffs = activeDiffs.length > 0 ? activeDiffs : diffs;
|
|
96
|
+
|
|
97
|
+
const minDiff = Math.min(...finalDiffs);
|
|
98
|
+
const maxDiff = Math.max(...finalDiffs);
|
|
99
|
+
|
|
100
|
+
const finalMinDeg = Math.round(radToDeg((finalAvg + minDiff + Math.PI * 2) % (Math.PI * 2)));
|
|
101
|
+
const finalMaxDeg = Math.round(radToDeg((finalAvg + maxDiff + Math.PI * 2) % (Math.PI * 2)));
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
twdMin: finalMinDeg,
|
|
105
|
+
twdMax: finalMaxDeg,
|
|
106
|
+
twsPeak: maxTws,
|
|
107
|
+
twsMin: minTws,
|
|
108
|
+
isCalm: false
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- 4. MOTORE GRAFICO DI DISEGNO DEL RADAR ---
|
|
113
|
+
function renderRadar() {
|
|
114
|
+
const ringsContainer = document.getElementById('radar-rings');
|
|
115
|
+
const defsContainer = document.getElementById('radar-gradients');
|
|
116
|
+
|
|
117
|
+
if (!ringsContainer || !defsContainer) return; // Protezione se l'SVG del radar non è presente nel DOM
|
|
118
|
+
|
|
119
|
+
defsContainer.innerHTML = `
|
|
120
|
+
<clipPath id="radar-boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
121
|
+
<filter id="radar-center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
122
|
+
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
123
|
+
</filter>
|
|
124
|
+
`;
|
|
125
|
+
ringsContainer.innerHTML = '';
|
|
126
|
+
|
|
127
|
+
const oraOrbit = document.getElementById('ora-orbit');
|
|
128
|
+
if (oraOrbit) oraOrbit.setAttribute("r", ringRadii[1]);
|
|
129
|
+
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const current30mSlot = Math.floor(now / 1800000) * 1800000;
|
|
132
|
+
|
|
133
|
+
const radarDataList = [];
|
|
134
|
+
|
|
135
|
+
// 1. ANELLO 0: Previsione Futura Open-Meteo
|
|
136
|
+
if (store.futureForecast) {
|
|
137
|
+
const twdDeg = Math.round(radToDeg(store.futureForecast.twd));
|
|
138
|
+
radarDataList.push({
|
|
139
|
+
twdMin: (twdDeg - 20 + 360) % 360,
|
|
140
|
+
twdMax: (twdDeg + 20 + 360) % 360,
|
|
141
|
+
twsPeak: store.futureForecast.tws,
|
|
142
|
+
isFuture: true
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
radarDataList.push(null);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2. ANELLO 1: Presente Mobile (Real-Time Client-Side)
|
|
149
|
+
const activeRing = calculateActive30mRing();
|
|
150
|
+
radarDataList.push(activeRing);
|
|
151
|
+
|
|
152
|
+
// 3. ANELLI 2-7: Storico consolidato dal server
|
|
153
|
+
const slots = store.windRadarSlots || [];
|
|
154
|
+
for (let i = 1; i <= 6; i++) {
|
|
155
|
+
const targetTimestamp = current30mSlot - (i * 1800000);
|
|
156
|
+
const matchedSlot = slots.find(s => s.timestamp === targetTimestamp);
|
|
157
|
+
|
|
158
|
+
if (matchedSlot) {
|
|
159
|
+
radarDataList.push({
|
|
160
|
+
twdMin: Math.round(radToDeg(matchedSlot.twdMin)),
|
|
161
|
+
twdMax: Math.round(radToDeg(matchedSlot.twdMax)),
|
|
162
|
+
twsPeak: matchedSlot.twsPeak,
|
|
163
|
+
twsMin: matchedSlot.twsMin !== undefined ? matchedSlot.twsMin : matchedSlot.twsPeak,
|
|
164
|
+
isCalm: matchedSlot.twsPeak < CALM_THRESHOLD_KTS
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
radarDataList.push(null);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Disegno degli archi
|
|
172
|
+
radarDataList.forEach((data, index) => {
|
|
173
|
+
if (!data) return;
|
|
174
|
+
|
|
175
|
+
const radius = ringRadii[index];
|
|
176
|
+
const gradId = `chord-gradient-${index}`;
|
|
177
|
+
const opacityValue = 1;
|
|
178
|
+
|
|
179
|
+
if (data.isCalm || data.twsPeak < CALM_THRESHOLD_KTS) {
|
|
180
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
181
|
+
circle.setAttribute("cx", "200");
|
|
182
|
+
circle.setAttribute("cy", "200");
|
|
183
|
+
circle.setAttribute("r", radius);
|
|
184
|
+
circle.setAttribute("fill", "none");
|
|
185
|
+
circle.setAttribute("stroke", document.body.classList.contains('night-mode') ? "#440000" : "#b0bec5");
|
|
186
|
+
circle.setAttribute("stroke-width", "1.2");
|
|
187
|
+
circle.setAttribute("stroke-dasharray", "4, 4");
|
|
188
|
+
ringsContainer.appendChild(circle);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let strokeColor = '';
|
|
193
|
+
|
|
194
|
+
const getColorForSpeed = (tws) => {
|
|
195
|
+
const R1 = CONFIG.graphs.reef1 || 15;
|
|
196
|
+
const R2 = CONFIG.graphs.reef2 || 20;
|
|
197
|
+
const R3 = R2 + (R2 - R1);
|
|
198
|
+
if (tws < R1 * 0.4) return '#ffffff';
|
|
199
|
+
if (tws < R1 * 0.75) return '#00C851';
|
|
200
|
+
if (tws < R1) return '#ff9800';
|
|
201
|
+
if (tws < R2) return '#ffaa00';
|
|
202
|
+
if (tws < R2 + (R3 - R2) * 0.5) return '#ff3b30';
|
|
203
|
+
return '#9c27b0';
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const baseTws = data.isFuture && store.futureForecast ? store.futureForecast.tws : (data.twsMin !== undefined ? data.twsMin : data.twsPeak);
|
|
207
|
+
const peakTws = data.isFuture && store.futureForecast ? store.futureForecast.gust : data.twsPeak;
|
|
208
|
+
|
|
209
|
+
const baseColor = getColorForSpeed(baseTws);
|
|
210
|
+
const peakColor = getColorForSpeed(peakTws);
|
|
211
|
+
|
|
212
|
+
if (baseColor !== peakColor) {
|
|
213
|
+
const startPt = polarToCartesian(200, 200, radius, data.twdMax);
|
|
214
|
+
const endPt = polarToCartesian(200, 200, radius, data.twdMin);
|
|
215
|
+
|
|
216
|
+
const xml = `
|
|
217
|
+
<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">
|
|
218
|
+
<stop offset="0%" stop-color="${baseColor}" />
|
|
219
|
+
<stop offset="50%" stop-color="${peakColor}" />
|
|
220
|
+
<stop offset="100%" stop-color="${baseColor}" />
|
|
221
|
+
</linearGradient>
|
|
222
|
+
`;
|
|
223
|
+
defsContainer.innerHTML += xml;
|
|
224
|
+
strokeColor = `url(#${gradId})`;
|
|
225
|
+
} else {
|
|
226
|
+
strokeColor = baseColor;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
|
|
230
|
+
|
|
231
|
+
if (!data.isFuture) {
|
|
232
|
+
const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
233
|
+
borderPath.setAttribute("d", pathData);
|
|
234
|
+
borderPath.setAttribute("fill", "none");
|
|
235
|
+
borderPath.setAttribute("stroke", "#000000");
|
|
236
|
+
borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
|
|
237
|
+
borderPath.setAttribute("stroke-linecap", "round");
|
|
238
|
+
borderPath.setAttribute("opacity", opacityValue);
|
|
239
|
+
ringsContainer.appendChild(borderPath);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
243
|
+
mainPath.setAttribute("d", pathData);
|
|
244
|
+
mainPath.setAttribute("fill", "none");
|
|
245
|
+
mainPath.setAttribute("stroke", strokeColor);
|
|
246
|
+
mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
|
|
247
|
+
mainPath.setAttribute("stroke-linecap", "round");
|
|
248
|
+
mainPath.setAttribute("opacity", data.isFuture ? "0.5" : opacityValue);
|
|
249
|
+
mainPath.id = data.isFuture ? "" : (index === 1 ? "active-present-arc" : "");
|
|
250
|
+
ringsContainer.appendChild(mainPath);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Disegno del LED lampeggiante del meteo-trend
|
|
254
|
+
if (activeRing && !activeRing.isCalm) {
|
|
255
|
+
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
256
|
+
const strategicWindowMs = (isNavigating ? 15 : 60) * 60000;
|
|
257
|
+
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
258
|
+
|
|
259
|
+
if (twdNow && twdRef) {
|
|
260
|
+
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
261
|
+
|
|
262
|
+
if (Math.abs(deltaMeteo) > 6.0) {
|
|
263
|
+
const isSouth = store.raw["navigation.position"] && store.raw["navigation.position"].latitude < 0;
|
|
264
|
+
let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#00C851" : "#ff3b30") : (deltaMeteo > 0 ? "#00C851" : "#ff3b30");
|
|
265
|
+
|
|
266
|
+
const radiusAnello1 = ringRadii[1];
|
|
267
|
+
const angleTarget = deltaMeteo > 0 ? activeRing.twdMax : activeRing.twdMin;
|
|
268
|
+
const pt = polarToCartesian(200, 200, radiusAnello1, angleTarget);
|
|
269
|
+
|
|
270
|
+
const led = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
271
|
+
led.setAttribute("cx", pt.x.toFixed(1));
|
|
272
|
+
led.setAttribute("cy", pt.y.toFixed(1));
|
|
273
|
+
led.setAttribute("r", "5.5");
|
|
274
|
+
led.setAttribute("fill", meteoColor);
|
|
275
|
+
led.setAttribute("class", "is-trending");
|
|
276
|
+
led.setAttribute("filter", "url(#radar-center-glow)");
|
|
277
|
+
ringsContainer.appendChild(led);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|