@sailingrotevista/rotevista-dash 2.0.17 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/app.js +58 -141
  2. package/package.json +1 -1
package/app.js CHANGED
@@ -1,11 +1,16 @@
1
1
  // ==========================================================================
2
- // 1. CONFIGURAZIONE E DEFAULT
2
+ // 1. CONFIGURAZIONE E DEFAULT (Sincronizzato con il Plugin SignalK)
3
3
  // ==========================================================================
4
4
  let CONFIG = {
5
5
  alarms: { depthDanger: 2.5, depthWarning: 5.0 },
6
6
  averages: { smoothWindow: 2000, longWindow: 60000, stabilityTolerance: 2000, stabilityThreshold: 0.90, minSpeed: 0.5 },
7
7
  graphs: { reef1: 15.0, reef2: 20.0, historyMinutes: 5, samples: 60 },
8
- scales: { stw: { stdMax: 12, hercSpan: 4, step: 2 }, sog: { stdMax: 12, hercSpan: 4, step: 2 }, tws: { stdMax: 25, hercSpan: 10, step: 5 }, depth: { stdMax: 20, hercSpan: 10, step: 10 } },
8
+ scales: {
9
+ stw: { stdMax: 12, hercSpan: 4, step: 2 },
10
+ sog: { stdMax: 12, hercSpan: 4, step: 2 },
11
+ tws: { stdMax: 25, hercSpan: 10, step: 5 },
12
+ depth: { stdMax: 20, hercSpan: 10, step: 10 }
13
+ },
9
14
  server: { fallbackIp: "192.168.111.240:3000" }
10
15
  };
11
16
 
@@ -26,9 +31,7 @@ let curBoatCompassRot = 0, curWindCompassRot = 0;
26
31
  let smoothedLeeway = 0, rotationTrend = 0;
27
32
  let lastShortAvgVal = null, lastInstantTwa = null;
28
33
  let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
29
- let twDirty = false;
30
- let reconnectDelay = 1000;
31
- let isNavigating = false;
34
+ let twDirty = false, isNavigating = false, reconnectDelay = 1000;
32
35
 
33
36
  let pressTimer, isFocusActive = false;
34
37
 
@@ -72,13 +75,16 @@ function ktsToMs(kts) { return kts / 1.94384; }
72
75
  function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
73
76
 
74
77
  /**
75
- * Gestione sicura dei buffer per prevenire memory leak.
78
+ * Gestione sicura dei buffer per prevenire memory leak (O(1) complexity).
76
79
  */
77
80
  function safePush(buffer, val, time, maxLen = 200) {
78
81
  buffer.push({ val: val, time: time });
79
82
  if (buffer.length > maxLen) { buffer.shift(); }
80
83
  }
81
84
 
85
+ /**
86
+ * Calcola medie circolari restituendo Radianti.
87
+ */
82
88
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
83
89
  const now = Date.now();
84
90
  const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
@@ -91,8 +97,13 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
91
97
  return { val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI), stable: isStable };
92
98
  }
93
99
 
100
+ // ==========================================================================
101
+ // 4. LOGICA FISICA E CALCOLI
102
+ // ==========================================================================
103
+
94
104
  /**
95
- * Calcola il Vento Reale (Acqua e Terra) partendo dagli Apparenti.
105
+ * Calcola il Vento Reale partendo dagli Apparenti.
106
+ * Logica Acqua (Vele) vs Terra (Meteo).
96
107
  */
97
108
  function computeTrueWind() {
98
109
  const aws = store.raw["environment.wind.speedApparent"];
@@ -103,11 +114,11 @@ function computeTrueWind() {
103
114
  if (aws === undefined || awa === undefined) return;
104
115
  if (awa > Math.PI) awa -= 2 * Math.PI;
105
116
 
106
- // Logica Acqua (TWA/TWS)
117
+ // Vento Reale su ACQUA (TWA/TWS)
107
118
  const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
108
119
  const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y), twa_water = Math.atan2(tw_water_y, tw_water_x);
109
120
 
110
- // Logica Terra (TWD) con gestione deriva normalizzata
121
+ // Vento Reale su TERRA (TWD)
111
122
  const drift_angle = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
112
123
  const sog_vec_x = sog * Math.cos(drift_angle), sog_vec_y = sog * Math.sin(drift_angle);
113
124
  const tw_ground_x = aws * Math.cos(awa) - sog_vec_x, tw_ground_y = aws * Math.sin(awa) - sog_vec_y;
@@ -125,6 +136,7 @@ function processIncomingData(path, val) {
125
136
  if (path === "navigation.position") store.raw["navigation.position"] = val;
126
137
  if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
127
138
 
139
+ // Dirty flag + Debounce 100ms
128
140
  const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
129
141
  if (twPaths.includes(path)) twDirty = true;
130
142
  if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
@@ -134,25 +146,15 @@ function processIncomingData(path, val) {
134
146
  }
135
147
 
136
148
  // ==========================================================================
137
- // 4. AUDIO E ALLARMI
149
+ // 5. AUDIO E ALLARMI
138
150
  // ==========================================================================
139
151
  function playBingBing() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
140
-
141
- function playGybeAlarm() {
142
- if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
143
- const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
144
- function note(f, s, d) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.type = 'square'; o.frequency.value = f; g.gain.setValueAtTime(0.05, s); g.gain.exponentialRampToValueAtTime(0.001, s + d); o.start(s); o.stop(s + d); }
145
- for (let i = 0; i < 4; i++) { note(1800, audioCtx.currentTime + (i * 0.15), 0.1); note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1); }
146
- }
147
-
152
+ function playGybeAlarm() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n; function note(f, s, d) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.type = 'square'; o.frequency.value = f; g.gain.setValueAtTime(0.05, s); g.gain.exponentialRampToValueAtTime(0.001, s + d); o.start(s); o.stop(s + d); } for (let i = 0; i < 4; i++) { note(1800, audioCtx.currentTime + (i * 0.15), 0.1); note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1); } }
148
153
  function checkDepthAlarm(m) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
149
154
 
150
155
  // ==========================================================================
151
- // 5. TREND E TATTICA
156
+ // 6. TREND VENTO E TATTICA
152
157
  // ==========================================================================
153
- /**
154
- * Monitoraggio Trend Vento Professionale (Tattica + Meteo + Gybe Safety)
155
- */
156
158
  function updateWindTrend() {
157
159
  const now = Date.now();
158
160
  const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true);
@@ -160,14 +162,12 @@ function updateWindTrend() {
160
162
  const instantTwaRad = store.raw["environment.wind.angleTrueWater"];
161
163
 
162
164
  if (!shortAvg || !twaAvgObj || instantTwaRad === undefined) return;
163
- const instantTwaDeg = radToDeg(instantTwaRad);
164
- const shortAvgDeg = radToDeg(shortAvg.val);
165
- const twaAvgDeg = radToDeg(twaAvgObj.val);
165
+ const instantTwaDeg = radToDeg(instantTwaRad), shortAvgDeg = radToDeg(shortAvg.val), twaAvgDeg = radToDeg(twaAvgObj.val);
166
166
 
167
167
  if (lastShortAvgVal === null) { lastShortAvgVal = shortAvgDeg; lastInstantTwa = instantTwaDeg; return; }
168
168
  const dt = (now - lastTrendTime) / 1000; lastTrendTime = now;
169
169
 
170
- // --- GYBE SAFETY ---
170
+ // Gybe Safety
171
171
  const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
172
172
  lastInstantTwa = instantTwaDeg;
173
173
  if (gybeDetected && isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
@@ -180,12 +180,12 @@ function updateWindTrend() {
180
180
  return;
181
181
  }
182
182
 
183
- // --- TREND CALCULATION (°/sec) ---
183
+ // Trend calculation con Clamping e Alpha adattivo
184
184
  let diff = (shortAvgDeg - lastShortAvgVal + 540) % 360 - 180; lastShortAvgVal = shortAvgDeg;
185
185
  if (dt > 0) {
186
- let currentRate = Math.max(-10, Math.min(10, diff / dt)); // Clamp a 10°/sec per evitare rumore
186
+ let rate = Math.max(-10, Math.min(10, diff / dt));
187
187
  const alpha = Math.min(1, dt / 5);
188
- rotationTrend = rotationTrend * (1 - alpha) + currentRate * alpha;
188
+ rotationTrend = rotationTrend * (1 - alpha) + rate * alpha;
189
189
  }
190
190
 
191
191
  if (Math.abs(rotationTrend) > 1.0) {
@@ -211,31 +211,34 @@ function updateWindTrend() {
211
211
  }
212
212
 
213
213
  // ==========================================================================
214
- // 6. RENDERING E LOOP PRINCIPALE
214
+ // 7. RENDERING ENGINE (TIERED RENDERING)
215
215
  // ==========================================================================
216
216
  function updateLeewayDisplay(deg) { const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125); ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w); ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`; }
217
217
 
218
- function startDisplayLoop() {
219
- let tick = 0; // Contatore per i cicli di rendering
218
+ function refreshGraph(t) {
219
+ const type = (t === 'vmg') ? 'sog' : t;
220
+ const data = store.histories[t]; if (!data || data.length < 2) return;
221
+ const mode = graphModes[type], cfg = calculateScale(type, data, mode);
222
+ const graphEl = document.getElementById(type + '-graph');
223
+ if (graphEl) { const box = graphEl.closest('.data-box'); if (box) box.classList.toggle('box-hercules', mode === 'hercules'); }
224
+ updateScaleLabels(type, cfg.min, cfg.max); drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
225
+ }
220
226
 
227
+ function startDisplayLoop() {
228
+ let tick = 0;
221
229
  renderInterval = setInterval(() => {
222
- const now = Date.now();
223
- tick++;
230
+ const now = Date.now(); tick++;
224
231
 
225
- // ==========================================================
226
- // LIVE TIER (Ogni 1s) - Reattività, Numeri, Allarmi, Lancette
227
- // ==========================================================
232
+ // --- LIVE TIER (1s) ---
228
233
  const pathsToWatch = { "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 };
229
234
  for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { if (pathsToWatch[p] === ui.awsSvg) ui.awsSvg.textContent = "---"; else pathsToWatch[p].innerText = "---"; delete store.raw[p]; } }
230
235
 
231
236
  const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0), sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
232
237
  isNavigating = stwKts > CONFIG.averages.minSpeed || sogKts > CONFIG.averages.minSpeed;
233
238
 
234
- // Numeri veloci
235
239
  if (store.raw["navigation.speedThroughWater"] !== undefined) { ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts); }
236
240
  if (store.raw["navigation.speedOverGround"] !== undefined) {
237
- const twaRad = store.raw["environment.wind.angleTrueWater"];
238
- const vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
241
+ const twaRad = store.raw["environment.wind.angleTrueWater"], vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
239
242
  manageHistory('vmg', vmgKts); manageHistory('sog', sogKts);
240
243
  if (displayModeSog === 'VMG') { ui.sog.innerText = vmgKts.toFixed(1); ui.sog.style.color = "#64ffda"; document.getElementById('sog-vmg-label').textContent = 'VMG'; }
241
244
  else { ui.sog.innerText = sogKts.toFixed(1); ui.sog.style.color = (sogKts-stwKts > 0.3) ? "#2ecc71" : (sogKts-stwKts < -0.3 ? "#e74c3c" : "#fff"); document.getElementById('sog-vmg-label').textContent = 'SOG'; }
@@ -244,38 +247,27 @@ function startDisplayLoop() {
244
247
  if (store.raw["environment.wind.speedTrue"] !== undefined) { const twsKts = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = twsKts.toFixed(1); ui.tws.style.color = (twsKts >= CONFIG.graphs.reef2) ? "#e74c3c" : (twsKts >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff"); manageHistory('tws', twsKts); }
245
248
  if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
246
249
 
247
- // Lancette (Sempre fluide ogni 1s)
248
250
  const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
249
251
  if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
250
252
  const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
251
253
  if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
252
254
 
253
- // Leeway (Ogni 1s)
254
255
  if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
255
- let driftRad = (store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI;
256
- let driftDeg = radToDeg(driftRad);
256
+ let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
257
257
  if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (driftDeg * 0.1);
258
258
  curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
259
- const isContaminated = Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7;
260
- ui.leewayVal.style.color = isContaminated ? "#f39c12" : "#fff";
259
+ const isContaminated = Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7; ui.leewayVal.style.color = isContaminated ? "#f39c12" : "#fff";
261
260
  updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
262
261
  }
263
262
 
264
- updateWindTrend(); // Trend calcolato ogni secondo
263
+ updateWindTrend(); // Fast update
265
264
 
266
- // ==========================================================
267
- // HEAVY TIER (Ogni 2s) - Disegno Sparklines SVG
268
- // ==========================================================
265
+ // --- HEAVY TIER (2s) ---
269
266
  if (tick % 2 === 0) {
270
- refreshGraph('stw');
271
- refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog');
272
- refreshGraph('depth');
273
- refreshGraph('tws');
267
+ refreshGraph('stw'); refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog'); refreshGraph('depth'); refreshGraph('tws');
274
268
  }
275
269
 
276
- // ==========================================================
277
- // SLOW TIER (Ogni 3s) - Medie MEAN e Calcoli TACK
278
- // ==========================================================
270
+ // --- SLOW TIER (3s) ---
279
271
  if (tick % 3 === 0) {
280
272
  let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
281
273
  cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
@@ -291,7 +283,6 @@ function startDisplayLoop() {
291
283
  }
292
284
  };
293
285
  upUI(ui.hdg, hObj, true); upUI(ui.cog, cObj, true); upUI(ui.awaAvg, awObj, false); upUI(ui.twaAvg, twObj, false); upUI(ui.twdAvg, twdObj, true);
294
-
295
286
  if (hObj && twObj && hObj.val !== null) {
296
287
  const tackHdgDeg = radToDeg((hObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
297
288
  ui.tackHdg.innerHTML = `${Math.round((tackHdgDeg + 360) % 360).toString().padStart(3, '0')}&deg;`;
@@ -300,25 +291,18 @@ function startDisplayLoop() {
300
291
  ui.tackCog.innerHTML = `${Math.round((tackCogDeg + 360) % 360).toString().padStart(3, '0')}&deg;`;
301
292
  }
302
293
  }
303
-
304
294
  const smHdg = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false), smTwd = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
305
295
  if (smHdg && smTwd) {
306
296
  curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwd.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
307
297
  curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdg.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
308
298
  }
309
299
  }
310
-
311
- // Pulizia buffer
312
- if (tick % 60 === 0) { // Ogni minuto pulisce tutto per sicurezza
313
- for (let b in store.smoothBuf) { while (store.smoothBuf[b].length > 0 && (now - store.smoothBuf[b][0].time) > CONFIG.averages.smoothWindow) store.smoothBuf[b].shift(); }
314
- for (let b in store.longBuf) { while (store.longBuf[b].length > 0 && (now - store.longBuf[b][0].time) > CONFIG.averages.longWindow) store.longBuf[b].shift(); }
315
- tick = 0; // Reset contatore
316
- }
300
+ if (tick % 60 === 0) tick = 0;
317
301
  }, RENDER_INTERVAL_MS);
318
302
  }
319
303
 
320
304
  // ==========================================================================
321
- // 7. CONFIGURAZIONE E GRAFICI
305
+ // 8. INTERAZIONI E RETE
322
306
  // ==========================================================================
323
307
  async function fetchServerConfig() {
324
308
  if (!window.location.protocol.includes("http")) return;
@@ -343,43 +327,9 @@ async function fetchServerConfig() {
343
327
  }
344
328
  }
345
329
 
346
- /**
347
- * Gestisce l'archiviazione dei dati storici.
348
- * Il disegno grafico viene ora gestito separatamente nel loop pesante.
349
- */
350
330
  function manageHistory(t, v) {
351
- const n = Date.now();
352
- const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
353
-
354
- if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
355
- store.histories[t].push(v);
356
- if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
357
- store.lastUpdates[t] = n;
358
- }
359
- }
360
-
361
- /**
362
- * Funzione dedicata al ridisegno dei grafici (chiamata dal loop HEAVY)
363
- */
364
- function refreshGraph(t) {
365
- const type = (t === 'vmg') ? 'sog' : t;
366
- const data = store.histories[t];
367
- if (!data || data.length < 2) return;
368
-
369
- const mode = graphModes[type];
370
- const cfg = calculateScale(type, data, mode);
371
-
372
- // Aggiornamento stile box Hercules
373
- const graphEl = document.getElementById(type + '-graph');
374
- if (graphEl) {
375
- const box = graphEl.closest('.data-box');
376
- if (box) {
377
- if (mode === 'hercules') box.classList.add('box-hercules');
378
- else box.classList.remove('box-hercules');
379
- }
380
- }
381
- updateScaleLabels(type, cfg.min, cfg.max);
382
- drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
331
+ const n = Date.now(); const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
332
+ if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) { store.histories[t].push(v); if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift(); store.lastUpdates[t] = n; }
383
333
  }
384
334
 
385
335
  function calculateScale(type, data, mode) {
@@ -409,9 +359,6 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
409
359
  svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(255,255,255,0.08)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${colorKey}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${colorKey}" />`;
410
360
  }
411
361
 
412
- // ==========================================================================
413
- // 8. INTERAZIONI E SIMULATORE
414
- // ==========================================================================
415
362
  function toggleFocusMode(type, element) {
416
363
  const container = document.querySelector('.main-container'); const parentPanel = element.closest('.side-panel'); const isLeft = parentPanel.classList.contains('left-panel');
417
364
  isFocusActive = !isFocusActive;
@@ -440,44 +387,14 @@ if (ui.hotspot) {
440
387
 
441
388
  function connect() {
442
389
  if (simulationMode) return;
443
-
444
390
  let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
445
391
  const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws';
446
-
447
392
  try {
448
393
  socket = new WebSocket(`${protocol}://${addr}/signalk/v1/stream?subscribe=self`);
449
-
450
- socket.onopen = () => {
451
- ui.status.className = "online";
452
- ui.status.innerText = "ONLINE";
453
- reconnectDelay = 1000; // Reset del ritardo dopo una connessione riuscita
454
- };
455
-
456
- socket.onmessage = (e) => {
457
- if (simulationMode) return;
458
- const d = JSON.parse(e.data);
459
- if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value)));
460
- };
461
-
462
- socket.onclose = () => {
463
- if (!simulationMode) {
464
- ui.status.className = "offline";
465
- ui.status.innerText = "RECONNECTING...";
466
-
467
- // Exponential Backoff: aumenta il tempo ad ogni tentativo fallito
468
- setTimeout(connect, reconnectDelay);
469
- reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); // Max 10 secondi
470
- }
471
- };
472
-
473
- socket.onerror = () => {
474
- socket.close(); // Forza il trigger di onclose
475
- };
476
-
477
- } catch (e) {
478
- setTimeout(connect, reconnectDelay);
479
- reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
480
- }
394
+ socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; reconnectDelay = 1000; };
395
+ 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))); };
396
+ socket.onclose = () => { if (!simulationMode) { ui.status.className = "offline"; ui.status.innerText = "RECONNECTING..."; setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); } };
397
+ } catch (e) { setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); }
481
398
  }
482
399
 
483
400
  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; } }; })());
@@ -507,7 +424,7 @@ function startDynamicSimulation() {
507
424
  }
508
425
 
509
426
  // ==========================================================================
510
- // 9. INIT
427
+ // 11. INIT
511
428
  // ==========================================================================
512
429
  window.addEventListener('contextmenu', e => e.preventDefault(), true);
513
430
  (function genTicks() { const c = document.getElementById('ticks'); if (c) { for (let i = 0; i < 360; i += 10) { const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50)); l.setAttribute("stroke", m ? "#fff" : "#666"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "2.0.17",
3
+ "version": "2.0.18",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {