@sailingrotevista/rotevista-dash 3.0.6 → 4.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 +468 -329
- package/index.js +71 -59
- package/package.json +1 -1
- package/settings.json +15 -9
- package/style.css +86 -57
package/app.js
CHANGED
|
@@ -1,35 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ==========================================================================
|
|
3
|
+
* Signal K Wind Dashboard - Pro Version 2.4 (Definitive)
|
|
4
|
+
* ==========================================================================
|
|
5
|
+
* Autore: Sailing Rotevista
|
|
6
|
+
* Motore di calcolo tattico per navigazione e crociera.
|
|
7
|
+
* Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico a 15min,
|
|
8
|
+
* Memoria UI persistente, Modalità Hercules e Focus Split Screen.
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
// ==========================================================================
|
|
2
12
|
// 1. CONFIGURAZIONE E DEFAULT
|
|
3
13
|
// ==========================================================================
|
|
4
14
|
let CONFIG = {
|
|
5
|
-
alarms: {
|
|
6
|
-
|
|
15
|
+
alarms: {
|
|
16
|
+
depthDanger: 2.5,
|
|
17
|
+
depthWarning: 5.0
|
|
18
|
+
},
|
|
19
|
+
// Gestione parametri di stabilità e medie
|
|
7
20
|
averages: {
|
|
8
|
-
smoothWindow: 2000,
|
|
9
|
-
longWindow: 30000,
|
|
10
|
-
stabilityTolerance: 2000,
|
|
11
|
-
stabilityThreshold: 0.
|
|
12
|
-
minSpeed:
|
|
21
|
+
smoothWindow: 2000, // Smoothing puntatori (2s)
|
|
22
|
+
longWindow: 30000, // Finestra per i valori MEAN (30s)
|
|
23
|
+
stabilityTolerance: 2000, // Millisecondi per considerare il buffer "pieno"
|
|
24
|
+
stabilityThreshold: 0.85, // Soglia coerenza R per il lampeggio (0.7 - 0.98)
|
|
25
|
+
minSpeed: 0, // Nodi minimi per attivare gli allarmi di instabilità
|
|
26
|
+
stabilityBreakout: 15
|
|
27
|
+
},
|
|
28
|
+
// Parametri per i grafici sparkline
|
|
29
|
+
graphs: {
|
|
30
|
+
reef1: 15.0, // Soglia primo reef (Orange)
|
|
31
|
+
reef2: 20.0, // Soglia secondo reef (Red)
|
|
32
|
+
historyMinutes: 5, // Finestra temporale visualizzata
|
|
33
|
+
samples: 60 // Numero di punti di campionamento
|
|
13
34
|
},
|
|
14
|
-
|
|
35
|
+
// Configurazioni scale automatiche
|
|
15
36
|
scales: {
|
|
16
37
|
stw: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
17
38
|
sog: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
18
39
|
tws: { stdMax: 25, hercSpan: 10, step: 5 },
|
|
19
40
|
depth: { stdMax: 20, hercSpan: 10, step: 10 }
|
|
20
41
|
},
|
|
21
|
-
server: {
|
|
42
|
+
server: {
|
|
43
|
+
fallbackIp: "192.168.111.240:3000"
|
|
44
|
+
}
|
|
22
45
|
};
|
|
23
46
|
|
|
24
47
|
const RENDER_INTERVAL_MS = 1000;
|
|
25
48
|
const TIMEOUT_MS = 5000;
|
|
26
49
|
const SIM_SAMPLE_INTERVAL = 1000;
|
|
50
|
+
const DASH_VERSION = "2.4"; // Versione per la gestione della memoria locale
|
|
27
51
|
|
|
28
52
|
// ==========================================================================
|
|
29
53
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
30
54
|
// ==========================================================================
|
|
31
55
|
let simulationMode = false;
|
|
32
|
-
let displayModeSog = 'SOG';
|
|
56
|
+
let displayModeSog = 'SOG'; // Può essere 'SOG' o 'VMG'
|
|
33
57
|
let socket, renderInterval, simInterval;
|
|
34
58
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
35
59
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
@@ -42,23 +66,27 @@ let twDirty = false, isNavigating = false, reconnectDelay = 1000;
|
|
|
42
66
|
|
|
43
67
|
let pressTimer, isFocusActive = false;
|
|
44
68
|
|
|
69
|
+
// Stato dei singoli grafici (Standard vs Hercules Zoom)
|
|
45
70
|
const graphModes = {
|
|
46
|
-
stw:
|
|
47
|
-
sog:
|
|
48
|
-
tws:
|
|
49
|
-
depth:
|
|
71
|
+
stw: 'standard',
|
|
72
|
+
sog: 'standard',
|
|
73
|
+
tws: 'standard',
|
|
74
|
+
depth: 'standard'
|
|
50
75
|
};
|
|
51
76
|
|
|
77
|
+
// Database centrale dello store dati
|
|
52
78
|
const store = {
|
|
53
|
-
raw: {},
|
|
79
|
+
raw: {},
|
|
80
|
+
timestamps: {},
|
|
54
81
|
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
55
82
|
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
56
83
|
histories: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
|
|
57
|
-
// Buffer temporaneo per
|
|
84
|
+
// Buffer temporaneo per il calcolo della media dell'intervallo del grafico
|
|
58
85
|
graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
|
|
59
86
|
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0 }
|
|
60
87
|
};
|
|
61
88
|
|
|
89
|
+
// Riferimenti agli elementi DOM mappati all'avvio
|
|
62
90
|
const ui = {
|
|
63
91
|
stw: document.getElementById('stw'), sog: document.getElementById('sog'),
|
|
64
92
|
hdg: document.getElementById('hdg'), cog: document.getElementById('cog'),
|
|
@@ -75,35 +103,54 @@ const ui = {
|
|
|
75
103
|
};
|
|
76
104
|
|
|
77
105
|
// ==========================================================================
|
|
78
|
-
// 3. UTILITIES (MATEMATICA, BUFFER, AUDIO)
|
|
106
|
+
// 3. UTILITIES (MATEMATICA, BUFFER, AUDIO, MEMORIA)
|
|
79
107
|
// ==========================================================================
|
|
80
108
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
81
109
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
82
110
|
function msToKts(ms) { return ms * 1.94384; }
|
|
83
111
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
84
|
-
function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
|
|
85
112
|
|
|
86
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Calcola il percorso più breve per la rotazione di un puntatore (evita giri di 360°)
|
|
115
|
+
*/
|
|
116
|
+
function getShortestRotation(curr, target) {
|
|
117
|
+
let diff = (target - curr) % 360;
|
|
118
|
+
if (diff > 180) diff -= 360;
|
|
119
|
+
else if (diff < -180) diff += 360;
|
|
120
|
+
return curr + diff;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Inserisce un dato nel buffer circolare limitandolo a 2000 campioni (30 min)
|
|
125
|
+
*/
|
|
126
|
+
function safePush(buffer, val, time, maxLen = 2000) {
|
|
87
127
|
buffer.push({ val: val, time: time });
|
|
88
128
|
if (buffer.length > maxLen) { buffer.shift(); }
|
|
89
129
|
}
|
|
90
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Media Circolare Vettoriale: Calcola angolo medio, stabilità R e deviazione standard (±)
|
|
133
|
+
*/
|
|
91
134
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
92
135
|
const now = Date.now();
|
|
93
136
|
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
94
137
|
if (validData.length === 0) return null;
|
|
138
|
+
|
|
95
139
|
let sSin = 0, sCos = 0;
|
|
96
|
-
validData.forEach(item => {
|
|
140
|
+
validData.forEach(item => {
|
|
141
|
+
sSin += Math.sin(item.val);
|
|
142
|
+
sCos += Math.cos(item.val);
|
|
143
|
+
});
|
|
97
144
|
|
|
98
145
|
let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
|
|
99
|
-
let
|
|
146
|
+
let historyDuration = (validData.length > 2) ? (validData[validData.length - 1].time - validData[0].time) : 0;
|
|
147
|
+
|
|
148
|
+
// Un dato è stabile se abbiamo abbastanza storia e la coerenza vettoriale R è alta
|
|
149
|
+
let isStable = (historyDuration > 10000) && (R > CONFIG.averages.stabilityThreshold);
|
|
100
150
|
let avgRad = Math.atan2(sSin, sCos);
|
|
101
151
|
|
|
102
|
-
// Calcolo
|
|
103
|
-
let deviation = 0;
|
|
104
|
-
if (R < 1 && R > 0) {
|
|
105
|
-
deviation = Math.round(Math.sqrt(-2 * Math.log(R)) * (180 / Math.PI));
|
|
106
|
-
}
|
|
152
|
+
// Calcolo della Deviazione Standard Circolare (±) in gradi
|
|
153
|
+
let deviation = (R < 1 && R > 0) ? Math.round(Math.sqrt(-2 * Math.log(R)) * (180 / Math.PI)) : 0;
|
|
107
154
|
|
|
108
155
|
return {
|
|
109
156
|
val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI),
|
|
@@ -112,18 +159,98 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
|
112
159
|
};
|
|
113
160
|
}
|
|
114
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Salva lo stato attuale della dashboard (dati e preferenze UI) nel browser
|
|
164
|
+
*/
|
|
165
|
+
function saveDashboardState() {
|
|
166
|
+
try {
|
|
167
|
+
const focusedBox = document.querySelector('.data-box.is-focused');
|
|
168
|
+
let focusedType = null;
|
|
169
|
+
if (focusedBox) {
|
|
170
|
+
const match = focusedBox.className.match(/box-([a-z]+)/);
|
|
171
|
+
if (match) focusedType = match[1];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const state = {
|
|
175
|
+
version: DASH_VERSION,
|
|
176
|
+
histories: store.histories,
|
|
177
|
+
longBuf: store.longBuf,
|
|
178
|
+
displayModeSog: displayModeSog,
|
|
179
|
+
graphModes: graphModes,
|
|
180
|
+
isNightMode: document.body.classList.contains('night-mode'),
|
|
181
|
+
isFocusActive: isFocusActive,
|
|
182
|
+
focusedBoxType: focusedType,
|
|
183
|
+
timestamp: Date.now()
|
|
184
|
+
};
|
|
185
|
+
localStorage.setItem('rotevista_dash_state', JSON.stringify(state));
|
|
186
|
+
} catch (e) { console.error("Save error:", e); }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Carica e ripristina lo stato salvato (entro i 20 minuti di vecchiaia)
|
|
191
|
+
*/
|
|
192
|
+
function loadDashboardState() {
|
|
193
|
+
const saved = localStorage.getItem('rotevista_dash_state');
|
|
194
|
+
if (!saved) return;
|
|
195
|
+
try {
|
|
196
|
+
const state = JSON.parse(saved);
|
|
197
|
+
if (state.version !== DASH_VERSION) { localStorage.removeItem('rotevista_dash_state'); return; }
|
|
198
|
+
|
|
199
|
+
if ((Date.now() - state.timestamp) / 60000 < 20) {
|
|
200
|
+
if (state.histories) Object.assign(store.histories, state.histories);
|
|
201
|
+
if (state.longBuf) Object.assign(store.longBuf, state.longBuf);
|
|
202
|
+
if (state.graphModes) Object.assign(graphModes, state.graphModes);
|
|
203
|
+
|
|
204
|
+
// Ripristino SOG/VMG
|
|
205
|
+
if (state.displayModeSog) {
|
|
206
|
+
displayModeSog = state.displayModeSog;
|
|
207
|
+
const labelEl = document.getElementById('sog-vmg-label');
|
|
208
|
+
if (labelEl) labelEl.textContent = displayModeSog;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Ripristino Tema Notte
|
|
212
|
+
if (state.isNightMode) document.body.classList.add('night-mode');
|
|
213
|
+
|
|
214
|
+
// Ripristino Focus (Dual Screen)
|
|
215
|
+
if (state.isFocusActive && state.focusedBoxType) {
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
const el = document.querySelector(`.box-${state.focusedBoxType}`);
|
|
218
|
+
if (el) { isFocusActive = false; toggleFocusMode(state.focusedBoxType, el); }
|
|
219
|
+
}, 200);
|
|
220
|
+
}
|
|
221
|
+
console.log("Stato ripristinato con successo dalla cache.");
|
|
222
|
+
}
|
|
223
|
+
} catch (e) { localStorage.removeItem('rotevista_dash_state'); }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ==========================================================================
|
|
227
|
+
// 4. AUDIO E ALLARMI
|
|
228
|
+
// ==========================================================================
|
|
115
229
|
function playBingBing() {
|
|
116
230
|
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
117
231
|
const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n;
|
|
118
|
-
function b(f, s) {
|
|
232
|
+
function b(f, s) {
|
|
233
|
+
const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
|
|
234
|
+
o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f;
|
|
235
|
+
g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4);
|
|
236
|
+
o.start(s); o.stop(s + 0.5);
|
|
237
|
+
}
|
|
119
238
|
b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6);
|
|
120
239
|
}
|
|
121
240
|
|
|
122
241
|
function playGybeAlarm() {
|
|
123
242
|
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
124
243
|
const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
|
|
125
|
-
function note(f, s, d) {
|
|
126
|
-
|
|
244
|
+
function note(f, s, d) {
|
|
245
|
+
const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
|
|
246
|
+
o.connect(g); g.connect(audioCtx.destination); o.type = 'square';
|
|
247
|
+
o.frequency.value = f; g.gain.setValueAtTime(0.05, s);
|
|
248
|
+
g.gain.exponentialRampToValueAtTime(0.001, s + d); o.start(s); o.stop(s + d);
|
|
249
|
+
}
|
|
250
|
+
for (let i = 0; i < 4; i++) {
|
|
251
|
+
note(1800, audioCtx.currentTime + (i * 0.15), 0.1);
|
|
252
|
+
note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1);
|
|
253
|
+
}
|
|
127
254
|
}
|
|
128
255
|
|
|
129
256
|
function checkDepthAlarm(m) {
|
|
@@ -139,330 +266,367 @@ function updateLeewayDisplay(deg) {
|
|
|
139
266
|
}
|
|
140
267
|
|
|
141
268
|
// ==========================================================================
|
|
142
|
-
//
|
|
269
|
+
// 5. MOTORE DI CALCOLO VENTO E DATA ROUTING
|
|
143
270
|
// ==========================================================================
|
|
144
271
|
function computeTrueWind() {
|
|
145
|
-
const aws = store.raw["environment.wind.speedApparent"];
|
|
146
|
-
let awa = store.raw["environment.wind.angleApparent"];
|
|
272
|
+
const aws = store.raw["environment.wind.speedApparent"], awa = store.raw["environment.wind.angleApparent"];
|
|
147
273
|
const stw = store.raw["navigation.speedThroughWater"] || 0, sog = store.raw["navigation.speedOverGround"] || 0;
|
|
148
274
|
const hdg = store.raw["navigation.headingTrue"] || 0, cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
149
|
-
|
|
150
275
|
if (aws === undefined || awa === undefined) return;
|
|
151
|
-
if (awa > Math.PI) awa -= 2 * Math.PI;
|
|
152
276
|
|
|
277
|
+
// Vento Reale Rispetto all'acqua (TWA/TWS Water)
|
|
153
278
|
const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
|
|
154
279
|
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
155
280
|
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const tw_ground_x = aws * Math.cos(awa) -
|
|
281
|
+
// Vento Reale Rispetto al fondo (TWD Ground)
|
|
282
|
+
const drift = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
|
|
283
|
+
const tw_ground_x = aws * Math.cos(awa) - sog * Math.cos(drift), tw_ground_y = aws * Math.sin(awa) - sog * Math.sin(drift);
|
|
159
284
|
const tws_ground = Math.sqrt(tw_ground_x * tw_ground_x + tw_ground_y * tw_ground_y);
|
|
160
285
|
|
|
161
286
|
const now = Date.now();
|
|
162
287
|
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
163
|
-
|
|
164
288
|
if (tws_water > 0.05) {
|
|
165
|
-
const
|
|
166
|
-
store.raw["environment.wind.angleTrueWater"] =
|
|
167
|
-
safePush(store.smoothBuf.twa,
|
|
289
|
+
const twa = Math.atan2(tw_water_y, tw_water_x);
|
|
290
|
+
store.raw["environment.wind.angleTrueWater"] = twa;
|
|
291
|
+
safePush(store.smoothBuf.twa, twa, now); safePush(store.longBuf.twa, twa, now);
|
|
168
292
|
}
|
|
169
293
|
if (tws_ground > 0.05) {
|
|
170
|
-
let
|
|
171
|
-
store.raw["environment.wind.directionTrue"] =
|
|
172
|
-
safePush(store.smoothBuf.twd,
|
|
294
|
+
let twd = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
|
|
295
|
+
store.raw["environment.wind.directionTrue"] = twd;
|
|
296
|
+
safePush(store.smoothBuf.twd, twd, now); safePush(store.longBuf.twd, twd, now);
|
|
173
297
|
}
|
|
174
298
|
}
|
|
175
299
|
|
|
176
300
|
function processIncomingData(path, val) {
|
|
177
301
|
const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
|
|
178
|
-
if (path === "navigation.position") store.raw["navigation.position"] = val;
|
|
179
302
|
if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
|
|
180
|
-
|
|
181
303
|
const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
|
|
182
304
|
if (twPaths.includes(path)) twDirty = true;
|
|
183
305
|
if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
|
|
184
|
-
|
|
185
306
|
if (path === "navigation.headingTrue") { safePush(store.smoothBuf.hdg, val, now); safePush(store.longBuf.hdg, val, now); }
|
|
186
307
|
if (path === "navigation.courseOverGroundTrue") { safePush(store.smoothBuf.cog, val, now); safePush(store.longBuf.cog, val, now); }
|
|
187
308
|
}
|
|
188
309
|
|
|
189
310
|
// ==========================================================================
|
|
190
|
-
//
|
|
311
|
+
// 6. TREND VENTO (Tattico 2s vs 10s | Strategico 1m vs 10m)
|
|
191
312
|
// ==========================================================================
|
|
192
|
-
/**
|
|
193
|
-
* Analizza i trend di rotazione del vento su due scale temporali:
|
|
194
|
-
* 1. Tattica (veloce): per la regolazione delle vele (sulla lancetta TWA)
|
|
195
|
-
* 2. Strategica (lenta): per le previsioni meteo a lungo termine (bussola TWD)
|
|
196
|
-
*/
|
|
197
313
|
function updateWindTrend() {
|
|
198
314
|
const now = Date.now();
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// ALLARME STRAMBATA
|
|
210
|
-
const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
|
|
211
|
-
lastInstantTwa = instantTwaDeg;
|
|
212
|
-
|
|
315
|
+
// TATTICA (Lancetta): 2s vs 10s (reazione rapida per trim)
|
|
316
|
+
const twaNow = getCircularAverageFromBuffer(store.longBuf.twa, 2000, true);
|
|
317
|
+
const twaRef = getCircularAverageFromBuffer(store.longBuf.twa, 10000, true);
|
|
318
|
+
|
|
319
|
+
// STRATEGIA (Bussola TWD): 1 min vs 30 minuti (tendenza meteo profonda)
|
|
320
|
+
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
321
|
+
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, 1800000, false); // 1.800.000 ms = 30 min
|
|
322
|
+
|
|
323
|
+
if (!twaNow || !twaRef || !twdNow || !twdRef) return;
|
|
213
324
|
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
214
325
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
215
326
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// CALCOLO TREND
|
|
223
|
-
let diff = (shortAvgDeg - lastShortAvgVal + 540) % 360 - 180; lastShortAvgVal = shortAvgDeg;
|
|
224
|
-
if (dt > 0) {
|
|
225
|
-
let rate = Math.max(-10, Math.min(10, diff / dt));
|
|
226
|
-
// Tattico (veloce ~15s)
|
|
227
|
-
const alphaT = Math.min(1, dt / 15);
|
|
228
|
-
rotationTrend = rotationTrend * (1 - alphaT) + rate * alphaT;
|
|
229
|
-
// Strategico (molto lento ~8-10min)
|
|
230
|
-
const alphaM = Math.min(1, dt / 500);
|
|
231
|
-
meteoTrend = meteoTrend * (1 - alphaM) + rate * alphaM;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// VISUALIZZAZIONE METEO (Bussola Centrale)
|
|
235
|
-
if (Math.abs(meteoTrend) > 0.2) {
|
|
327
|
+
// TREND METEO (Bussola Centrale TWD)
|
|
328
|
+
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
329
|
+
if (Math.abs(deltaMeteo) > 6.0) {
|
|
236
330
|
const isSouth = store.raw["navigation.position"]?.latitude < 0;
|
|
237
|
-
let meteoColor = (!isSouth) ? (
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
331
|
+
let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#27ae60" : "#c0392b") : (deltaMeteo > 0 ? "#27ae60" : "#c0392b");
|
|
332
|
+
if (deltaMeteo > 0) {
|
|
333
|
+
compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor);
|
|
334
|
+
compassDots.ccw.classList.remove('is-trending');
|
|
241
335
|
} else {
|
|
242
|
-
|
|
243
|
-
|
|
336
|
+
compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor);
|
|
337
|
+
compassDots.cw.classList.remove('is-trending');
|
|
244
338
|
}
|
|
245
|
-
} else {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (Math.abs(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
339
|
+
} else { [compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
340
|
+
|
|
341
|
+
// TREND TATTICO (Lancetta Arancione)
|
|
342
|
+
let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
343
|
+
const curTwaDeg = radToDeg(twaNow.val);
|
|
344
|
+
if (Math.abs(deltaTac) > 3.0) {
|
|
345
|
+
// Calcolo logica tattica: LIFT (Verde), HEADER (Rosso) o NEUTRO (Grigio)
|
|
346
|
+
let absTwa = Math.abs(curTwaDeg);
|
|
347
|
+
let tacticColor;
|
|
348
|
+
|
|
349
|
+
// Se siamo tra 75° e 105° (Traverso), il cambio è considerato neutro
|
|
350
|
+
if (absTwa > 75 && absTwa < 105) {
|
|
351
|
+
tacticColor = "#bbb"; // Grigio/Bianco sporco neutro
|
|
257
352
|
} else {
|
|
258
|
-
|
|
259
|
-
if (
|
|
353
|
+
let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
|
|
354
|
+
if (absTwa >= 90) isLift = !isLift; // Inversione logica per andature portanti
|
|
355
|
+
tacticColor = isLift ? "#27ae60" : "#c0392b";
|
|
260
356
|
}
|
|
261
|
-
|
|
262
|
-
|
|
357
|
+
if (deltaTac > 0) {
|
|
358
|
+
gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor);
|
|
359
|
+
gaugeDots.ccw.classList.remove('is-trending');
|
|
360
|
+
} else {
|
|
361
|
+
gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor);
|
|
362
|
+
gaugeDots.cw.classList.remove('is-trending');
|
|
363
|
+
}
|
|
364
|
+
} else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
365
|
+
|
|
366
|
+
// ALLARME STRAMBATA (GYBE)
|
|
367
|
+
const instTwa = radToDeg(store.raw["environment.wind.angleTrueWater"] || 0);
|
|
368
|
+
if (Math.abs(instTwa) > 155 && Math.sign(instTwa) !== Math.sign(lastInstantTwa)) {
|
|
369
|
+
if (isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
|
|
263
370
|
}
|
|
371
|
+
lastInstantTwa = instTwa;
|
|
264
372
|
}
|
|
265
373
|
|
|
266
374
|
// ==========================================================================
|
|
267
|
-
//
|
|
375
|
+
// 7. RENDERING ENGINE E AGGIORNAMENTO UI
|
|
268
376
|
// ==========================================================================
|
|
269
377
|
function refreshGraph(t) {
|
|
270
378
|
const type = (t === 'vmg') ? 'sog' : t;
|
|
271
379
|
const data = store.histories[t]; if (!data || data.length < 2) return;
|
|
272
380
|
const mode = graphModes[type], cfg = calculateScale(type, data, mode);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
381
|
+
|
|
382
|
+
// Gestione visualizzazione Hercules Zoom (sfondo rosso)
|
|
383
|
+
const box = document.querySelector(`.box-${type}`);
|
|
384
|
+
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
385
|
+
|
|
386
|
+
updateScaleLabels(type, cfg.min, cfg.max);
|
|
387
|
+
drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
276
388
|
}
|
|
277
389
|
|
|
390
|
+
/**
|
|
391
|
+
* upUI: Aggiornamento valori digitali con logica anti-ritardo (Istantaneo vs Media)
|
|
392
|
+
*/
|
|
393
|
+
const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
394
|
+
if (!obj || obj.val === null || instantRaw === undefined) {
|
|
395
|
+
el.innerHTML = "---°"; el.classList.remove('unstable-data');
|
|
396
|
+
} else {
|
|
397
|
+
let valDeg = Math.round(radToDeg(obj.val));
|
|
398
|
+
let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
399
|
+
let dev = (obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.35em; opacity: 0.5; margin-left: 4px; vertical-align: middle;">±${obj.dev}</span>` : "";
|
|
400
|
+
el.innerHTML = mainVal + dev;
|
|
401
|
+
|
|
402
|
+
let diff = Math.abs((radToDeg(instantRaw) - radToDeg(obj.val) + 540) % 360 - 180);
|
|
403
|
+
// Allarme lampeggio solo se in navigazione E (R bassa O deviazione alta O salto istantaneo brusco)
|
|
404
|
+
if (isNavigating && (!obj.stable || obj.dev > CONFIG.averages.stabilityBreakout || diff > CONFIG.averages.stabilityBreakout)) el.classList.add('unstable-data');
|
|
405
|
+
else el.classList.remove('unstable-data');
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
411
|
+
*/
|
|
412
|
+
/**
|
|
413
|
+
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
414
|
+
* Gestisce la gerarchia di aggiornamento Live (1s), Heavy (2s) e Slow (3s).
|
|
415
|
+
*/
|
|
278
416
|
function startDisplayLoop() {
|
|
279
|
-
let tick = 0;
|
|
280
417
|
renderInterval = setInterval(() => {
|
|
281
|
-
const now = Date.now();
|
|
282
|
-
|
|
283
|
-
const
|
|
418
|
+
const now = Date.now();
|
|
419
|
+
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
|
|
420
|
+
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
421
|
+
|
|
422
|
+
// Verifica stato navigazione basato su soglia impostata
|
|
284
423
|
isNavigating = stwKts > CONFIG.averages.minSpeed || sogKts > CONFIG.averages.minSpeed;
|
|
285
424
|
|
|
286
|
-
//
|
|
287
|
-
const
|
|
288
|
-
for (let p in
|
|
425
|
+
// --- TIER LIVE (1s): CONTROLLO TIMEOUT DATI ---
|
|
426
|
+
const watch = { "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog, "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog, "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth, "environment.wind.speedTrue": ui.tws };
|
|
427
|
+
for (let p in watch) {
|
|
428
|
+
if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
|
|
429
|
+
watch[p].innerText = "---"; delete store.raw[p];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
289
432
|
|
|
290
|
-
|
|
433
|
+
// --- AGGIORNAMENTO DATI ISTANTANEI ---
|
|
434
|
+
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
435
|
+
ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// --- LOGICA SOG / VMG E COLORI DINAMICI ---
|
|
291
439
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
292
|
-
const
|
|
293
|
-
manageHistory('vmg',
|
|
440
|
+
const vmg = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
441
|
+
manageHistory('vmg', vmg); manageHistory('sog', sogKts);
|
|
442
|
+
|
|
443
|
+
const labelEl = document.getElementById('sog-vmg-label');
|
|
294
444
|
if (displayModeSog === 'VMG') {
|
|
295
|
-
ui.sog.innerText =
|
|
445
|
+
ui.sog.innerText = vmg.toFixed(1);
|
|
446
|
+
ui.sog.style.setProperty('color', '#16a085', 'important'); // Verde Petrolio
|
|
447
|
+
if (labelEl) labelEl.textContent = 'VMG';
|
|
296
448
|
} else {
|
|
297
449
|
ui.sog.innerText = sogKts.toFixed(1);
|
|
298
|
-
|
|
299
|
-
|
|
450
|
+
if (labelEl) labelEl.textContent = 'SOG';
|
|
451
|
+
|
|
452
|
+
// Colore Corrente: se neutro usiamo "", così il CSS Night Mode può agire
|
|
453
|
+
if (sogKts - stwKts > 0.3) ui.sog.style.setProperty('color', '#27ae60', 'important');
|
|
454
|
+
else if (sogKts - stwKts < -0.3) ui.sog.style.setProperty('color', '#c0392b', 'important');
|
|
455
|
+
else ui.sog.style.color = "";
|
|
300
456
|
}
|
|
301
457
|
}
|
|
302
|
-
|
|
303
|
-
if (store.raw["environment.
|
|
304
|
-
|
|
458
|
+
|
|
459
|
+
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
460
|
+
ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
|
|
461
|
+
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
462
|
+
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined) {
|
|
466
|
+
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
|
|
467
|
+
ui.tws.innerText = twsKts.toFixed(1);
|
|
468
|
+
|
|
469
|
+
// Colore Reef: se normale usiamo "", il CSS metterà Nero (giorno) o Rosso (notte)
|
|
470
|
+
if (twsKts >= CONFIG.graphs.reef2) ui.tws.style.setProperty('color', '#e74c3c', 'important');
|
|
471
|
+
else if (twsKts >= CONFIG.graphs.reef1) ui.tws.style.setProperty('color', '#e67e22', 'important');
|
|
472
|
+
else ui.tws.style.color = "";
|
|
473
|
+
|
|
474
|
+
manageHistory('tws', twsKts);
|
|
475
|
+
}
|
|
305
476
|
|
|
477
|
+
if (store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
478
|
+
ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- PUNTATORI ANALOGICI (Smoothing 2s) ---
|
|
306
482
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
|
|
307
|
-
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
308
483
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
|
|
484
|
+
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
309
485
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
310
|
-
|
|
486
|
+
|
|
487
|
+
// --- CALCOLO LEEWAY E TRACK POINTER ---
|
|
311
488
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
312
489
|
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
313
|
-
|
|
490
|
+
// Azzeramento sotto soglia minima impostata
|
|
491
|
+
smoothedLeeway = (sogKts < CONFIG.averages.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
314
492
|
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
315
|
-
ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#e67e22" : "
|
|
493
|
+
ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#e67e22" : "";
|
|
316
494
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
317
495
|
}
|
|
318
|
-
|
|
496
|
+
|
|
319
497
|
updateWindTrend();
|
|
320
|
-
if (tick % 2 === 0) { refreshGraph('stw'); refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog'); refreshGraph('depth'); refreshGraph('tws'); }
|
|
321
|
-
|
|
322
|
-
// SLOW TIER (3s)
|
|
323
|
-
if (tick % 3 === 0) {
|
|
324
|
-
// Utilizziamo CONFIG.averages.longWindow (che ora è 30000ms)
|
|
325
|
-
// In questo modo, se cambi il tempo su Signal K, la dashboard si aggiorna da sola.
|
|
326
|
-
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow * 2, false)
|
|
327
|
-
cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
|
|
328
|
-
awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true),
|
|
329
|
-
twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true),
|
|
330
|
-
twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
|
|
331
|
-
|
|
332
|
-
const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
333
|
-
if (!obj || obj.val === null || instantRaw === undefined) {
|
|
334
|
-
el.innerHTML = "---°";
|
|
335
|
-
el.classList.remove('unstable-data');
|
|
336
|
-
} else {
|
|
337
|
-
let valDeg = Math.round(radToDeg(obj.val));
|
|
338
|
-
let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
339
|
-
|
|
340
|
-
// Mostriamo lo scarto medio (±)
|
|
341
|
-
let devDisplay = (obj.dev > 1 && obj.dev < 90) ?
|
|
342
|
-
`<span style="font-size: 0.35em; opacity: 0.5; margin-left: 4px; vertical-align: middle;">±${obj.dev}</span>` : "";
|
|
343
|
-
|
|
344
|
-
el.innerHTML = mainVal + devDisplay;
|
|
345
|
-
|
|
346
|
-
// --- LOGICA ALLARME ISTANTANEA (ANTI-RITARDO) ---
|
|
347
|
-
// Calcoliamo la differenza tra istantaneo e media (con gestione giro bussola)
|
|
348
|
-
let instantDeg = radToDeg(instantRaw);
|
|
349
|
-
let diff = Math.abs((instantDeg - radToDeg(obj.val) + 540) % 360 - 180);
|
|
350
|
-
|
|
351
|
-
// Lampeggia se:
|
|
352
|
-
// 1. La statistica R è bassa (obj.stable è false)
|
|
353
|
-
// 2. Lo scarto medio è alto (> 15°)
|
|
354
|
-
// 3. C'è un salto improvviso tra istantaneo e media (> 15°)
|
|
355
|
-
if (isNavigating && (!obj.stable || obj.dev > 15 || diff > 15)) {
|
|
356
|
-
el.classList.add('unstable-data');
|
|
357
|
-
} else {
|
|
358
|
-
el.classList.remove('unstable-data');
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
// Passiamo: (elemento UI, oggetto media, valore istantaneo dal sensore, è una bussola?)
|
|
363
|
-
upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
|
|
364
|
-
upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
|
|
365
|
-
upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
|
|
366
|
-
upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
|
|
367
|
-
upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
|
|
368
|
-
|
|
369
|
-
// --- CALCOLO E VALIDAZIONE TACK ---
|
|
370
|
-
if (hObj && twObj) {
|
|
371
|
-
// Calcoliamo i valori teorici
|
|
372
|
-
const tackHdgDeg = radToDeg((hObj.val + twObj.val * 2 + Math.PI * 2) % (Math.PI * 2));
|
|
373
|
-
const tackCogDeg = cObj ? radToDeg((cObj.val + twObj.val * 2 + Math.PI * 2) % (Math.PI * 2)) : null;
|
|
374
498
|
|
|
375
|
-
|
|
376
|
-
|
|
499
|
+
// TIER HEAVY (2s) - Grafici e Persistenza Browser
|
|
500
|
+
if (lastAvgUIUpdate++ % 2 === 0) {
|
|
501
|
+
['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
|
|
502
|
+
saveDashboardState();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// TIER SLOW (3s) - Medie Lunghe e Calcolo TACK
|
|
506
|
+
if (lastAvgUIUpdate % 3 === 0) {
|
|
507
|
+
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow * 2, false);
|
|
508
|
+
let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false);
|
|
509
|
+
let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true);
|
|
510
|
+
let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true);
|
|
511
|
+
let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
|
|
512
|
+
|
|
513
|
+
upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
|
|
514
|
+
upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
|
|
515
|
+
upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
|
|
516
|
+
upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
|
|
517
|
+
upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
|
|
518
|
+
|
|
519
|
+
// --- LOGICA TACK STRATEGICA (Riflessione geometrica su TWD) ---
|
|
520
|
+
if (hObj && twdObj) {
|
|
521
|
+
const tH = radToDeg((2 * twdObj.val - hObj.val + Math.PI * 2) % (Math.PI * 2));
|
|
522
|
+
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averages.stabilityBreakout || twdObj.dev > CONFIG.averages.stabilityBreakout;
|
|
523
|
+
|
|
524
|
+
if (!isNavigating) {
|
|
525
|
+
ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.remove('unstable-data');
|
|
526
|
+
} else if (unstableH) {
|
|
527
|
+
ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data');
|
|
528
|
+
} else {
|
|
529
|
+
ui.tackHdg.innerHTML = `${Math.round((tH + 360) % 360).toString().padStart(3, '0')}°`;
|
|
530
|
+
ui.tackHdg.classList.remove('unstable-data');
|
|
531
|
+
}
|
|
377
532
|
|
|
378
|
-
|
|
533
|
+
if (cObj) {
|
|
534
|
+
const tC = radToDeg((2 * twdObj.val - cObj.val + Math.PI * 2) % (Math.PI * 2));
|
|
535
|
+
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averages.stabilityBreakout || twdObj.dev > CONFIG.averages.stabilityBreakout;
|
|
536
|
+
|
|
379
537
|
if (!isNavigating) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
ui.
|
|
383
|
-
} else if (isTackUnstable) {
|
|
384
|
-
// Caso 2: Manovra in corso -> Trattini lampeggianti
|
|
385
|
-
ui.tackHdg.innerHTML = "---°";
|
|
386
|
-
ui.tackHdg.classList.add('unstable-data');
|
|
538
|
+
ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.remove('unstable-data');
|
|
539
|
+
} else if (unstableC) {
|
|
540
|
+
ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data');
|
|
387
541
|
} else {
|
|
388
|
-
|
|
389
|
-
ui.
|
|
390
|
-
ui.tackHdg.classList.remove('unstable-data');
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// --- GESTIONE INTERFACCIA TACK COG ---
|
|
394
|
-
if (cObj) {
|
|
395
|
-
const isCogTackUnstable = !cObj.stable || !twObj.stable || cObj.dev > 15 || twObj.dev > 15;
|
|
396
|
-
|
|
397
|
-
if (!isNavigating) {
|
|
398
|
-
ui.tackCog.innerHTML = "---°";
|
|
399
|
-
ui.tackCog.classList.remove('unstable-data');
|
|
400
|
-
} else if (isCogTackUnstable) {
|
|
401
|
-
ui.tackCog.innerHTML = "---°";
|
|
402
|
-
ui.tackCog.classList.add('unstable-data');
|
|
403
|
-
} else {
|
|
404
|
-
ui.tackCog.innerHTML = `${Math.round((tackCogDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
405
|
-
ui.tackCog.classList.remove('unstable-data');
|
|
406
|
-
}
|
|
542
|
+
ui.tackCog.innerHTML = `${Math.round((tC + 360) % 360).toString().padStart(3, '0')}°`;
|
|
543
|
+
ui.tackCog.classList.remove('unstable-data');
|
|
407
544
|
}
|
|
408
545
|
}
|
|
409
|
-
const smHdg = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false), smTwd = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
410
|
-
if (smHdg && smTwd) {
|
|
411
|
-
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwd.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
412
|
-
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdg.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
413
546
|
}
|
|
414
|
-
|
|
547
|
+
|
|
548
|
+
// Rotazione Mini-Bussole
|
|
549
|
+
const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
|
|
550
|
+
const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
551
|
+
if (smHdgIcons && smTwdIcons) {
|
|
552
|
+
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
553
|
+
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
554
|
+
}
|
|
415
555
|
}
|
|
416
|
-
if (tick % 60 === 0) tick = 0;
|
|
417
556
|
}, RENDER_INTERVAL_MS);
|
|
418
557
|
}
|
|
419
558
|
|
|
420
559
|
// ==========================================================================
|
|
421
|
-
//
|
|
560
|
+
// 8. CONFIGURAZIONE E GRAFICI UTILS
|
|
422
561
|
// ==========================================================================
|
|
562
|
+
/**
|
|
563
|
+
* Recupera la configurazione dal server Signal K e sovrascrive i default.
|
|
564
|
+
* Include una funzione di parsing per garantire che i valori siano numeri.
|
|
565
|
+
*/
|
|
423
566
|
async function fetchServerConfig() {
|
|
424
567
|
if (!window.location.protocol.includes("http")) return;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
568
|
+
|
|
569
|
+
// Proviamo i due percorsi standard di Signal K per i settings dei plugin
|
|
570
|
+
const urls = [
|
|
571
|
+
'/plugins/rotevista-dash/settings',
|
|
572
|
+
'/plugins/@sailingrotevista%2frotevista-dash/settings'
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
for (let url of urls) {
|
|
428
576
|
try {
|
|
429
577
|
const response = await fetch(url);
|
|
430
578
|
if (response.ok) {
|
|
431
579
|
const data = await response.json();
|
|
580
|
+
// Signal K mette i dati reali dentro l'oggetto configuration o direttamente nel body
|
|
432
581
|
const actual = data.configuration || data;
|
|
582
|
+
|
|
433
583
|
if (actual) {
|
|
434
|
-
|
|
435
|
-
|
|
584
|
+
// FUNZIONE DI PULIZIA: Trasforma le stringhe "123" in numeri 123 reali
|
|
585
|
+
const parseObj = (obj) => {
|
|
586
|
+
for (let k in obj) {
|
|
587
|
+
if (typeof obj[k] === 'object') parseObj(obj[k]);
|
|
588
|
+
else if (!isNaN(obj[k]) && obj[k] !== "") obj[k] = parseFloat(obj[k]);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
parseObj(actual);
|
|
592
|
+
|
|
593
|
+
// MAPPATURA INTELLIGENTE: Sovrascrive CONFIG solo con i dati ricevuti
|
|
436
594
|
if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
|
|
437
595
|
if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
|
|
438
596
|
if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
|
|
439
|
-
if (actual.scales) {
|
|
597
|
+
if (actual.scales) {
|
|
598
|
+
for (let k in actual.scales) {
|
|
599
|
+
CONFIG.scales[k] = { ...CONFIG.scales[k], ...actual.scales[k] };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
console.log("Configurazione caricata correttamente da:", url);
|
|
603
|
+
return; // Esci dal loop se il caricamento ha avuto successo
|
|
440
604
|
}
|
|
441
605
|
}
|
|
442
|
-
} catch (e) { }
|
|
606
|
+
} catch (e) { console.warn(`Tentativo su ${url} fallito.`); }
|
|
443
607
|
}
|
|
444
608
|
}
|
|
445
609
|
|
|
446
610
|
function manageHistory(t, v) {
|
|
447
|
-
const n = Date.now();
|
|
448
|
-
const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
611
|
+
const n = Date.now(), interval = (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
449
612
|
if (!store.graphTempBuf[t]) store.graphTempBuf[t] = [];
|
|
450
613
|
store.graphTempBuf[t].push(v);
|
|
451
|
-
|
|
452
614
|
if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
|
|
453
|
-
const
|
|
454
|
-
const avg = sum / store.graphTempBuf[t].length;
|
|
615
|
+
const avg = store.graphTempBuf[t].reduce((a, b) => a + b, 0) / store.graphTempBuf[t].length;
|
|
455
616
|
store.histories[t].push(avg);
|
|
456
617
|
if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
|
|
457
|
-
store.graphTempBuf[t] = [];
|
|
458
|
-
store.lastUpdates[t] = n;
|
|
618
|
+
store.graphTempBuf[t] = []; store.lastUpdates[t] = n;
|
|
459
619
|
}
|
|
460
620
|
}
|
|
461
621
|
|
|
462
622
|
function calculateScale(type, data, mode) {
|
|
463
|
-
const s = CONFIG.scales[type]
|
|
464
|
-
if (mode === 'hercules') {
|
|
465
|
-
|
|
623
|
+
const s = CONFIG.scales[type]; let aMin = Math.min(...data), aMax = Math.max(...data);
|
|
624
|
+
if (mode === 'hercules') {
|
|
625
|
+
let avg = (aMin + aMax) / 2; let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin));
|
|
626
|
+
if (span % 2 !== 0) span += 1; let min = Math.max(0, Math.floor(avg - (span / 2)));
|
|
627
|
+
return { min, max: min + span };
|
|
628
|
+
}
|
|
629
|
+
return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
|
|
466
630
|
}
|
|
467
631
|
|
|
468
632
|
function updateScaleLabels(t, min, max) { const el = document.getElementById(t + '-scale'); if (el) el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`; }
|
|
@@ -470,9 +634,15 @@ function updateScaleLabels(t, min, max) { const el = document.getElementById(t +
|
|
|
470
634
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
471
635
|
const svg = document.getElementById(id); if (!svg || d.length < 2) return;
|
|
472
636
|
const w = 200, h = 40, range = max - min || 1;
|
|
473
|
-
let grids = "";
|
|
474
|
-
|
|
475
|
-
|
|
637
|
+
let grids = ""; [0.25, 0.5, 0.75].forEach(p => { grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(0,0,0,0.12)" stroke-width="0.5" />`; });
|
|
638
|
+
/**
|
|
639
|
+
* Griglia Temporale Intelligente:
|
|
640
|
+
* - Storia <= 15 min: linee ogni 1 minuto.
|
|
641
|
+
* - Storia > 15 min: linee ogni 5 minuti.
|
|
642
|
+
*/
|
|
643
|
+
const gridInterval = (CONFIG.graphs.historyMinutes <= 15) ? 1 : 5;
|
|
644
|
+
|
|
645
|
+
for (let m = gridInterval; m < CONFIG.graphs.historyMinutes; m += gridInterval) {
|
|
476
646
|
const x = w - (m / CONFIG.graphs.historyMinutes) * w;
|
|
477
647
|
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
478
648
|
}
|
|
@@ -482,7 +652,7 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
482
652
|
if (isTws && i > 0) {
|
|
483
653
|
const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
|
|
484
654
|
let c = (v >= CONFIG.graphs.reef2) ? "#e74c3c" : (v >= CONFIG.graphs.reef1 ? "#e67e22" : "#000");
|
|
485
|
-
cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
|
|
655
|
+
cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="tws-reef-line ${isHercules?'line-hercules':''}" />`;
|
|
486
656
|
}
|
|
487
657
|
});
|
|
488
658
|
const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#000' };
|
|
@@ -491,101 +661,41 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
491
661
|
}
|
|
492
662
|
|
|
493
663
|
// ==========================================================================
|
|
494
|
-
//
|
|
664
|
+
// 9. INTERAZIONI E GESTI
|
|
495
665
|
// ==========================================================================
|
|
496
666
|
function toggleFocusMode(type, element) {
|
|
497
667
|
const container = document.querySelector('.main-container');
|
|
498
|
-
|
|
499
|
-
// Con la nuova struttura Flat HTML, non abbiamo più '.left-panel'.
|
|
500
|
-
// Dobbiamo determinare il lato guardando il tipo di box.
|
|
501
|
-
const leftBoxes = ['stw', 'sog', 'hdg', 'cog', 'tack'];
|
|
502
|
-
const isLeft = leftBoxes.includes(type);
|
|
503
|
-
|
|
668
|
+
const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
|
|
504
669
|
isFocusActive = !isFocusActive;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right');
|
|
508
|
-
element.classList.add('is-focused');
|
|
509
|
-
} else {
|
|
510
|
-
container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
|
|
511
|
-
document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
|
|
512
|
-
}
|
|
670
|
+
if (isFocusActive) { container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right'); element.classList.add('is-focused'); }
|
|
671
|
+
else { container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right'); document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused')); }
|
|
513
672
|
}
|
|
514
673
|
|
|
515
674
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
516
|
-
// Risale dal grafico al contenitore principale (funziona con la nuova struttura)
|
|
517
675
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
518
676
|
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
519
|
-
|
|
520
|
-
el.addEventListener('pointerdown', (e) => {
|
|
521
|
-
isLongPressActive = false;
|
|
522
|
-
pressTimer = setTimeout(() => {
|
|
523
|
-
if (!isFocusActive) {
|
|
524
|
-
isLongPressActive = true;
|
|
525
|
-
toggleFocusMode(type, el);
|
|
526
|
-
lastTapTime = 0;
|
|
527
|
-
}
|
|
528
|
-
}, 1000);
|
|
529
|
-
});
|
|
530
|
-
|
|
677
|
+
el.addEventListener('pointerdown', (e) => { isLongPressActive = false; pressTimer = setTimeout(() => { if (!isFocusActive) { isLongPressActive = true; toggleFocusMode(type, el); lastTapTime = 0; } }, 1000); });
|
|
531
678
|
el.addEventListener('pointerup', (e) => {
|
|
532
|
-
clearTimeout(pressTimer);
|
|
533
|
-
if (isLongPressActive) return; // Se era focus, ignora l'up
|
|
534
|
-
|
|
679
|
+
clearTimeout(pressTimer); if (isLongPressActive) return;
|
|
535
680
|
const currentTime = new Date().getTime(), tapDelay = currentTime - lastTapTime;
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (tapDelay < 300 && tapDelay > 0) {
|
|
539
|
-
clearTimeout(tapTimeout); // Cancella l'uscita dal Focus
|
|
540
|
-
graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
|
|
541
|
-
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
542
|
-
refreshGraph(type); // Mostra istantaneamente
|
|
543
|
-
lastTapTime = 0;
|
|
544
|
-
}
|
|
545
|
-
// TAP SINGOLO (con ritardo di 250ms per aspettare l'eventuale doppio tap)
|
|
546
|
-
else {
|
|
547
|
-
lastTapTime = currentTime;
|
|
548
|
-
tapTimeout = setTimeout(() => {
|
|
549
|
-
// Uscita Focus
|
|
550
|
-
if (isFocusActive && el.classList.contains('is-focused')) {
|
|
551
|
-
toggleFocusMode(type, el);
|
|
552
|
-
}
|
|
553
|
-
// Toggle SOG/VMG (solo se non in focus)
|
|
554
|
-
else if (!isFocusActive && type === 'sog') {
|
|
555
|
-
displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
|
|
556
|
-
el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
|
557
|
-
setTimeout(() => el.style.backgroundColor = "", 150);
|
|
558
|
-
}
|
|
559
|
-
}, 250); // Attesa critica per il doppio tap
|
|
560
|
-
}
|
|
681
|
+
if (tapDelay < 300 && tapDelay > 0) { clearTimeout(tapTimeout); graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); refreshGraph(type); lastTapTime = 0; }
|
|
682
|
+
else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(0, 0, 0, 0.05)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
|
|
561
683
|
});
|
|
562
|
-
|
|
563
684
|
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
564
685
|
});
|
|
565
686
|
|
|
566
687
|
if (ui.hotspot) {
|
|
567
688
|
ui.hotspot.addEventListener('pointerdown', (e) => { pressTimer = setTimeout(() => { document.body.classList.toggle('night-mode'); ui.hotspot.style.opacity = "0.5"; setTimeout(() => ui.hotspot.style.opacity = "1", 200); pressTimer = null; }, 1000); });
|
|
568
|
-
ui.hotspot.addEventListener('pointerup', (e) => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; const doc = document.documentElement
|
|
689
|
+
ui.hotspot.addEventListener('pointerup', (e) => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; const doc = document.documentElement; if (document.fullscreenElement) document.exitFullscreen(); else doc.requestFullscreen(); } });
|
|
569
690
|
ui.hotspot.addEventListener('pointerleave', () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } });
|
|
570
691
|
}
|
|
571
692
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws';
|
|
576
|
-
try {
|
|
577
|
-
socket = new WebSocket(`${protocol}://${addr}/signalk/v1/stream?subscribe=self`);
|
|
578
|
-
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; reconnectDelay = 1000; };
|
|
579
|
-
socket.onmessage = (e) => { if (simulationMode) return; const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
|
|
580
|
-
socket.onclose = () => { if (!simulationMode) { ui.status.className = "offline"; ui.status.innerText = "RECONNECTING..."; setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); } };
|
|
581
|
-
} catch (e) { setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); }
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
ui.depth.closest('.data-box').addEventListener('click', (function() { let dC = 0, lC = 0; return function() { const n = Date.now(); if (n - lC < 500) dC++; else dC = 1; lC = n; if (dC === 3) { simulationMode = !simulationMode; if (simulationMode) { if (socket) socket.close(); startDynamicSimulation(); } else location.reload(); dC = 0; } }; })());
|
|
585
|
-
|
|
693
|
+
// ==========================================================================
|
|
694
|
+
// 10. SIMULAZIONE E RETE
|
|
695
|
+
// ==========================================================================
|
|
586
696
|
function startDynamicSimulation() {
|
|
587
697
|
ui.status.innerText = "SIM ATTIVO";
|
|
588
|
-
let sim = { hdg: 45, tws: 12, twd: 90, depth: 12, stw: 5,
|
|
698
|
+
let sim = { hdg: 45, tws: 12, twd: 90, depth: 12, stw: 5, startTime: Date.now() };
|
|
589
699
|
simInterval = setInterval(() => {
|
|
590
700
|
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
591
701
|
sim.twd = (sim.twd + (Math.sin(elapsed / 20) * 0.5) + 360) % 360;
|
|
@@ -593,21 +703,50 @@ function startDynamicSimulation() {
|
|
|
593
703
|
sim.tws += (targetTws - sim.tws) * 0.05; sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
|
|
594
704
|
let twaRel = (sim.twd - sim.hdg + 360) % 360; if (twaRel > 180) twaRel -= 360;
|
|
595
705
|
let targetStw = 5; sim.stw += (targetStw - sim.stw) * 0.05;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
processIncomingData("
|
|
601
|
-
processIncomingData("
|
|
602
|
-
processIncomingData("navigation.
|
|
603
|
-
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
|
|
706
|
+
processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
707
|
+
processIncomingData("environment.wind.speedApparent", ktsToMs(sim.stw + 2));
|
|
708
|
+
processIncomingData("environment.wind.angleApparent", degToRad(twaRel + 5));
|
|
709
|
+
processIncomingData("environment.depth.belowTransducer", sim.depth);
|
|
710
|
+
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw));
|
|
711
|
+
processIncomingData("navigation.speedOverGround", ktsToMs(sim.stw));
|
|
712
|
+
processIncomingData("navigation.courseOverGroundTrue", degToRad(sim.hdg));
|
|
604
713
|
}, 1000);
|
|
605
714
|
}
|
|
606
715
|
|
|
716
|
+
function connect() {
|
|
717
|
+
if (simulationMode) return;
|
|
718
|
+
let addr = window.location.host || CONFIG.server.fallbackIp;
|
|
719
|
+
try {
|
|
720
|
+
socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
|
|
721
|
+
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; reconnectDelay = 1000; };
|
|
722
|
+
socket.onmessage = (e) => { const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
|
|
723
|
+
socket.onclose = () => { if (!simulationMode) { ui.status.className = "offline"; ui.status.innerText = "RECONNECTING..."; setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); } };
|
|
724
|
+
} catch (e) { setTimeout(connect, reconnectDelay); }
|
|
725
|
+
}
|
|
726
|
+
|
|
607
727
|
// ==========================================================================
|
|
608
|
-
//
|
|
728
|
+
// 11. INIT E CICLO DI VITA
|
|
609
729
|
// ==========================================================================
|
|
610
730
|
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
611
|
-
(function genTicks() {
|
|
612
|
-
|
|
731
|
+
(function genTicks() {
|
|
732
|
+
const c = document.getElementById('ticks');
|
|
733
|
+
if (c) {
|
|
734
|
+
for (let i = 0; i < 360; i += 10) {
|
|
735
|
+
const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
736
|
+
const m = i % 30 === 0;
|
|
737
|
+
l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50));
|
|
738
|
+
l.setAttribute("stroke", m ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1");
|
|
739
|
+
l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
})();
|
|
743
|
+
|
|
744
|
+
async function init() {
|
|
745
|
+
loadDashboardState();
|
|
746
|
+
await fetchServerConfig();
|
|
747
|
+
startDisplayLoop();
|
|
748
|
+
connect();
|
|
749
|
+
}
|
|
750
|
+
|
|
613
751
|
window.addEventListener('load', init);
|
|
752
|
+
window.addEventListener('pagehide', saveDashboardState);
|