@sailingrotevista/rotevista-dash 4.0.21 → 5.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 +344 -311
- package/index.html +4 -1
- package/index.js +3 -3
- package/package.json +2 -2
package/app.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ==========================================================================
|
|
3
|
-
* Signal K Wind Dashboard - Pro Version
|
|
3
|
+
* Signal K Wind Dashboard - Pro Version 3.5 (Time-Based Architecture)
|
|
4
4
|
* ==========================================================================
|
|
5
5
|
* Autore: Sailing Rotevista
|
|
6
6
|
* Motore di calcolo tattico per navigazione e crociera.
|
|
7
|
-
* Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico
|
|
8
|
-
* Memoria UI persistente, Modalità Hercules
|
|
7
|
+
* Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
|
|
8
|
+
* Memoria UI persistente, Modalità Hercules, Focus Split Screen e
|
|
9
|
+
* Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
// ==========================================================================
|
|
@@ -17,14 +18,14 @@ let CONFIG = {
|
|
|
17
18
|
smoothWindow: 2000,
|
|
18
19
|
longWindow: 30000,
|
|
19
20
|
stabilityTolerance: 2000,
|
|
20
|
-
stabilityThreshold: 0.
|
|
21
|
-
minSpeed: 0,
|
|
21
|
+
stabilityThreshold: 0.99,
|
|
22
|
+
minSpeed: 0.5,
|
|
22
23
|
stabilityBreakout: 15
|
|
23
24
|
},
|
|
24
|
-
graphs: { reef1:
|
|
25
|
+
graphs: { reef1: 10, reef2: 15, historyMinutes: 10, samples: 60 },
|
|
25
26
|
scales: {
|
|
26
|
-
stw: { stdMax:
|
|
27
|
-
sog: { stdMax:
|
|
27
|
+
stw: { stdMax: 4, hercSpan: 4, step: 2 },
|
|
28
|
+
sog: { stdMax: 4, hercSpan: 4, step: 2 },
|
|
28
29
|
tws: { stdMax: 15, hercSpan: 10, step: 5 },
|
|
29
30
|
depth: { stdMax: 8, hercSpan: 5, step: 5 }
|
|
30
31
|
},
|
|
@@ -34,15 +35,15 @@ let CONFIG = {
|
|
|
34
35
|
const RENDER_INTERVAL_MS = 1000;
|
|
35
36
|
const TIMEOUT_MS = 5000;
|
|
36
37
|
const SIM_SAMPLE_INTERVAL = 1000;
|
|
37
|
-
const DASH_VERSION = "
|
|
38
|
+
const DASH_VERSION = "3.5"; // Major Update: Time-Based storage and GAP handling
|
|
38
39
|
const sourceLocks = {};
|
|
39
40
|
|
|
40
|
-
|
|
41
41
|
// ==========================================================================
|
|
42
42
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
43
43
|
// ==========================================================================
|
|
44
44
|
let simulationMode = false;
|
|
45
|
-
let displayModeSog = 'SOG';
|
|
45
|
+
let displayModeSog = 'SOG';
|
|
46
|
+
let displayModeTws = 'TWS';
|
|
46
47
|
let socket, renderInterval, simInterval;
|
|
47
48
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
48
49
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
@@ -59,7 +60,6 @@ let emaTwaSin = 0;
|
|
|
59
60
|
let emaTwaCos = 0;
|
|
60
61
|
let firstEmaRun = true;
|
|
61
62
|
|
|
62
|
-
// Stato dei singoli grafici (Standard vs Hercules Zoom)
|
|
63
63
|
const graphModes = {
|
|
64
64
|
stw: 'standard',
|
|
65
65
|
sog: 'standard',
|
|
@@ -73,10 +73,10 @@ const store = {
|
|
|
73
73
|
timestamps: {},
|
|
74
74
|
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
75
75
|
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
76
|
-
histories
|
|
77
|
-
|
|
78
|
-
graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
|
|
79
|
-
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0 }
|
|
76
|
+
// histories ora conterrà array di oggetti: { time: Date.now(), val: numero }
|
|
77
|
+
histories: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
|
|
78
|
+
graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
|
|
79
|
+
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 }
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
// Riferimenti agli elementi DOM mappati all'avvio
|
|
@@ -96,16 +96,13 @@ const ui = {
|
|
|
96
96
|
};
|
|
97
97
|
|
|
98
98
|
// ==========================================================================
|
|
99
|
-
// 3. UTILITIES (MATEMATICA, BUFFER
|
|
99
|
+
// 3. UTILITIES (MATEMATICA, BUFFER E MEMORIA)
|
|
100
100
|
// ==========================================================================
|
|
101
101
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
102
102
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
103
103
|
function msToKts(ms) { return ms * 1.94384; }
|
|
104
104
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
* Calcola il percorso più breve per la rotazione di un puntatore (evita giri di 360°)
|
|
108
|
-
*/
|
|
109
106
|
function getShortestRotation(curr, target) {
|
|
110
107
|
let diff = (target - curr) % 360;
|
|
111
108
|
if (diff > 180) diff -= 360;
|
|
@@ -113,15 +110,10 @@ function getShortestRotation(curr, target) {
|
|
|
113
110
|
return curr + diff;
|
|
114
111
|
}
|
|
115
112
|
|
|
116
|
-
/**
|
|
117
|
-
* Inserisce un dato nel buffer circolare limitandolo a 2000 campioni (30 min)
|
|
118
|
-
*/
|
|
119
113
|
/**
|
|
120
114
|
* Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
|
|
121
115
|
*/
|
|
122
116
|
function safePush(buffer, val, time) {
|
|
123
|
-
// --- PROTEZIONE ANTI-NaN (FONDAMENTALE ALL'AVVIO) ---
|
|
124
|
-
// Se il valore è nullo, non definito o non è un numero, ignoriamo l'inserimento
|
|
125
117
|
if (val === null || val === undefined || isNaN(val)) return;
|
|
126
118
|
|
|
127
119
|
buffer.push({
|
|
@@ -131,21 +123,15 @@ function safePush(buffer, val, time) {
|
|
|
131
123
|
cos: Math.cos(val)
|
|
132
124
|
});
|
|
133
125
|
|
|
134
|
-
// Teniamo sempre in memoria il DOPPIO della storia impostata (per la modalità ancoraggio)
|
|
135
|
-
// + 1 minuto di margine
|
|
136
126
|
const maxHistoryMs = (CONFIG.graphs.historyMinutes * 60000 * 2) + 60000;
|
|
137
|
-
|
|
138
127
|
while (buffer.length > 0 && (time - buffer[0].time) > maxHistoryMs) {
|
|
139
128
|
buffer.shift();
|
|
140
129
|
}
|
|
141
|
-
|
|
142
|
-
// Tetto massimo di campioni per sicurezza (circa 2 ore a 5Hz)
|
|
143
130
|
if (buffer.length > 36000) buffer.shift();
|
|
144
131
|
}
|
|
145
132
|
|
|
146
133
|
/**
|
|
147
134
|
* Media Circolare Vettoriale - Versione "Soft Outlier Rejection"
|
|
148
|
-
* Riduce l'impatto degli sbalzi limitando il loro angolo massimo di discostamento.
|
|
149
135
|
*/
|
|
150
136
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
|
|
151
137
|
now = now || Date.now();
|
|
@@ -155,7 +141,6 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
|
|
|
155
141
|
let sSin = 0, sCos = 0, count = 0;
|
|
156
142
|
let newestTime = 0, oldestTime = 0;
|
|
157
143
|
|
|
158
|
-
// 1. MEDIA PILOTA: Guardiamo l'ultima frazione di dati (es. max 15 campioni) per sapere dove punta "ora"
|
|
159
144
|
let pilotSin = 0, pilotCos = 0;
|
|
160
145
|
const pilotSamples = Math.min(len, 15);
|
|
161
146
|
for (let i = len - 1; i >= len - pilotSamples; i--) {
|
|
@@ -163,34 +148,21 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
|
|
|
163
148
|
pilotCos += bufferArray[i].cos;
|
|
164
149
|
}
|
|
165
150
|
const pilotRad = Math.atan2(pilotSin, pilotCos);
|
|
166
|
-
|
|
167
|
-
// Il limite elastico in radianti (basato sul tuo stabilityBreakout in gradi)
|
|
168
151
|
const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
|
|
169
152
|
|
|
170
|
-
// 2. CALCOLO AMMORTIZZATO
|
|
171
153
|
for (let i = len - 1; i >= 0; i--) {
|
|
172
154
|
const item = bufferArray[i];
|
|
173
155
|
if ((now - item.time) > windowMs) break;
|
|
174
156
|
|
|
175
|
-
|
|
176
|
-
let diffRad = Math.atan2(
|
|
177
|
-
Math.sin(item.val - pilotRad),
|
|
178
|
-
Math.cos(item.val - pilotRad)
|
|
179
|
-
);
|
|
180
|
-
|
|
157
|
+
let diffRad = Math.atan2(Math.sin(item.val - pilotRad), Math.cos(item.val - pilotRad));
|
|
181
158
|
let finalSin, finalCos;
|
|
182
159
|
|
|
183
|
-
// Se lo scarto è maggiore del limite, "Pattiniamo" (Ammortizzazione)
|
|
184
160
|
if (Math.abs(diffRad) > limitRad) {
|
|
185
|
-
// Tronchiamo la differenza al limite massimo consentito (mantenendo il segno)
|
|
186
161
|
const clampedDiff = Math.sign(diffRad) * limitRad;
|
|
187
|
-
// Ricalcoliamo l'angolo ammortizzato
|
|
188
162
|
const clampedRad = pilotRad + clampedDiff;
|
|
189
|
-
|
|
190
163
|
finalSin = Math.sin(clampedRad);
|
|
191
164
|
finalCos = Math.cos(clampedRad);
|
|
192
165
|
} else {
|
|
193
|
-
// Il dato è buono, usiamo i valori precalcolati
|
|
194
166
|
finalSin = item.sin;
|
|
195
167
|
finalCos = item.cos;
|
|
196
168
|
}
|
|
@@ -219,9 +191,6 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
|
|
|
219
191
|
};
|
|
220
192
|
}
|
|
221
193
|
|
|
222
|
-
/**
|
|
223
|
-
* Salva lo stato attuale della dashboard (dati e preferenze UI) nel browser
|
|
224
|
-
*/
|
|
225
194
|
function saveDashboardState() {
|
|
226
195
|
try {
|
|
227
196
|
const focusedBox = document.querySelector('.data-box.is-focused');
|
|
@@ -236,6 +205,7 @@ function saveDashboardState() {
|
|
|
236
205
|
histories: store.histories,
|
|
237
206
|
longBuf: store.longBuf,
|
|
238
207
|
displayModeSog: displayModeSog,
|
|
208
|
+
displayModeTws: displayModeTws,
|
|
239
209
|
graphModes: graphModes,
|
|
240
210
|
isNightMode: document.body.classList.contains('night-mode'),
|
|
241
211
|
isFocusActive: isFocusActive,
|
|
@@ -246,9 +216,6 @@ function saveDashboardState() {
|
|
|
246
216
|
} catch (e) { console.error("Save error:", e); }
|
|
247
217
|
}
|
|
248
218
|
|
|
249
|
-
/**
|
|
250
|
-
* Carica e ripristina lo stato salvato (entro i 20 minuti di vecchiaia)
|
|
251
|
-
*/
|
|
252
219
|
function loadDashboardState() {
|
|
253
220
|
const saved = localStorage.getItem('rotevista_dash_state');
|
|
254
221
|
if (!saved) return;
|
|
@@ -261,24 +228,24 @@ function loadDashboardState() {
|
|
|
261
228
|
if (state.longBuf) Object.assign(store.longBuf, state.longBuf);
|
|
262
229
|
if (state.graphModes) Object.assign(graphModes, state.graphModes);
|
|
263
230
|
|
|
264
|
-
// Ripristino SOG/VMG
|
|
265
231
|
if (state.displayModeSog) {
|
|
266
232
|
displayModeSog = state.displayModeSog;
|
|
267
233
|
const labelEl = document.getElementById('sog-vmg-label');
|
|
268
234
|
if (labelEl) labelEl.textContent = displayModeSog;
|
|
269
235
|
}
|
|
270
|
-
|
|
271
|
-
|
|
236
|
+
if (state.displayModeTws) {
|
|
237
|
+
displayModeTws = state.displayModeTws;
|
|
238
|
+
const labelEl = document.getElementById('tws-aws-label');
|
|
239
|
+
if (labelEl) labelEl.textContent = displayModeTws;
|
|
240
|
+
}
|
|
272
241
|
if (state.isNightMode) document.body.classList.add('night-mode');
|
|
273
242
|
|
|
274
|
-
// Ripristino Focus (Dual Screen)
|
|
275
243
|
if (state.isFocusActive && state.focusedBoxType) {
|
|
276
244
|
setTimeout(() => {
|
|
277
245
|
const el = document.querySelector(`.box-${state.focusedBoxType}`);
|
|
278
246
|
if (el) { isFocusActive = false; toggleFocusMode(state.focusedBoxType, el); }
|
|
279
247
|
}, 200);
|
|
280
248
|
}
|
|
281
|
-
console.log("Stato ripristinato con successo dalla cache.");
|
|
282
249
|
}
|
|
283
250
|
} catch (e) { localStorage.removeItem('rotevista_dash_state'); }
|
|
284
251
|
}
|
|
@@ -314,13 +281,10 @@ function playGybeAlarm() {
|
|
|
314
281
|
}
|
|
315
282
|
|
|
316
283
|
function checkDepthAlarm(m) {
|
|
317
|
-
// Rimuoviamo sempre le classi prima di riapplicarle
|
|
318
284
|
ui.depth.classList.remove('alarm-warning', 'alarm-danger', 'blink-alarm');
|
|
319
|
-
|
|
320
|
-
// Logica di confronto dinamica
|
|
321
285
|
if (m < CONFIG.alarms.depthDanger) {
|
|
322
286
|
ui.depth.classList.add('alarm-danger', 'blink-alarm');
|
|
323
|
-
playBingBing();
|
|
287
|
+
playBingBing();
|
|
324
288
|
} else if (m < CONFIG.alarms.depthWarning) {
|
|
325
289
|
ui.depth.classList.add('alarm-warning');
|
|
326
290
|
}
|
|
@@ -341,11 +305,9 @@ function computeTrueWind() {
|
|
|
341
305
|
const hdg = store.raw["navigation.headingTrue"] || 0, cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
342
306
|
if (aws === undefined || awa === undefined) return;
|
|
343
307
|
|
|
344
|
-
// Vento Reale Rispetto all'acqua (TWA/TWS Water)
|
|
345
308
|
const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
|
|
346
309
|
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
347
310
|
|
|
348
|
-
// Vento Reale Rispetto al fondo (TWD Ground)
|
|
349
311
|
const drift = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
|
|
350
312
|
const tw_ground_x = aws * Math.cos(awa) - sog * Math.cos(drift), tw_ground_y = aws * Math.sin(awa) - sog * Math.sin(drift);
|
|
351
313
|
const tws_ground = Math.sqrt(tw_ground_x * tw_ground_x + tw_ground_y * tw_ground_y);
|
|
@@ -367,49 +329,37 @@ function computeTrueWind() {
|
|
|
367
329
|
function processIncomingData(path, val, source) {
|
|
368
330
|
const now = Date.now();
|
|
369
331
|
|
|
370
|
-
//
|
|
332
|
+
// FILTRO SORGENTE STICKY
|
|
371
333
|
if (!sourceLocks[path] || sourceLocks[path].label === source || (now - sourceLocks[path].lastSeen > 2000)) {
|
|
372
334
|
sourceLocks[path] = { label: source, lastSeen: now };
|
|
373
335
|
} else {
|
|
374
|
-
// Se arriva un dato da un'altra sorgente mentre il lock è attivo, lo scartiamo
|
|
375
336
|
return;
|
|
376
337
|
}
|
|
377
338
|
|
|
378
|
-
// --- 2. AGGIORNAMENTO DATI (Solo per la sorgente eletta) ---
|
|
379
339
|
store.timestamps[path] = now;
|
|
380
340
|
store.raw[path] = val;
|
|
381
341
|
|
|
382
|
-
// Buffer per AWA (Vento Apparente)
|
|
383
342
|
if (path === "environment.wind.angleApparent") {
|
|
384
343
|
safePush(store.smoothBuf.awa, val, now);
|
|
385
344
|
safePush(store.longBuf.awa, val, now);
|
|
386
345
|
}
|
|
387
|
-
|
|
388
|
-
// Buffer per HDG (Prua)
|
|
389
346
|
if (path === "navigation.headingTrue") {
|
|
390
347
|
safePush(store.smoothBuf.hdg, val, now);
|
|
391
348
|
safePush(store.longBuf.hdg, val, now);
|
|
392
349
|
}
|
|
393
|
-
|
|
394
|
-
// Buffer per COG (Rotta Fondo)
|
|
395
350
|
if (path === "navigation.courseOverGroundTrue") {
|
|
396
351
|
safePush(store.smoothBuf.cog, val, now);
|
|
397
352
|
safePush(store.longBuf.cog, val, now);
|
|
398
353
|
}
|
|
399
354
|
|
|
400
|
-
// --- 3. TRIGGER CALCOLO VENTO REALE (TWA/TWS/TWD) ---
|
|
401
355
|
const twPaths = [
|
|
402
|
-
"environment.wind.speedApparent",
|
|
403
|
-
"
|
|
404
|
-
"navigation.
|
|
405
|
-
"navigation.speedOverGround",
|
|
406
|
-
"navigation.headingTrue",
|
|
407
|
-
"navigation.courseOverGroundTrue"
|
|
356
|
+
"environment.wind.speedApparent", "environment.wind.angleApparent",
|
|
357
|
+
"navigation.speedThroughWater", "navigation.speedOverGround",
|
|
358
|
+
"navigation.headingTrue", "navigation.courseOverGroundTrue"
|
|
408
359
|
];
|
|
409
360
|
|
|
410
361
|
if (twPaths.includes(path)) {
|
|
411
362
|
twDirty = true;
|
|
412
|
-
// Calcolo limitato a 10Hz per non pesare sulla CPU
|
|
413
363
|
if (twDirty && (now - lastTWCompute > 100)) {
|
|
414
364
|
computeTrueWind();
|
|
415
365
|
lastTWCompute = now;
|
|
@@ -423,20 +373,20 @@ function processIncomingData(path, val, source) {
|
|
|
423
373
|
// ==========================================================================
|
|
424
374
|
function updateWindTrend() {
|
|
425
375
|
const now = Date.now();
|
|
426
|
-
// TATTICA (Lancetta): 2s vs 10s (reazione rapida per trim)
|
|
427
376
|
const twaNow = getCircularAverageFromBuffer(store.longBuf.twa, 2000, true);
|
|
428
377
|
const twaRef = getCircularAverageFromBuffer(store.longBuf.twa, 10000, true);
|
|
429
378
|
|
|
430
|
-
// STRATEGIA (Bussola TWD): 1 min vs 30 minuti (tendenza meteo profonda)
|
|
431
379
|
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
432
380
|
const multiplier = isNavigating ? 1 : 2;
|
|
433
381
|
const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
|
|
434
382
|
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
383
|
+
|
|
435
384
|
if (!twaNow || !twaRef || !twdNow || !twdRef) return;
|
|
385
|
+
|
|
436
386
|
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
437
387
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
438
388
|
|
|
439
|
-
// TREND METEO
|
|
389
|
+
// TREND METEO
|
|
440
390
|
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
441
391
|
if (Math.abs(deltaMeteo) > 6.0) {
|
|
442
392
|
const isSouth = store.raw["navigation.position"]?.latitude < 0;
|
|
@@ -450,20 +400,17 @@ function updateWindTrend() {
|
|
|
450
400
|
}
|
|
451
401
|
} else { [compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
452
402
|
|
|
453
|
-
// TREND TATTICO
|
|
403
|
+
// TREND TATTICO
|
|
454
404
|
let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
455
405
|
const curTwaDeg = radToDeg(twaNow.val);
|
|
456
406
|
if (Math.abs(deltaTac) > 3.0) {
|
|
457
|
-
// Calcolo logica tattica: LIFT (Verde), HEADER (Rosso) o NEUTRO (Grigio)
|
|
458
407
|
let absTwa = Math.abs(curTwaDeg);
|
|
459
408
|
let tacticColor;
|
|
460
|
-
|
|
461
|
-
// Se siamo tra 75° e 105° (Traverso), il cambio è considerato neutro
|
|
462
409
|
if (absTwa > 75 && absTwa < 105) {
|
|
463
|
-
tacticColor = "#bbb";
|
|
410
|
+
tacticColor = "#bbb";
|
|
464
411
|
} else {
|
|
465
412
|
let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
|
|
466
|
-
if (absTwa >= 90) isLift = !isLift;
|
|
413
|
+
if (absTwa >= 90) isLift = !isLift;
|
|
467
414
|
tacticColor = isLift ? "#27ae60" : "#c0392b";
|
|
468
415
|
}
|
|
469
416
|
if (deltaTac > 0) {
|
|
@@ -475,65 +422,45 @@ function updateWindTrend() {
|
|
|
475
422
|
}
|
|
476
423
|
} else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
477
424
|
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
425
|
+
// ALLARME STRAMBATA (EMA FILTERED)
|
|
426
|
+
const instTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
427
|
+
if (instTwaRad !== undefined) {
|
|
428
|
+
const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
|
|
429
|
+
const currentSin = Math.sin(instTwaRad);
|
|
430
|
+
const currentCos = Math.cos(instTwaRad);
|
|
484
431
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
firstEmaRun = false;
|
|
492
|
-
} else {
|
|
493
|
-
// Media Esponenziale Vettoriale
|
|
494
|
-
emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
|
|
495
|
-
emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
|
|
496
|
-
}
|
|
432
|
+
if (firstEmaRun) {
|
|
433
|
+
emaTwaSin = currentSin; emaTwaCos = currentCos; firstEmaRun = false;
|
|
434
|
+
} else {
|
|
435
|
+
emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
|
|
436
|
+
emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
|
|
437
|
+
}
|
|
497
438
|
|
|
498
|
-
|
|
499
|
-
const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
|
|
439
|
+
const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
|
|
500
440
|
|
|
501
|
-
|
|
502
|
-
if (Math.
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
|
|
508
|
-
}
|
|
441
|
+
if (Math.abs(smoothedTwaDeg) > 155) {
|
|
442
|
+
if (Math.sign(smoothedTwaDeg) !== Math.sign(lastInstantTwa) && lastInstantTwa !== null) {
|
|
443
|
+
if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
|
|
444
|
+
lastGybeAlarmTime = now;
|
|
445
|
+
playGybeAlarm();
|
|
446
|
+
console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
|
|
509
447
|
}
|
|
510
448
|
}
|
|
511
|
-
lastInstantTwa = smoothedTwaDeg;
|
|
512
449
|
}
|
|
450
|
+
lastInstantTwa = smoothedTwaDeg;
|
|
451
|
+
}
|
|
513
452
|
}
|
|
514
453
|
|
|
515
454
|
// ==========================================================================
|
|
516
455
|
// 7. RENDERING ENGINE E AGGIORNAMENTO UI
|
|
517
456
|
// ==========================================================================
|
|
518
|
-
function refreshGraph(t) {
|
|
519
|
-
const type = (t === 'vmg') ? 'sog' : t;
|
|
520
|
-
const data = store.histories[t]; if (!data || data.length < 2) return;
|
|
521
|
-
const mode = graphModes[type], cfg = calculateScale(type, data, mode);
|
|
522
|
-
|
|
523
|
-
// Gestione visualizzazione Hercules Zoom (sfondo rosso)
|
|
524
|
-
const box = document.querySelector(`.box-${type}`);
|
|
525
|
-
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
526
|
-
|
|
527
|
-
updateScaleLabels(type, cfg.min, cfg.max);
|
|
528
|
-
drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
529
|
-
}
|
|
530
457
|
|
|
531
458
|
/**
|
|
532
|
-
* upUI: Aggiornamento valori digitali
|
|
459
|
+
* upUI: Aggiornamento valori digitali
|
|
533
460
|
*/
|
|
534
461
|
const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
535
462
|
if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
|
|
536
|
-
el.innerHTML = "---°";
|
|
463
|
+
el.innerHTML = "---°";
|
|
537
464
|
el.classList.remove('unstable-data');
|
|
538
465
|
} else {
|
|
539
466
|
let valDeg = Math.round(radToDeg(obj.val));
|
|
@@ -542,7 +469,6 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
542
469
|
el.innerHTML = mainVal + dev;
|
|
543
470
|
|
|
544
471
|
let diff = Math.abs((radToDeg(instantRaw) - radToDeg(obj.val) + 540) % 360 - 180);
|
|
545
|
-
// Allarme lampeggio solo se in navigazione E (R bassa O deviazione alta O salto istantaneo brusco)
|
|
546
472
|
if (isNavigating && (!obj.stable || obj.dev > CONFIG.averaging.stabilityBreakout || diff > CONFIG.averaging.stabilityBreakout)) el.classList.add('unstable-data');
|
|
547
473
|
else el.classList.remove('unstable-data');
|
|
548
474
|
}
|
|
@@ -551,102 +477,143 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
551
477
|
/**
|
|
552
478
|
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
553
479
|
*/
|
|
554
|
-
/**
|
|
555
|
-
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
556
|
-
* Gestisce la gerarchia di aggiornamento Live (1s), Heavy (2s) e Slow (3s).
|
|
557
|
-
*/
|
|
558
480
|
function startDisplayLoop() {
|
|
559
481
|
renderInterval = setInterval(() => {
|
|
560
482
|
const now = Date.now();
|
|
483
|
+
const isNight = document.body.classList.contains('night-mode');
|
|
484
|
+
|
|
561
485
|
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
|
|
562
486
|
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
563
487
|
|
|
564
|
-
// Verifica stato navigazione basato su soglia impostata
|
|
565
488
|
isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
|
|
566
489
|
|
|
567
|
-
// ---
|
|
568
|
-
const
|
|
490
|
+
// --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
|
|
491
|
+
const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
492
|
+
const requiredMs = viewportMinutes * 60000;
|
|
493
|
+
const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
|
|
494
|
+
|
|
495
|
+
if (oldestStw) {
|
|
496
|
+
const availableMs = now - oldestStw.time;
|
|
497
|
+
if (availableMs >= requiredMs) {
|
|
498
|
+
ui.status.innerText = `ONLINE ${viewportMinutes}min`;
|
|
499
|
+
} else {
|
|
500
|
+
const availableMin = Math.max(1, Math.floor(availableMs / 60000));
|
|
501
|
+
ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
ui.status.innerText = `ONLINE`;
|
|
505
|
+
}
|
|
506
|
+
ui.status.className = (socket && socket.readyState === WebSocket.OPEN) ? "online" : "offline";
|
|
507
|
+
|
|
508
|
+
// --- WATCHDOG: CONTROLLO TIMEOUT ---
|
|
509
|
+
const watch = {
|
|
510
|
+
"navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
|
|
511
|
+
"navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
|
|
512
|
+
"environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
|
|
513
|
+
"environment.wind.speedTrue": ui.tws
|
|
514
|
+
};
|
|
569
515
|
for (let p in watch) {
|
|
570
516
|
if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
|
|
571
517
|
watch[p].innerText = "---"; delete store.raw[p];
|
|
572
518
|
}
|
|
573
519
|
}
|
|
574
520
|
|
|
575
|
-
// --- AGGIORNAMENTO
|
|
521
|
+
// --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
|
|
576
522
|
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
577
|
-
ui.stw.innerText = stwKts.toFixed(1);
|
|
523
|
+
ui.stw.innerText = stwKts.toFixed(1);
|
|
524
|
+
ui.stw.style.color = ""; // Neutro
|
|
525
|
+
manageHistory('stw', stwKts);
|
|
578
526
|
}
|
|
579
527
|
|
|
580
|
-
// --- LOGICA SOG / VMG
|
|
528
|
+
// --- LOGICA SOG / VMG ---
|
|
581
529
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
582
|
-
const
|
|
583
|
-
manageHistory('vmg',
|
|
530
|
+
const vmgVal = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
531
|
+
manageHistory('vmg', vmgVal);
|
|
532
|
+
manageHistory('sog', sogKts);
|
|
584
533
|
|
|
585
|
-
const
|
|
534
|
+
const labelSogVmg = document.getElementById('sog-vmg-label');
|
|
586
535
|
if (displayModeSog === 'VMG') {
|
|
587
|
-
ui.sog.innerText =
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (labelEl) labelEl.textContent = 'VMG';
|
|
536
|
+
ui.sog.innerText = vmgVal.toFixed(1);
|
|
537
|
+
ui.sog.style.setProperty('color', '#00b8d4', 'important'); // Cyan
|
|
538
|
+
if (labelSogVmg) labelSogVmg.textContent = 'VMG';
|
|
591
539
|
} else {
|
|
592
540
|
ui.sog.innerText = sogKts.toFixed(1);
|
|
593
|
-
if (
|
|
541
|
+
if (labelSogVmg) labelSogVmg.textContent = 'SOG';
|
|
594
542
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
543
|
+
if (isNavigating) {
|
|
544
|
+
const lastSog = store.histories.sog.length > 0 ? store.histories.sog[store.histories.sog.length - 1].val : sogKts;
|
|
545
|
+
const lastStw = store.histories.stw.length > 0 ? store.histories.stw[store.histories.stw.length - 1].val : stwKts;
|
|
546
|
+
const drift = lastSog - lastStw;
|
|
547
|
+
|
|
548
|
+
if (drift < -0.3) ui.sog.style.setProperty('color', '#ff3b30', 'important'); // Contro
|
|
549
|
+
else if (drift > 0.3) ui.sog.style.setProperty('color', '#00C851', 'important'); // Favore
|
|
550
|
+
else ui.sog.style.setProperty('color', '#ffbb33', 'important'); // Neutro SOG
|
|
551
|
+
} else {
|
|
552
|
+
ui.sog.style.color = "";
|
|
553
|
+
}
|
|
600
554
|
}
|
|
601
555
|
}
|
|
602
556
|
|
|
557
|
+
// --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
|
|
603
558
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
604
559
|
ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
|
|
605
560
|
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
606
561
|
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
607
562
|
}
|
|
608
563
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
564
|
+
// --- GESTIONE VENTO (TWS / AWS SWITCH) ---
|
|
565
|
+
const twsVal = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
566
|
+
const awsVal = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
|
|
567
|
+
|
|
568
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsVal);
|
|
569
|
+
if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsVal);
|
|
570
|
+
|
|
571
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
572
|
+
const labelWind = document.getElementById('tws-aws-label');
|
|
573
|
+
const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
|
|
612
574
|
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
else if (twsKts >= CONFIG.graphs.reef1) ui.tws.style.setProperty('color', '#e67e22', 'important');
|
|
616
|
-
else ui.tws.style.color = "";
|
|
575
|
+
ui.tws.innerText = currentWind.toFixed(1);
|
|
576
|
+
if (labelWind) labelWind.textContent = displayModeTws;
|
|
617
577
|
|
|
618
|
-
|
|
578
|
+
if (currentWind >= CONFIG.graphs.reef2) {
|
|
579
|
+
ui.tws.style.setProperty('color', '#ff3b30', 'important');
|
|
580
|
+
} else if (currentWind >= CONFIG.graphs.reef1) {
|
|
581
|
+
ui.tws.style.setProperty('color', '#ff9800', 'important');
|
|
582
|
+
} else {
|
|
583
|
+
if (displayModeTws === 'AWS') {
|
|
584
|
+
ui.tws.style.setProperty('color', '#5c6bc0', 'important');
|
|
585
|
+
} else {
|
|
586
|
+
const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
|
|
587
|
+
ui.tws.style.setProperty('color', navyNight, 'important');
|
|
588
|
+
}
|
|
589
|
+
}
|
|
619
590
|
}
|
|
620
591
|
|
|
621
592
|
if (store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
622
|
-
ui.awsSvg.textContent =
|
|
593
|
+
ui.awsSvg.textContent = awsVal.toFixed(1);
|
|
623
594
|
}
|
|
624
595
|
|
|
625
|
-
// --- PUNTATORI ANALOGICI
|
|
596
|
+
// --- PUNTATORI ANALOGICI ---
|
|
626
597
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
|
|
627
598
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
|
|
628
|
-
if (smAwa) {
|
|
629
|
-
if (smTwa) {
|
|
599
|
+
if (smAwa) ui.awa.setAttribute('transform', `rotate(${curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val))}, 200, 200)`);
|
|
600
|
+
if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
|
|
630
601
|
|
|
631
|
-
// --- CALCOLO LEEWAY E TRACK POINTER ---
|
|
632
602
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
633
603
|
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
634
|
-
// Azzeramento sotto soglia minima impostata
|
|
635
604
|
smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
636
|
-
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
637
|
-
ui.
|
|
605
|
+
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
606
|
+
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
607
|
+
ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#ff9800" : "";
|
|
638
608
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
639
609
|
}
|
|
640
610
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
// TIER HEAVY (2s) - Grafici e Persistenza Browser
|
|
611
|
+
// --- HEAVY TIER (2s) E SLOW TIER (3s) ---
|
|
644
612
|
if (lastAvgUIUpdate++ % 2 === 0) {
|
|
645
613
|
['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
|
|
646
614
|
saveDashboardState();
|
|
647
615
|
}
|
|
648
616
|
|
|
649
|
-
// TIER SLOW (3s) - Medie Lunghe e Calcolo TACK
|
|
650
617
|
if (lastAvgUIUpdate % 3 === 0) {
|
|
651
618
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
|
|
652
619
|
let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
|
|
@@ -660,50 +627,39 @@ function startDisplayLoop() {
|
|
|
660
627
|
upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
|
|
661
628
|
upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
|
|
662
629
|
|
|
663
|
-
// --- LOGICA TACK STRATEGICA (VETTORIALE) ---
|
|
664
630
|
if (hObj && twdObj) {
|
|
665
|
-
// Funzione interna per riflettere un angolo rispetto all'asse del vento (TWD)
|
|
666
631
|
const reflectAngle = (targetRad, axisRad) => {
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
return Math.atan2(Math.sin(axisRad) *
|
|
670
|
-
Math.cos(axisRad) * diffCos - Math.sin(axisRad) * diffSin);
|
|
632
|
+
const dS = Math.sin(axisRad - targetRad);
|
|
633
|
+
const dC = Math.cos(axisRad - targetRad);
|
|
634
|
+
return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
|
|
671
635
|
};
|
|
672
|
-
|
|
673
636
|
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
674
|
-
|
|
675
|
-
if (
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
ui.tackHdg.innerHTML =
|
|
679
|
-
} else {
|
|
680
|
-
const reflectedH = reflectAngle(hObj.val, twdObj.val);
|
|
681
|
-
const outH = (radToDeg(reflectedH) + 360) % 360;
|
|
682
|
-
ui.tackHdg.innerHTML = `${Math.round(outH).toString().padStart(3, '0')}°`;
|
|
637
|
+
if (!isNavigating) ui.tackHdg.innerHTML = "---°";
|
|
638
|
+
else if (unstableH) { ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data'); }
|
|
639
|
+
else {
|
|
640
|
+
const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
|
|
641
|
+
ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}°`;
|
|
683
642
|
ui.tackHdg.classList.remove('unstable-data');
|
|
684
643
|
}
|
|
685
|
-
|
|
686
644
|
if (cObj) {
|
|
687
645
|
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
688
|
-
if (!isNavigating)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const reflectedC = reflectAngle(cObj.val, twdObj.val);
|
|
694
|
-
const outC = (radToDeg(reflectedC) + 360) % 360;
|
|
695
|
-
ui.tackCog.innerHTML = `${Math.round(outC).toString().padStart(3, '0')}°`;
|
|
646
|
+
if (!isNavigating) ui.tackCog.innerHTML = "---°";
|
|
647
|
+
else if (unstableC) { ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data'); }
|
|
648
|
+
else {
|
|
649
|
+
const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
|
|
650
|
+
ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}°`;
|
|
696
651
|
ui.tackCog.classList.remove('unstable-data');
|
|
697
652
|
}
|
|
698
653
|
}
|
|
699
654
|
}
|
|
700
655
|
|
|
701
|
-
// Rotazione Mini-Bussole
|
|
702
656
|
const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
|
|
703
657
|
const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
704
658
|
if (smHdgIcons && smTwdIcons) {
|
|
705
|
-
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
|
|
706
|
-
|
|
659
|
+
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
|
|
660
|
+
ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
661
|
+
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val));
|
|
662
|
+
ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
707
663
|
}
|
|
708
664
|
}
|
|
709
665
|
}, RENDER_INTERVAL_MS);
|
|
@@ -712,53 +668,57 @@ function startDisplayLoop() {
|
|
|
712
668
|
// ==========================================================================
|
|
713
669
|
// 8. CONFIGURAZIONE E GRAFICI UTILS
|
|
714
670
|
// ==========================================================================
|
|
715
|
-
/**
|
|
716
|
-
* Recupera la configurazione dal server e applica migrazioni automatiche per le vecchie versioni
|
|
717
|
-
*/
|
|
718
671
|
async function fetchServerConfig() {
|
|
719
672
|
try {
|
|
720
673
|
const response = await fetch('/rotevista-config');
|
|
721
674
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
722
675
|
const data = await response.json();
|
|
723
676
|
|
|
724
|
-
// Stampa di debug per verificare cosa riceve il client
|
|
725
|
-
console.log("🔍 Configurazione ricevuta dal Server:", data);
|
|
726
|
-
|
|
727
|
-
// Merge intelligente dei dati ricevuti
|
|
728
677
|
Object.assign(CONFIG.alarms, data.alarms || {});
|
|
729
678
|
Object.assign(CONFIG.graphs, data.graphs || {});
|
|
730
679
|
Object.assign(CONFIG.averaging, data.averaging || {});
|
|
731
680
|
|
|
732
|
-
// --- LOGICA DI MIGRAZIONE SILENZIOSA ---
|
|
733
|
-
// Se il valore ricevuto è il vecchio default (0.85) o inferiore, lo portiamo al nuovo standard 0.95.
|
|
734
|
-
// Questo è necessario perché con i nuovi filtri "Soft" lo 0.85 non farebbe quasi mai lampeggiare gli allarmi.
|
|
735
681
|
if (CONFIG.averaging.stabilityThreshold <= 0.85) {
|
|
736
682
|
CONFIG.averaging.stabilityThreshold = 0.95;
|
|
737
683
|
console.log("♻️ Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85). Aggiornato a 0.95 per ottimizzazione filtri.");
|
|
738
684
|
}
|
|
739
685
|
|
|
740
|
-
// Per le scale, siccome sono nidificate, facciamo un loop di merge profondo
|
|
741
686
|
if (data.scales) {
|
|
742
687
|
for (let key in data.scales) {
|
|
743
688
|
if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
|
|
744
689
|
}
|
|
745
690
|
}
|
|
746
|
-
|
|
747
|
-
console.log("✅ Configurazione applicata. Stabilità attiva:", CONFIG.averaging.stabilityThreshold);
|
|
748
691
|
} catch (err) {
|
|
749
692
|
console.warn("⚠️ Utilizzo default locali. Motivo:", err.message);
|
|
750
693
|
}
|
|
751
694
|
}
|
|
752
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Rielaborazione Storica Time-Based (Rimuove accumulo e gestisce il Pruning dinamicamente)
|
|
698
|
+
*/
|
|
753
699
|
function manageHistory(t, v) {
|
|
754
|
-
const n = Date.now()
|
|
700
|
+
const n = Date.now();
|
|
701
|
+
const interval = (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
702
|
+
|
|
755
703
|
if (!store.graphTempBuf[t]) store.graphTempBuf[t] = [];
|
|
756
704
|
store.graphTempBuf[t].push(v);
|
|
705
|
+
|
|
757
706
|
if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
|
|
758
707
|
const avg = store.graphTempBuf[t].reduce((a, b) => a + b, 0) / store.graphTempBuf[t].length;
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
store.
|
|
708
|
+
|
|
709
|
+
// NUOVO STORAGE TIME-BASED
|
|
710
|
+
store.histories[t].push({ time: n, val: avg });
|
|
711
|
+
|
|
712
|
+
// PRUNING DINAMICO
|
|
713
|
+
const maxViewportMinutes = CONFIG.graphs.historyMinutes * 2;
|
|
714
|
+
const maxHistoryMs = (maxViewportMinutes * 60000) + 30000;
|
|
715
|
+
|
|
716
|
+
while (store.histories[t].length > 0 && (n - store.histories[t][0].time) > maxHistoryMs) {
|
|
717
|
+
store.histories[t].shift();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
store.graphTempBuf[t] = [];
|
|
721
|
+
store.lastUpdates[t] = n;
|
|
762
722
|
}
|
|
763
723
|
}
|
|
764
724
|
|
|
@@ -778,12 +738,36 @@ function updateScaleLabels(t, min, max) {
|
|
|
778
738
|
}
|
|
779
739
|
|
|
780
740
|
/**
|
|
781
|
-
*
|
|
782
|
-
* Usa un Gradiente Lineare SVG dinamico per eliminare le giunzioni dei poligoni.
|
|
741
|
+
* refreshGraph: Recupero dati, switch AWS/TWS e passaggio a motore grafico.
|
|
783
742
|
*/
|
|
743
|
+
function refreshGraph(t) {
|
|
744
|
+
const boxType = (t === 'vmg') ? 'sog' : t;
|
|
745
|
+
let rawData;
|
|
746
|
+
|
|
747
|
+
if (t === 'tws' && displayModeTws === 'AWS') {
|
|
748
|
+
rawData = store.histories['aws'];
|
|
749
|
+
} else {
|
|
750
|
+
rawData = store.histories[t];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (!rawData || rawData.length < 2) return;
|
|
754
|
+
|
|
755
|
+
// ESTRAZIONE SOLO VALORI NUMERICI per calculateScale()
|
|
756
|
+
const values = rawData.map(p => p.val);
|
|
757
|
+
const mode = graphModes[boxType];
|
|
758
|
+
const cfg = calculateScale(boxType, values, mode);
|
|
759
|
+
|
|
760
|
+
const box = document.querySelector(`.box-${boxType}`);
|
|
761
|
+
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
762
|
+
|
|
763
|
+
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
764
|
+
|
|
765
|
+
// Passiamo tutto l'array (oggetti) al nuovo motore
|
|
766
|
+
drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
767
|
+
}
|
|
768
|
+
|
|
784
769
|
/**
|
|
785
|
-
* drawGraph:
|
|
786
|
-
* Versione bilanciata: 1.5px per allarmi critici, 1px per il resto.
|
|
770
|
+
* drawGraph: Motore SVG con Timeline Reale e Gestione GAP
|
|
787
771
|
*/
|
|
788
772
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
789
773
|
const svg = document.getElementById(id);
|
|
@@ -792,96 +776,112 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
792
776
|
const w = 200, h = 40;
|
|
793
777
|
const range = max - min || 1;
|
|
794
778
|
const isDepth = (id === 'depth-graph');
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
// 1. Griglia (Sottile 0.5px)
|
|
798
|
-
let grids = "";
|
|
799
|
-
[0.25, 0.5, 0.75].forEach(p => {
|
|
800
|
-
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" />`;
|
|
801
|
-
});
|
|
802
|
-
const gridInterval = (CONFIG.graphs.historyMinutes <= 15) ? 1 : 5;
|
|
803
|
-
for (let m = gridInterval; m < CONFIG.graphs.historyMinutes; m += gridInterval) {
|
|
804
|
-
const x = w - (m / CONFIG.graphs.historyMinutes) * w;
|
|
805
|
-
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
806
|
-
}
|
|
779
|
+
const now = Date.now();
|
|
807
780
|
|
|
808
|
-
//
|
|
809
|
-
const
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
781
|
+
// VIEWPORT TEMPORALE DINAMICO
|
|
782
|
+
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
783
|
+
const viewportMs = visibleMinutes * 60000;
|
|
784
|
+
const viewportStart = now - viewportMs;
|
|
785
|
+
|
|
786
|
+
// FILTRO DATI VISIBILI (Gestisce Compressione/Zoom in modo naturale)
|
|
787
|
+
const visibleData = d.filter(p => p.time >= viewportStart);
|
|
788
|
+
if (visibleData.length < 2) return;
|
|
789
|
+
|
|
790
|
+
// COLORI
|
|
791
|
+
const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
|
|
792
|
+
const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
|
|
793
|
+
|
|
794
|
+
const getColorProps = (val) => {
|
|
795
|
+
let color = colTws, opacity = "0.15", stroke = "1";
|
|
796
|
+
if (isTws) {
|
|
797
|
+
const baseWind = (displayModeTws === 'AWS') ? colAws : colTws;
|
|
798
|
+
if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke = "1.2"; }
|
|
799
|
+
else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke = "1"; }
|
|
800
|
+
else color = baseWind;
|
|
801
|
+
} else if (isDepth) {
|
|
802
|
+
if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke = "1.2"; }
|
|
803
|
+
else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke = "1"; }
|
|
804
|
+
else color = colDepth;
|
|
805
|
+
} else {
|
|
806
|
+
if (id === 'stw-graph') color = colStw;
|
|
807
|
+
else if (id === 'sog-graph') color = (displayModeSog === 'VMG') ? colVmg : colSog;
|
|
819
808
|
}
|
|
809
|
+
return { color, opacity, stroke };
|
|
820
810
|
};
|
|
821
811
|
|
|
822
|
-
//
|
|
823
|
-
let
|
|
824
|
-
|
|
825
|
-
let areaPath = `M 0 ${h} `;
|
|
826
|
-
|
|
827
|
-
for (let i = 1; i < d.length; i++) {
|
|
828
|
-
const percentPrev = ((i - 1) / (samples - 1)) * 100;
|
|
829
|
-
const percentCurr = (i / (samples - 1)) * 100;
|
|
830
|
-
|
|
831
|
-
const x1 = ((i - 1) / (samples - 1)) * w;
|
|
832
|
-
const y1 = h - (Math.max(0, Math.min(1, (d[i - 1] - min) / range)) * h);
|
|
833
|
-
const x2 = (i / (samples - 1)) * w;
|
|
834
|
-
const y2 = h - (Math.max(0, Math.min(1, (d[i] - min) / range)) * h);
|
|
835
|
-
|
|
836
|
-
const color = getColor(d[i]);
|
|
837
|
-
|
|
838
|
-
// Logica Opacità e Spessore (Tua configurazione)
|
|
839
|
-
let fillOpacity = "0.15";
|
|
840
|
-
let strokeWidth = "1";
|
|
841
|
-
|
|
842
|
-
if (color === "#ff3b30") {
|
|
843
|
-
fillOpacity = "0.85"; // Rosso: Molto visibile
|
|
844
|
-
strokeWidth = "1.5"; // Rosso: Più marcato
|
|
845
|
-
} else if (color === "#ff9800") {
|
|
846
|
-
fillOpacity = "0.45"; // Arancio: Velo medio
|
|
847
|
-
strokeWidth = "1"; // Arancio: Sottile
|
|
848
|
-
}
|
|
812
|
+
// GRIGLIA ORIZZONTALE
|
|
813
|
+
let grids = "";
|
|
814
|
+
[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" />`);
|
|
849
815
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
816
|
+
// GRIGLIA VERTICALE VERA
|
|
817
|
+
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
818
|
+
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
819
|
+
const x = w - ((m / visibleMinutes) * w);
|
|
820
|
+
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
821
|
+
}
|
|
853
822
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
823
|
+
// RENDER TIME-BASED
|
|
824
|
+
let gradientStops = "", lines = "", areaPath = "";
|
|
825
|
+
let started = false;
|
|
826
|
+
|
|
827
|
+
for (let i = 1; i < visibleData.length; i++) {
|
|
828
|
+
const pA = visibleData[i - 1];
|
|
829
|
+
const pB = visibleData[i];
|
|
830
|
+
|
|
831
|
+
// POSIZIONE X ESATTA AL MILLISECONDO
|
|
832
|
+
const x1 = ((pA.time - viewportStart) / viewportMs) * w;
|
|
833
|
+
const x2 = ((pB.time - viewportStart) / viewportMs) * w;
|
|
834
|
+
const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
|
|
835
|
+
const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
|
|
836
|
+
|
|
837
|
+
const props = getColorProps(pB.val);
|
|
838
|
+
|
|
839
|
+
// GESTIONE GAP TEMPORALI (Network loss)
|
|
840
|
+
const deltaTime = pB.time - pA.time;
|
|
841
|
+
const expectedInterval = viewportMs / CONFIG.graphs.samples;
|
|
842
|
+
const isGap = deltaTime > (expectedInterval * 2.5);
|
|
843
|
+
|
|
844
|
+
// STOPS GRADIENTE
|
|
845
|
+
const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
|
|
846
|
+
gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
847
|
+
gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
848
|
+
|
|
849
|
+
// --- FIX: GESTIONE AREA E LINEE DURANTE I GAP ---
|
|
850
|
+
if (isGap) {
|
|
851
|
+
// Se c'è un buco nei dati e avevamo già iniziato a disegnare...
|
|
852
|
+
if (started) {
|
|
853
|
+
// Chiudiamo il pezzo di area precedente scendendo verticalmente (Z non serve qui)
|
|
854
|
+
areaPath += `L ${x1} ${h} `;
|
|
855
|
+
started = false; // Resettiamo il flag per far ripartire l'area al prossimo punto
|
|
856
|
+
}
|
|
857
|
+
} else {
|
|
858
|
+
// I dati sono continui, disegniamo la linea superiore
|
|
859
|
+
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" />`;
|
|
860
860
|
|
|
861
|
-
//
|
|
862
|
-
if (
|
|
863
|
-
|
|
861
|
+
// Gestione Area
|
|
862
|
+
if (!started) {
|
|
863
|
+
// Iniziamo un nuovo pezzo di area dal fondo, saliamo a y1
|
|
864
|
+
areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
|
|
865
|
+
started = true;
|
|
864
866
|
}
|
|
867
|
+
// Aggiungiamo il punto attuale
|
|
868
|
+
areaPath += `L ${x2} ${y2} `;
|
|
865
869
|
}
|
|
870
|
+
}
|
|
866
871
|
|
|
867
|
-
//
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
${gradientStops}
|
|
873
|
-
</linearGradient>
|
|
874
|
-
</defs>
|
|
875
|
-
`;
|
|
876
|
-
|
|
877
|
-
// 5. Render Finale
|
|
878
|
-
if (isTws || isDepth) {
|
|
879
|
-
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
880
|
-
} else {
|
|
881
|
-
// Per STW/SOG usiamo il colore dell'ultimo punto con area fissa 0.15
|
|
882
|
-
const currentPathColor = getColor(d[d.length - 1]);
|
|
883
|
-
svg.innerHTML = `${grids}<path d="${areaPath}" fill="${currentPathColor}" fill-opacity="0.15" stroke="none" />${lines}`;
|
|
872
|
+
// CHIUSURA FINALE DELL'AREA (Solo se non siamo finiti dentro un gap)
|
|
873
|
+
if (started) {
|
|
874
|
+
const last = visibleData[visibleData.length - 1];
|
|
875
|
+
const lastX = ((last.time - viewportStart) / viewportMs) * w;
|
|
876
|
+
areaPath += `L ${lastX} ${h} Z`; // Z chiude automaticamente il path tornando al punto 'M' iniziale
|
|
884
877
|
}
|
|
878
|
+
|
|
879
|
+
// GRADIENTE
|
|
880
|
+
const gradId = `grad-${id}`;
|
|
881
|
+
const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
|
|
882
|
+
|
|
883
|
+
// RENDER FINALE
|
|
884
|
+
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
885
885
|
}
|
|
886
886
|
|
|
887
887
|
// ==========================================================================
|
|
@@ -898,13 +898,49 @@ function toggleFocusMode(type, element) {
|
|
|
898
898
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
899
899
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
900
900
|
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
901
|
-
|
|
901
|
+
|
|
902
|
+
el.addEventListener('pointerdown', (e) => {
|
|
903
|
+
isLongPressActive = false;
|
|
904
|
+
pressTimer = setTimeout(() => {
|
|
905
|
+
if (!isFocusActive) {
|
|
906
|
+
isLongPressActive = true;
|
|
907
|
+
toggleFocusMode(type, el);
|
|
908
|
+
lastTapTime = 0;
|
|
909
|
+
}
|
|
910
|
+
}, 1000);
|
|
911
|
+
});
|
|
912
|
+
|
|
902
913
|
el.addEventListener('pointerup', (e) => {
|
|
903
|
-
clearTimeout(pressTimer);
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
914
|
+
clearTimeout(pressTimer);
|
|
915
|
+
if (isLongPressActive) return;
|
|
916
|
+
|
|
917
|
+
const currentTime = new Date().getTime();
|
|
918
|
+
const tapDelay = currentTime - lastTapTime;
|
|
919
|
+
|
|
920
|
+
if (tapDelay < 300 && tapDelay > 0) {
|
|
921
|
+
clearTimeout(tapTimeout);
|
|
922
|
+
graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard';
|
|
923
|
+
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
924
|
+
refreshGraph(type);
|
|
925
|
+
lastTapTime = 0;
|
|
926
|
+
} else {
|
|
927
|
+
lastTapTime = currentTime;
|
|
928
|
+
tapTimeout = setTimeout(() => {
|
|
929
|
+
if (isFocusActive && el.classList.contains('is-focused')) {
|
|
930
|
+
toggleFocusMode(type, el);
|
|
931
|
+
} else if (!isFocusActive) {
|
|
932
|
+
if (type === 'sog') {
|
|
933
|
+
displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
|
|
934
|
+
} else if (type === 'tws') {
|
|
935
|
+
displayModeTws = (displayModeTws === 'TWS') ? 'AWS' : 'TWS';
|
|
936
|
+
}
|
|
937
|
+
el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
|
938
|
+
setTimeout(() => el.style.backgroundColor = "", 150);
|
|
939
|
+
}
|
|
940
|
+
}, 250);
|
|
941
|
+
}
|
|
907
942
|
});
|
|
943
|
+
|
|
908
944
|
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
909
945
|
});
|
|
910
946
|
|
|
@@ -948,10 +984,7 @@ function connect() {
|
|
|
948
984
|
const d = JSON.parse(e.data);
|
|
949
985
|
if (d.updates) {
|
|
950
986
|
d.updates.forEach(u => {
|
|
951
|
-
// 1. Estraiamo il nome del sensore/sorgente (es. "yacht_device" o "Unknown")
|
|
952
987
|
const sourceLabel = u.source ? (u.source.label || u.source.talker || "Unknown") : "Unknown";
|
|
953
|
-
|
|
954
|
-
// 2. Passiamo il nome della sorgente come TERZO parametro
|
|
955
988
|
if (u.values) {
|
|
956
989
|
u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
|
|
957
990
|
}
|
package/index.html
CHANGED
|
@@ -168,7 +168,10 @@
|
|
|
168
168
|
</div>
|
|
169
169
|
|
|
170
170
|
<div class="data-box box-tws">
|
|
171
|
-
<div class="label-row"
|
|
171
|
+
<div class="label-row">
|
|
172
|
+
<span class="label" id="tws-aws-label">TWS</span>
|
|
173
|
+
<span class="unit">kts</span>
|
|
174
|
+
</div>
|
|
172
175
|
<span class="value" id="tws">0.0</span>
|
|
173
176
|
<div class="graph-wrapper">
|
|
174
177
|
<div class="scale-labels" id="tws-scale"></div>
|
package/index.js
CHANGED
|
@@ -84,19 +84,19 @@ module.exports = function (app) {
|
|
|
84
84
|
reef1: {
|
|
85
85
|
type: 'number',
|
|
86
86
|
title: '1st Reef Alert (Orange)',
|
|
87
|
-
description: "Wind speed
|
|
87
|
+
description: "Wind speed at which the graph turns orange. This threshold applies to the active mode: in TWS it indicates weather intensity, in AWS it indicates pressure on sails/rigging.",
|
|
88
88
|
default: 15.0
|
|
89
89
|
},
|
|
90
90
|
reef2: {
|
|
91
91
|
type: 'number',
|
|
92
92
|
title: '2nd Reef Alert (Red)',
|
|
93
|
-
description: "
|
|
93
|
+
description: "Critical wind speed at which the graph turns red. This threshold applies to the active mode: in TWS it warns of high sea state, in AWS it warns of excessive load on the mast/sails.",
|
|
94
94
|
default: 20.0
|
|
95
95
|
},
|
|
96
96
|
historyMinutes: {
|
|
97
97
|
type: 'number',
|
|
98
98
|
title: 'Strategic Timeline (Minutes)',
|
|
99
|
-
description: "
|
|
99
|
+
description: "Sets the time duration for all charts. It also defines the comparison window for the Strategic Weather Trend (the dot in the TWD compass) to detect long-term wind shifts.",
|
|
100
100
|
default: 5,
|
|
101
101
|
enum: [5, 10, 15, 30, 60]
|
|
102
102
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sailingrotevista/rotevista-dash",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.0.3",
|
|
4
|
+
"description": "Wind Dashboard with navigation and course aids",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|