@sailingrotevista/rotevista-dash 1.0.0 → 1.0.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/app.js +21 -0
- package/package.json +1 -1
- package/style.css +89 -189
- package/app.js.txt +0 -311
package/app.js
CHANGED
|
@@ -460,3 +460,24 @@ startDisplayLoop();
|
|
|
460
460
|
window.addEventListener('load', () => {
|
|
461
461
|
setTimeout(connect, 500);
|
|
462
462
|
});
|
|
463
|
+
|
|
464
|
+
// ==========================================================================
|
|
465
|
+
// 11. GESTIONE FULLSCREEN (Al primo tocco)
|
|
466
|
+
// ==========================================================================
|
|
467
|
+
function requestFullScreen() {
|
|
468
|
+
const docElm = document.documentElement; // Prende l'intero tag <html>
|
|
469
|
+
|
|
470
|
+
// Controlla se siamo già in fullscreen
|
|
471
|
+
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
|
|
472
|
+
// Tenta di attivare il fullscreen (con fallback per vari browser)
|
|
473
|
+
if (docElm.requestFullscreen) {
|
|
474
|
+
docElm.requestFullscreen().catch(err => console.log("Fullscreen bloccato:", err));
|
|
475
|
+
} else if (docElm.webkitRequestFullscreen) { // Chrome/Safari (se supportato)
|
|
476
|
+
docElm.webkitRequestFullscreen();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Ascolta il primissimo click o tocco ovunque nello schermo per attivare il fullscreen
|
|
482
|
+
document.addEventListener('click', requestFullScreen, { once: true });
|
|
483
|
+
document.addEventListener('touchstart', requestFullScreen, { once: true });
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -15,33 +15,50 @@ body {
|
|
|
15
15
|
|
|
16
16
|
.main-container {
|
|
17
17
|
display: flex;
|
|
18
|
+
flex-direction: row;
|
|
19
|
+
flex-wrap: wrap; /* CRUCIALE: Permette di andare a capo su mobile */
|
|
18
20
|
width: 100%;
|
|
19
21
|
height: 100%;
|
|
20
22
|
padding: 5px;
|
|
21
23
|
box-sizing: border-box;
|
|
22
|
-
align-
|
|
24
|
+
align-content: stretch;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/* ==========================================================================
|
|
26
|
-
2. PANNELLI
|
|
28
|
+
2. PANNELLI (LAYOUT DESKTOP)
|
|
27
29
|
========================================================================== */
|
|
28
30
|
.side-panel {
|
|
29
|
-
flex: 1
|
|
31
|
+
flex: 1 1 0%;
|
|
32
|
+
height: 100%;
|
|
30
33
|
display: flex;
|
|
31
34
|
flex-direction: column;
|
|
32
35
|
justify-content: flex-start;
|
|
33
36
|
background: rgba(255, 255, 255, 0.02);
|
|
34
37
|
border-radius: 10px;
|
|
35
38
|
z-index: 10;
|
|
39
|
+
order: 1; /* Sinistra */
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.center-panel {
|
|
43
|
+
flex: 3.5 1 0%;
|
|
44
|
+
height: 100%;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
justify-content: flex-start;
|
|
48
|
+
align-items: center;
|
|
49
|
+
padding: 0;
|
|
50
|
+
margin: 0;
|
|
51
|
+
order: 2; /* Centro */
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
.right-panel {
|
|
39
55
|
align-items: flex-end;
|
|
40
56
|
text-align: right;
|
|
57
|
+
order: 3; /* Destra */
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
.data-box {
|
|
44
|
-
position: relative;
|
|
61
|
+
position: relative;
|
|
45
62
|
border-bottom: 1px solid #222;
|
|
46
63
|
padding: 8px 8px;
|
|
47
64
|
display: flex;
|
|
@@ -49,221 +66,104 @@ body {
|
|
|
49
66
|
min-height: 82px;
|
|
50
67
|
width: 100%;
|
|
51
68
|
box-sizing: border-box;
|
|
52
|
-
flex: 1;
|
|
69
|
+
flex: 1;
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
/* ==========================================================================
|
|
56
|
-
3.
|
|
73
|
+
3. STRUMENTO VENTO SVG
|
|
57
74
|
========================================================================== */
|
|
58
|
-
|
|
59
|
-
display: flex;
|
|
60
|
-
justify-content: space-between;
|
|
61
|
-
align-items: baseline;
|
|
62
|
-
margin-bottom: 2px;
|
|
75
|
+
#wind-gauge {
|
|
63
76
|
width: 100%;
|
|
77
|
+
height: auto;
|
|
78
|
+
max-height: 95vh;
|
|
79
|
+
object-fit: contain;
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
.label {
|
|
67
|
-
color: #666;
|
|
68
|
-
font-size: 0.6rem;
|
|
69
|
-
font-weight: bold;
|
|
70
|
-
text-transform: uppercase;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.unit {
|
|
74
|
-
color: #888;
|
|
75
|
-
font-size: 0.55rem;
|
|
76
|
-
font-weight: bold;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/* Valore Numerico Base (usato per i riquadri con grafici) */
|
|
80
|
-
.value {
|
|
81
|
-
color: #fff;
|
|
82
|
-
font-size: 2.4rem;
|
|
83
|
-
font-weight: 600;
|
|
84
|
-
line-height: 0.9;
|
|
85
|
-
letter-spacing: -1px;
|
|
86
|
-
transition: color 0.3s ease;
|
|
87
|
-
padding-bottom: 5px;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/* Valore Numerico Dinamico (usato per i riquadri SENZA grafici) */
|
|
91
|
-
.value-large {
|
|
92
|
-
margin-top: auto; /* Spinge il numero verso il basso */
|
|
93
|
-
/*
|
|
94
|
-
clamp(min, preferito, max)
|
|
95
|
-
Minimo: 2.4rem (come i grafici)
|
|
96
|
-
Preferito: 7.5vh (scala molto in base all'altezza schermo)
|
|
97
|
-
Massimo: 4.5rem (font molto grande ma sicuro per non sbordare)
|
|
98
|
-
*/
|
|
99
|
-
font-size: clamp(2.4rem, 7.5vh, 4.5rem);
|
|
100
|
-
line-height: 0.85; /* Evita che il font grande sposti il layout */
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/* Etichetta periodo media (es. per future implementazioni) */
|
|
104
|
-
.period-label {
|
|
105
|
-
position: absolute;
|
|
106
|
-
bottom: 4px;
|
|
107
|
-
left: 8px;
|
|
108
|
-
font-size: 0.5rem;
|
|
109
|
-
color: #444;
|
|
110
|
-
font-weight: bold;
|
|
111
|
-
text-transform: uppercase;
|
|
112
|
-
}
|
|
113
|
-
.right-panel .period-label { left: auto; right: 8px; }
|
|
114
|
-
|
|
115
82
|
/* ==========================================================================
|
|
116
|
-
4.
|
|
83
|
+
4. TIPOGRAFIA ED ETICHETTE
|
|
117
84
|
========================================================================== */
|
|
118
|
-
|
|
119
|
-
.
|
|
120
|
-
|
|
121
|
-
justify-content: space-between;
|
|
122
|
-
align-items: flex-end;
|
|
123
|
-
width: 100%;
|
|
124
|
-
margin-top: auto;
|
|
125
|
-
padding-bottom: 5px;
|
|
126
|
-
}
|
|
85
|
+
.label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
|
|
86
|
+
.label { color: #666; font-size: 0.6rem; font-weight: bold; text-transform: uppercase; }
|
|
87
|
+
.unit { color: #888; font-size: 0.55rem; font-weight: bold; }
|
|
127
88
|
|
|
128
|
-
.
|
|
129
|
-
|
|
130
|
-
flex-direction: column;
|
|
131
|
-
justify-content: space-between; /* Spinge l'etichetta su e il numero giù */
|
|
132
|
-
width: 48%; /* Lascia un piccolo margine al centro */
|
|
133
|
-
height: 100%; /* Prende tutta l'altezza disponibile nel contenitore */
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.dual-value-col.right-col {
|
|
137
|
-
align-items: flex-end;
|
|
138
|
-
text-align: right;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.dual-label {
|
|
142
|
-
color: #666;
|
|
143
|
-
font-size: 0.55rem;
|
|
144
|
-
font-weight: bold;
|
|
145
|
-
text-transform: uppercase;
|
|
146
|
-
margin-bottom: auto; /* Mantiene l'etichetta schiacciata verso l'alto */
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.value.dual-val {
|
|
150
|
-
font-size: 1.8rem; /* Font leggermente più piccolo per farne stare due */
|
|
151
|
-
padding-bottom: 0;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/* Layout Bussolina TWD */
|
|
155
|
-
.value-with-compass {
|
|
156
|
-
display: flex;
|
|
157
|
-
justify-content: space-between;
|
|
158
|
-
align-items: center;
|
|
159
|
-
width: 100%;
|
|
160
|
-
margin-top: auto;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.mini-compass {
|
|
164
|
-
width: 38px;
|
|
165
|
-
height: 38px;
|
|
166
|
-
background: rgba(0,0,0,0.2);
|
|
167
|
-
border-radius: 50%;
|
|
168
|
-
flex-shrink: 0;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.value-with-compass .value-large {
|
|
172
|
-
margin-top: 0; /* Annulla il margin-top perché gestito dal padre flex */
|
|
173
|
-
}
|
|
89
|
+
.value { color: #fff; font-size: 2.4rem; font-weight: 600; line-height: 0.9; letter-spacing: -1px; transition: color 0.3s ease; padding-bottom: 5px; }
|
|
90
|
+
.value-large { margin-top: auto; font-size: clamp(2.4rem, 7.5vh, 4.5rem); line-height: 0.85; }
|
|
174
91
|
|
|
175
92
|
/* ==========================================================================
|
|
176
|
-
5.
|
|
93
|
+
5. LAYOUT SPECIALI (TACK e TWD COMPASS)
|
|
177
94
|
========================================================================== */
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
margin-top: 4px;
|
|
184
|
-
}
|
|
95
|
+
.dual-value-container { display: flex; justify-content: space-between; align-items: flex-end; width: 100%; margin-top: auto; padding-bottom: 5px; }
|
|
96
|
+
.dual-value-col { display: flex; flex-direction: column; justify-content: space-between; width: 48%; height: 100%; }
|
|
97
|
+
.dual-value-col.right-col { align-items: flex-end; text-align: right; }
|
|
98
|
+
.dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: auto; }
|
|
99
|
+
.value.dual-val { font-size: 1.8rem; padding-bottom: 0; }
|
|
185
100
|
|
|
186
|
-
.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
background: rgba(255, 255, 255, 0.03);
|
|
190
|
-
border-radius: 4px;
|
|
191
|
-
}
|
|
101
|
+
.value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: auto; }
|
|
102
|
+
.mini-compass { width: 38px; height: 38px; background: rgba(0,0,0,0.2); border-radius: 50%; flex-shrink: 0; }
|
|
103
|
+
.value-with-compass .value-large { margin-top: 0; }
|
|
192
104
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
justify-content: space-between;
|
|
200
|
-
font-size: 8px;
|
|
201
|
-
color: #555;
|
|
202
|
-
padding: 1px 5px;
|
|
203
|
-
font-weight: bold;
|
|
204
|
-
}
|
|
105
|
+
/* ==========================================================================
|
|
106
|
+
6. GRAFICI STORICI (SPARKLINES)
|
|
107
|
+
========================================================================== */
|
|
108
|
+
.graph-wrapper { position: relative; width: 100%; flex-grow: 1; min-height: 20px; margin-top: 4px; }
|
|
109
|
+
.sparkline { width: 100%; height: 100%; background: rgba(255, 255, 255, 0.03); border-radius: 4px; }
|
|
110
|
+
.scale-labels { position: absolute; top: 0; height: 100%; display: flex; flex-direction: column; justify-content: space-between; font-size: 8px; color: #555; padding: 1px 5px; font-weight: bold; }
|
|
205
111
|
.scale-labels.right { right: 0; text-align: right; }
|
|
206
112
|
.scale-labels.left { left: 0; text-align: left; }
|
|
207
113
|
|
|
208
|
-
/* Colori specifici delle linee dei grafici */
|
|
209
114
|
#stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.15); }
|
|
210
115
|
#sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.15); }
|
|
211
116
|
#depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.15); }
|
|
212
117
|
#tws-graph { stroke: #f1c40f; fill: rgba(241, 196, 15, 0.15); }
|
|
213
118
|
|
|
214
119
|
/* ==========================================================================
|
|
215
|
-
|
|
120
|
+
7. STATI E ANIMAZIONI
|
|
216
121
|
========================================================================== */
|
|
217
|
-
.
|
|
218
|
-
flex: 3.5;
|
|
219
|
-
display: flex;
|
|
220
|
-
flex-direction: column;
|
|
221
|
-
justify-content: flex-start;
|
|
222
|
-
align-items: center;
|
|
223
|
-
height: 100%;
|
|
224
|
-
padding: 0;
|
|
225
|
-
margin: 0;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
#wind-gauge {
|
|
229
|
-
width: 100%;
|
|
230
|
-
height: auto;
|
|
231
|
-
max-height: 95vh;
|
|
232
|
-
object-fit: contain;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/* ==========================================================================
|
|
236
|
-
7. STATI, ALLARMI E ANIMAZIONI
|
|
237
|
-
========================================================================== */
|
|
238
|
-
/* Stato Connessione */
|
|
239
|
-
#status {
|
|
240
|
-
position: absolute;
|
|
241
|
-
top: 5px;
|
|
242
|
-
right: 15px;
|
|
243
|
-
font-size: 0.5rem;
|
|
244
|
-
text-transform: uppercase;
|
|
245
|
-
z-index: 1000;
|
|
246
|
-
}
|
|
122
|
+
#status { position: absolute; top: 5px; right: 15px; font-size: 0.5rem; text-transform: uppercase; z-index: 1000; }
|
|
247
123
|
.online { color: #2ecc71; opacity: 0.5; }
|
|
248
124
|
.offline { color: #e74c3c; font-weight: bold; }
|
|
249
125
|
|
|
250
|
-
|
|
251
|
-
#
|
|
252
|
-
transition: all 0.6s cubic-bezier(0.1, 0.7, 0.1, 1);
|
|
253
|
-
}
|
|
254
|
-
#leeway-mask-rect { transition: none; } /* Deve essere istantaneo */
|
|
126
|
+
#awa-pointer, #twa-pointer, #track-pointer, #twd-arrow { transition: all 0.6s cubic-bezier(0.1, 0.7, 0.1, 1); }
|
|
127
|
+
#leeway-mask-rect { transition: none; }
|
|
255
128
|
|
|
256
|
-
/* Allarmi Profondità */
|
|
257
129
|
.alarm-warning { color: #f1c40f !important; }
|
|
258
130
|
.alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
|
|
259
131
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
132
|
+
@keyframes blink-unstable { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
|
|
133
|
+
.unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
|
|
134
|
+
|
|
135
|
+
/* ==========================================================================
|
|
136
|
+
8. RESPONSIVE LAYOUT (Schermi < 960px - Tablet Verticali e Smartphone)
|
|
137
|
+
========================================================================== */
|
|
138
|
+
@media (max-width: 959px) {
|
|
139
|
+
/* Il Vento viene forzato come primo elemento e occupa mezza altezza */
|
|
140
|
+
.center-panel {
|
|
141
|
+
order: 1;
|
|
142
|
+
flex: 0 0 100%; /* Larghezza 100% */
|
|
143
|
+
height: 50%; /* Altezza 50% dello schermo */
|
|
144
|
+
padding-bottom: 5px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#wind-gauge {
|
|
148
|
+
max-height: 100%;
|
|
149
|
+
width: auto;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Il Pannello Sinistro si mette sotto a sinistra */
|
|
153
|
+
.side-panel {
|
|
154
|
+
order: 2;
|
|
155
|
+
flex: 0 0 calc(50% - 2.5px); /* Larghezza 50% meno un piccolo margine */
|
|
156
|
+
height: calc(50% - 5px); /* Altezza 50% dello schermo */
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Il Pannello Destro si mette sotto a destra */
|
|
160
|
+
.right-panel {
|
|
161
|
+
order: 3;
|
|
162
|
+
margin-left: auto; /* Lo spinge contro il bordo destro */
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Ridimensionamenti font per evitare sbordature su schermi stretti */
|
|
166
|
+
.value-large { font-size: clamp(2rem, 5vh, 3rem); }
|
|
167
|
+
.value.dual-val { font-size: 1.5rem; }
|
|
168
|
+
.data-box { padding: 5px 8px; }
|
|
269
169
|
}
|
package/app.js.txt
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
const SIGNALK_SERVER_IP = "192.168.111.240:3000";
|
|
2
|
-
const ALARM_DANGER_DEPTH = 2.5;
|
|
3
|
-
const ALARM_WARNING_DEPTH = 5.0;
|
|
4
|
-
|
|
5
|
-
const SMOOTH_WINDOW_MS = 2000;
|
|
6
|
-
const LONG_AVG_WINDOW_MS = 60000;
|
|
7
|
-
const RENDER_INTERVAL_MS = 1000;
|
|
8
|
-
const TIMEOUT_MS = 5000;
|
|
9
|
-
|
|
10
|
-
const MAX_HISTORY_SAMPLES = 60;
|
|
11
|
-
const REAL_SAMPLE_INTERVAL = 5000;
|
|
12
|
-
const SIM_SAMPLE_INTERVAL = 1000;
|
|
13
|
-
|
|
14
|
-
let simulationMode = false;
|
|
15
|
-
let socket;
|
|
16
|
-
let renderInterval = null;
|
|
17
|
-
let simInterval = null;
|
|
18
|
-
let lastAvgUIUpdate = 0;
|
|
19
|
-
let audioCtx = null, lastAlarmTime = 0;
|
|
20
|
-
|
|
21
|
-
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
22
|
-
|
|
23
|
-
const store = {
|
|
24
|
-
raw: {},
|
|
25
|
-
timestamps: {},
|
|
26
|
-
smoothBuf: { hdg: [], awa: [], twa: [], twd: [], leeway: [] },
|
|
27
|
-
longBuf: { awa: [], twa: [], twd: [] },
|
|
28
|
-
histories: { stw: [], sog: [], depth: [], tws: [] },
|
|
29
|
-
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 }
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const ui = {
|
|
33
|
-
stw: document.getElementById('stw'), sog: document.getElementById('sog'),
|
|
34
|
-
hdg: document.getElementById('hdg'), cog: document.getElementById('cog'),
|
|
35
|
-
awsSvg: document.getElementById('aws-val-svg'), awa: document.getElementById('awa-pointer'),
|
|
36
|
-
twa: document.getElementById('twa-pointer'), track: document.getElementById('track-pointer'),
|
|
37
|
-
tws: document.getElementById('tws'), depth: document.getElementById('depth'),
|
|
38
|
-
twaAvg: document.getElementById('twa-avg'), awaAvg: document.getElementById('awa-avg'),
|
|
39
|
-
twdAvg: document.getElementById('twd-avg'), twdArrow: document.getElementById('twd-arrow'),
|
|
40
|
-
leewayMask: document.getElementById('leeway-mask-rect'), leewayVal: document.getElementById('leeway-val'),
|
|
41
|
-
status: document.getElementById('status')
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
45
|
-
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
46
|
-
function msToKts(ms) { return ms * 1.94384; }
|
|
47
|
-
function ktsToMs(kts) { return kts / 1.94384; }
|
|
48
|
-
|
|
49
|
-
function getShortestRotation(curr, target) {
|
|
50
|
-
let diff = (target - curr) % 360;
|
|
51
|
-
if (diff > 180) diff -= 360; else if (diff < -180) diff += 360;
|
|
52
|
-
return curr + diff;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
56
|
-
const now = Date.now();
|
|
57
|
-
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
58
|
-
if (validData.length === 0) return null;
|
|
59
|
-
let sumSin = 0, sumCos = 0;
|
|
60
|
-
validData.forEach(item => { sumSin += Math.sin(item.val); sumCos += Math.cos(item.val); });
|
|
61
|
-
let avgRad = Math.atan2(sumSin, sumCos);
|
|
62
|
-
let avgDeg = Math.round(radToDeg(avgRad));
|
|
63
|
-
return signed ? avgDeg : (avgDeg + 360) % 360;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function cleanBuffer(bufferArray, windowMs) {
|
|
67
|
-
const now = Date.now();
|
|
68
|
-
while (bufferArray.length > 0 && (now - bufferArray[0].time) > windowMs) bufferArray.shift();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function processIncomingData(path, val) {
|
|
72
|
-
const now = Date.now();
|
|
73
|
-
store.timestamps[path] = now;
|
|
74
|
-
store.raw[path] = val;
|
|
75
|
-
|
|
76
|
-
if (path === "navigation.headingMagnetic") store.smoothBuf.hdg.push({ val: val, time: now });
|
|
77
|
-
if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
|
|
78
|
-
if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
|
|
79
|
-
|
|
80
|
-
if (path === "environment.wind.angleTrueWater" || path === "navigation.headingMagnetic") {
|
|
81
|
-
if (store.raw["navigation.headingMagnetic"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) {
|
|
82
|
-
let twdRad = (store.raw["navigation.headingMagnetic"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
|
|
83
|
-
if (twdRad < 0) twdRad += (2 * Math.PI);
|
|
84
|
-
store.raw["environment.wind.directionTrue"] = twdRad;
|
|
85
|
-
store.timestamps["environment.wind.directionTrue"] = now;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (store.raw["environment.wind.directionTrue"] !== undefined) {
|
|
90
|
-
store.smoothBuf.twd.push({ val: store.raw["environment.wind.directionTrue"], time: now });
|
|
91
|
-
store.longBuf.twd.push({ val: store.raw["environment.wind.directionTrue"], time: now });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (path === "environment.wind.angleTrueWater" || path === "environment.wind.speedTrue" || path === "navigation.speedThroughWater") {
|
|
95
|
-
if (store.raw["environment.wind.angleTrueWater"] !== undefined && store.raw["environment.wind.speedTrue"] !== undefined && store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
96
|
-
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
|
|
97
|
-
const stwKts = msToKts(store.raw["navigation.speedThroughWater"]);
|
|
98
|
-
const stwSafe = Math.max(stwKts, 0.1);
|
|
99
|
-
let leewayDeg = - (12 * twsKts / (stwSafe * stwSafe)) * Math.sin(store.raw["environment.wind.angleTrueWater"]);
|
|
100
|
-
leewayDeg = Math.max(-20, Math.min(20, leewayDeg));
|
|
101
|
-
store.raw["navigation.leewayAngle"] = degToRad(leewayDeg);
|
|
102
|
-
store.timestamps["navigation.leewayAngle"] = now;
|
|
103
|
-
store.smoothBuf.leeway.push({ val: degToRad(leewayDeg), time: now });
|
|
104
|
-
}
|
|
105
|
-
} else if (path === "navigation.leewayAngle") {
|
|
106
|
-
store.smoothBuf.leeway.push({ val: val, time: now });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// --- RENDER ENGINE PRINCIPALE (1Hz) ---
|
|
111
|
-
function startDisplayLoop() {
|
|
112
|
-
if (renderInterval) clearInterval(renderInterval);
|
|
113
|
-
|
|
114
|
-
renderInterval = setInterval(() => {
|
|
115
|
-
const now = Date.now();
|
|
116
|
-
|
|
117
|
-
// 1. Watchdog Timeout
|
|
118
|
-
const pathsToWatch = {
|
|
119
|
-
"navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
|
|
120
|
-
"navigation.headingMagnetic": ui.hdg, "environment.wind.speedApparent": ui.awsSvg,
|
|
121
|
-
"environment.depth.belowTransducer": ui.depth, "environment.wind.speedTrue": ui.tws
|
|
122
|
-
};
|
|
123
|
-
for (let p in pathsToWatch) {
|
|
124
|
-
if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
|
|
125
|
-
pathsToWatch[p][pathsToWatch[p] === ui.awsSvg ? 'textContent' : 'innerText'] = "---";
|
|
126
|
-
if (p === "environment.depth.belowTransducer") ui.depth.classList.remove('alarm-warning', 'alarm-danger');
|
|
127
|
-
delete store.raw[p];
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 2. Render Istantanei
|
|
132
|
-
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
133
|
-
const stw = msToKts(store.raw["navigation.speedThroughWater"]); ui.stw.innerText = stw.toFixed(1); manageHistory('stw', stw);
|
|
134
|
-
}
|
|
135
|
-
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
136
|
-
const sog = msToKts(store.raw["navigation.speedOverGround"]); ui.sog.innerText = sog.toFixed(1); manageHistory('sog', sog);
|
|
137
|
-
}
|
|
138
|
-
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
139
|
-
const d = store.raw["environment.depth.belowTransducer"]; ui.depth.innerText = d.toFixed(1); checkDepthAlarm(d); manageHistory('depth', d);
|
|
140
|
-
}
|
|
141
|
-
if (store.raw["environment.wind.speedTrue"] !== undefined) {
|
|
142
|
-
const tws = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = tws.toFixed(1); manageHistory('tws', tws);
|
|
143
|
-
}
|
|
144
|
-
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
145
|
-
|
|
146
|
-
// 3. Render Smussati (2s)
|
|
147
|
-
const smoothHdg = getCircularAverageFromBuffer(store.smoothBuf.hdg, SMOOTH_WINDOW_MS, false);
|
|
148
|
-
const smoothLeeway = getCircularAverageFromBuffer(store.smoothBuf.leeway, SMOOTH_WINDOW_MS, true);
|
|
149
|
-
|
|
150
|
-
if (smoothHdg !== null) {
|
|
151
|
-
ui.hdg.innerHTML = `${Math.round(smoothHdg).toString().padStart(3, '0')}°`; // Aggiunto gradi in chiaro
|
|
152
|
-
if (smoothLeeway !== null) {
|
|
153
|
-
let cog = (smoothHdg + smoothLeeway + 360) % 360;
|
|
154
|
-
ui.cog.innerHTML = `${Math.round(cog).toString().padStart(3, '0')}°`;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const smoothAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, SMOOTH_WINDOW_MS, true);
|
|
159
|
-
if (smoothAwa !== null) { curAwaRot = getShortestRotation(curAwaRot, smoothAwa); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
160
|
-
|
|
161
|
-
const smoothTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, SMOOTH_WINDOW_MS, true);
|
|
162
|
-
if (smoothTwa !== null) { curTwaRot = getShortestRotation(curTwaRot, smoothTwa); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
163
|
-
|
|
164
|
-
if (smoothLeeway !== null) {
|
|
165
|
-
updateLeewayDisplay(smoothLeeway);
|
|
166
|
-
curTrackRot = getShortestRotation(curTrackRot, smoothLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 4. Render Medie Lunghe (60s)
|
|
170
|
-
if (now - lastAvgUIUpdate > 3000) {
|
|
171
|
-
let awA = getCircularAverageFromBuffer(store.longBuf.awa, LONG_AVG_WINDOW_MS, true);
|
|
172
|
-
let twA = getCircularAverageFromBuffer(store.longBuf.twa, LONG_AVG_WINDOW_MS, true);
|
|
173
|
-
let twD = getCircularAverageFromBuffer(store.longBuf.twd, LONG_AVG_WINDOW_MS, false);
|
|
174
|
-
|
|
175
|
-
ui.awaAvg.innerHTML = awA === null ? `---°` : `${awA.toString().padStart(3, '0')}°`;
|
|
176
|
-
ui.twaAvg.innerHTML = twA === null ? `---°` : `${twA.toString().padStart(3, '0')}°`;
|
|
177
|
-
ui.twdAvg.innerHTML = twD === null ? `---°` : `${twD.toString().padStart(3, '0')}°`;
|
|
178
|
-
|
|
179
|
-
if (twD !== null) { curTwdRoseRot = getShortestRotation(curTwdRoseRot, twD); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); }
|
|
180
|
-
lastAvgUIUpdate = now;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
cleanBuffer(store.smoothBuf.hdg, SMOOTH_WINDOW_MS); cleanBuffer(store.smoothBuf.awa, SMOOTH_WINDOW_MS);
|
|
184
|
-
cleanBuffer(store.smoothBuf.twa, SMOOTH_WINDOW_MS); cleanBuffer(store.smoothBuf.twd, SMOOTH_WINDOW_MS);
|
|
185
|
-
cleanBuffer(store.smoothBuf.leeway, SMOOTH_WINDOW_MS); cleanBuffer(store.longBuf.awa, LONG_AVG_WINDOW_MS);
|
|
186
|
-
cleanBuffer(store.longBuf.twa, LONG_AVG_WINDOW_MS); cleanBuffer(store.longBuf.twd, LONG_AVG_WINDOW_MS);
|
|
187
|
-
|
|
188
|
-
}, RENDER_INTERVAL_MS);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
document.addEventListener('click', () => { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }, { once: true });
|
|
192
|
-
function playBingBing() {
|
|
193
|
-
if (!audioCtx) return; const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n;
|
|
194
|
-
function b(f, s) {
|
|
195
|
-
const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
|
|
196
|
-
o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f;
|
|
197
|
-
g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4);
|
|
198
|
-
o.start(s); o.stop(s + 0.5);
|
|
199
|
-
}
|
|
200
|
-
b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6);
|
|
201
|
-
}
|
|
202
|
-
function checkDepthAlarm(m) {
|
|
203
|
-
ui.depth.classList.remove('alarm-warning', 'alarm-danger');
|
|
204
|
-
if (m < ALARM_DANGER_DEPTH) { ui.depth.classList.add('alarm-danger'); playBingBing(); }
|
|
205
|
-
else if (m < ALARM_WARNING_DEPTH) ui.depth.classList.add('alarm-warning');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function connect() {
|
|
209
|
-
if (simulationMode) return;
|
|
210
|
-
socket = new WebSocket(`ws://${SIGNALK_SERVER_IP}/signalk/v1/stream?subscribe=all`);
|
|
211
|
-
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; };
|
|
212
|
-
socket.onmessage = (e) => {
|
|
213
|
-
if (simulationMode) return;
|
|
214
|
-
const d = JSON.parse(e.data);
|
|
215
|
-
if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value)));
|
|
216
|
-
};
|
|
217
|
-
socket.onclose = () => !simulationMode && setTimeout(connect, 5000);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const physicsEngine = {
|
|
221
|
-
time: 0,
|
|
222
|
-
config: { hdgStart: Math.random() * 360, hdgDir: Math.random() > 0.5 ? 1 : -1, twdBase: Math.random() * 360, rotSpeed: 360 / 600 },
|
|
223
|
-
getPolars: function(twaDeg, twsKts) {
|
|
224
|
-
let eff = 0; const aT = Math.abs(twaDeg);
|
|
225
|
-
if (aT < 30) eff = 0.1; else if (aT < 45) eff = 0.75; else if (aT < 90) eff = 1.0; else if (aT < 150) eff = 0.85; else eff = 0.65;
|
|
226
|
-
return Math.min(Math.sqrt(twsKts) * 2.2 * eff, 11.0);
|
|
227
|
-
},
|
|
228
|
-
step: function() {
|
|
229
|
-
this.time++;
|
|
230
|
-
const hdgDeg = (this.config.hdgStart + (this.time * this.config.rotSpeed * this.config.hdgDir) + 360) % 360;
|
|
231
|
-
const twdDeg = (this.config.twdBase + Math.sin(this.time / 20) * 15 + 360) % 360;
|
|
232
|
-
const twsKts = 14 + Math.sin(this.time / 40) * 6;
|
|
233
|
-
const depthM = 20 + Math.sin(this.time / 25) * 18;
|
|
234
|
-
let twaDeg = (twdDeg - hdgDeg + 360) % 360; if (twaDeg > 180) twaDeg -= 360;
|
|
235
|
-
const stwKts = this.getPolars(twaDeg, twsKts);
|
|
236
|
-
const stwMs = ktsToMs(stwKts), twsMs = ktsToMs(twsKts), twaRad = degToRad(twaDeg);
|
|
237
|
-
const vAx = twsMs * Math.sin(twaRad), vAy = twsMs * Math.cos(twaRad) + stwMs;
|
|
238
|
-
const awsMs = Math.sqrt(vAx*vAx + vAy*vAy), awaRad = Math.atan2(vAx, vAy);
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
"navigation.headingMagnetic": degToRad(hdgDeg),
|
|
242
|
-
"navigation.speedThroughWater": stwMs, "navigation.speedOverGround": stwMs * 1.05,
|
|
243
|
-
"environment.wind.speedTrue": twsMs, "environment.wind.directionTrue": degToRad(twdDeg),
|
|
244
|
-
"environment.wind.angleTrueWater": twaRad, "environment.wind.speedApparent": awsMs,
|
|
245
|
-
"environment.wind.angleApparent": awaRad, "environment.depth.belowTransducer": depthM
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
let dC = 0, lC = 0;
|
|
251
|
-
ui.depth.closest('.data-box').addEventListener('click', () => {
|
|
252
|
-
const n = Date.now(); if (n - lC < 500) dC++; else dC = 1; lC = n;
|
|
253
|
-
if (dC === 3) {
|
|
254
|
-
simulationMode = !simulationMode;
|
|
255
|
-
if (simulationMode) {
|
|
256
|
-
if (socket) socket.close(); ui.status.innerText = "SIM ENGINE ATTIVO";
|
|
257
|
-
if(simInterval) clearInterval(simInterval);
|
|
258
|
-
simInterval = setInterval(() => { const simulatedData = physicsEngine.step(); for (let path in simulatedData) processIncomingData(path, simulatedData[path]); }, 200);
|
|
259
|
-
} else location.reload();
|
|
260
|
-
dC = 0;
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
function updateLeewayDisplay(deg) {
|
|
265
|
-
const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125);
|
|
266
|
-
ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w);
|
|
267
|
-
ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function manageHistory(t, v) {
|
|
271
|
-
const n = Date.now(), i = simulationMode ? SIM_SAMPLE_INTERVAL : REAL_SAMPLE_INTERVAL;
|
|
272
|
-
if (n - store.lastUpdates[t] > i || store.histories[t].length === 0) {
|
|
273
|
-
store.histories[t].push(v); if (store.histories[t].length > MAX_HISTORY_SAMPLES) store.histories[t].shift(); store.lastUpdates[t] = n;
|
|
274
|
-
}
|
|
275
|
-
const m = Math.max(...store.histories[t], 1); const c = getDynamicScale(t, m);
|
|
276
|
-
updateScaleLabels(t, c.scale); drawGraph(store.histories[t], t + '-graph', c.scale, c.gridLines);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function getDynamicScale(t, m) {
|
|
280
|
-
if (t === 'stw' || t === 'sog') return { scale: 12, gridLines: [3, 6, 9] };
|
|
281
|
-
const s = { tws: [10, 25, 45, 60], depth: [10, 20, 50, 200] };
|
|
282
|
-
const av = s[t] || [10, 20, 50]; let sel = av[av.length - 1];
|
|
283
|
-
for (let x of av) if (m <= x) { sel = x; break; }
|
|
284
|
-
return { scale: sel, gridLines: [sel/4, sel/2, (sel/4)*3] };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function updateScaleLabels(t, s) { const el = document.getElementById(t + '-scale'); if (el) el.innerHTML = `<span>${s}</span><span>${s/2}</span><span>0</span>`; }
|
|
288
|
-
|
|
289
|
-
function drawGraph(d, id, s, gl) {
|
|
290
|
-
const svg = document.getElementById(id); if (!svg || d.length < 2) return;
|
|
291
|
-
const w = 200, h = 40; let gH = "";
|
|
292
|
-
gl.forEach(v => { const y = h - (v / s) * h; gH += `<line x1="0" y1="${y}" x2="${w}" y2="${y}" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />`; });
|
|
293
|
-
let p = ""; d.forEach((v, i) => { const x = (i/(MAX_HISTORY_SAMPLES - 1))*w; const y = h - (Math.min(v, s)/s)*h; p += `${i===0?'M':'L'} ${x} ${y} `; });
|
|
294
|
-
const aP = p + ` L ${((d.length-1)/(MAX_HISTORY_SAMPLES - 1))*w} ${h} L 0 ${h} Z`;
|
|
295
|
-
const clrs = { 'stw-graph': { s: '#2ecc71', f: 'rgba(46, 204, 113, 0.15)' }, 'sog-graph': { s: '#f39c12', f: 'rgba(243, 156, 18, 0.15)' }, 'depth-graph': { s: '#3498db', f: 'rgba(52, 152, 219, 0.15)' }, 'tws-graph': { s: '#f1c40f', f: 'rgba(241, 196, 15, 0.15)' } };
|
|
296
|
-
svg.innerHTML = `${gH}<path d="${aP}" fill="${clrs[id].f}" stroke="none" /><path d="${p}" fill="none" stroke="${clrs[id].s}" stroke-width="1.5" />`;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function generateTicks() {
|
|
300
|
-
const c = document.getElementById('ticks');
|
|
301
|
-
for (let i = 0; i < 360; i += 10) {
|
|
302
|
-
const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
303
|
-
const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40");
|
|
304
|
-
l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50));
|
|
305
|
-
l.setAttribute("stroke", m ? "#fff" : "#666");
|
|
306
|
-
l.setAttribute("stroke-width", m ? "2" : "1");
|
|
307
|
-
l.setAttribute("transform", `rotate(${i}, 200, 200)`);
|
|
308
|
-
c.appendChild(l);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
generateTicks(); startDisplayLoop(); connect();
|