@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.
Files changed (4) hide show
  1. package/app.js +21 -0
  2. package/package.json +1 -1
  3. package/style.css +89 -189
  4. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Dashboard pubblica per Bavaria 39 Rotevista",
5
5
  "main": "index.html",
6
6
  "publishConfig": {
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-items: stretch; /* Estende le colonne per tutta l'altezza */
24
+ align-content: stretch;
23
25
  }
24
26
 
25
27
  /* ==========================================================================
26
- 2. PANNELLI LATERALI E DATA-BOX (Riquadri)
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; /* Necessario per posizionare gli elementi assoluti interni */
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; /* Distribuisce lo spazio equamente in altezza */
69
+ flex: 1;
53
70
  }
54
71
 
55
72
  /* ==========================================================================
56
- 3. TIPOGRAFIA ED ETICHETTE
73
+ 3. STRUMENTO VENTO SVG
57
74
  ========================================================================== */
58
- .label-row {
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. LAYOUT SPECIALI (TACK e TWD COMPASS)
83
+ 4. TIPOGRAFIA ED ETICHETTE
117
84
  ========================================================================== */
118
- /* Layout a doppia colonna per il riquadro TACK */
119
- .dual-value-container {
120
- display: flex;
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
- .dual-value-col {
129
- display: flex;
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. GRAFICI STORICI (SPARKLINES)
93
+ 5. LAYOUT SPECIALI (TACK e TWD COMPASS)
177
94
  ========================================================================== */
178
- .graph-wrapper {
179
- position: relative;
180
- width: 100%;
181
- flex-grow: 1; /* Il contenitore si espande in altezza */
182
- min-height: 20px;
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
- .sparkline {
187
- width: 100%;
188
- height: 100%;
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
- .scale-labels {
194
- position: absolute;
195
- top: 0;
196
- height: 100%;
197
- display: flex;
198
- flex-direction: column;
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
- 6. PANNELLO CENTRALE (STRUMENTO VENTO SVG)
120
+ 7. STATI E ANIMAZIONI
216
121
  ========================================================================== */
217
- .center-panel {
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
- /* Animazione fluidità lancette SVG */
251
- #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow {
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
- /* Lampeggio dati instabili / Ricalcolo buffer */
261
- @keyframes blink-unstable {
262
- 0% { opacity: 1; }
263
- 50% { opacity: 0.3; }
264
- 100% { opacity: 1; }
265
- }
266
- .unstable-data {
267
- animation: blink-unstable 1.5s infinite ease-in-out;
268
- color: #f39c12 !important; /* Arancione per evidenziare il ricalcolo */
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')}&deg;`; // 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')}&deg;`;
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 ? `---&deg;` : `${awA.toString().padStart(3, '0')}&deg;`;
176
- ui.twaAvg.innerHTML = twA === null ? `---&deg;` : `${twA.toString().padStart(3, '0')}&deg;`;
177
- ui.twdAvg.innerHTML = twD === null ? `---&deg;` : `${twD.toString().padStart(3, '0')}&deg;`;
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();