@sailingrotevista/rotevista-dash 2.0.17 → 2.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/app.js +58 -141
  2. package/index.html +12 -0
  3. package/package.json +1 -1
  4. package/style.css +68 -65
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/index.html CHANGED
@@ -123,10 +123,22 @@
123
123
  <g id="ticks"></g>
124
124
 
125
125
  <g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="hanging" font-family="Arial" font-weight="bold">
126
+ <!-- Etichette Principali -->
126
127
  <text font-size="16" transform="translate(200, 65)">0</text>
127
128
  <text font-size="16" transform="translate(335, 200) rotate(90)">90</text>
128
129
  <text font-size="16" transform="translate(65, 200) rotate(-90)">90</text>
129
130
  <text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
131
+
132
+ <!-- Etichette Intermedie (30-150 Gradi) -->
133
+ <text font-size="11" transform="translate(267.5, 83) rotate(30)">30</text>
134
+ <text font-size="11" transform="translate(317, 132.5) rotate(60)">60</text>
135
+ <text font-size="11" transform="translate(317, 267.5) rotate(120)">120</text>
136
+ <text font-size="11" transform="translate(267.5, 317) rotate(150)">150</text>
137
+
138
+ <text font-size="11" transform="translate(132.5, 83) rotate(-30)">30</text>
139
+ <text font-size="11" transform="translate(83, 132.5) rotate(-60)">60</text>
140
+ <text font-size="11" transform="translate(83, 267.5) rotate(-120)">120</text>
141
+ <text font-size="11" transform="translate(132.5, 317) rotate(-150)">150</text>
130
142
  </g>
131
143
 
132
144
  <g id="track-pointer" transform="rotate(0, 200, 200)">
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.19",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/style.css CHANGED
@@ -10,7 +10,6 @@ body {
10
10
  height: 100vh;
11
11
  width: 100vw;
12
12
  overflow: hidden;
13
- /* BLOCCO TOTALE GESTI DI SISTEMA: Fondamentale per il Long Press su mobile */
14
13
  -webkit-touch-callout: none;
15
14
  -webkit-user-select: none;
16
15
  user-select: none;
@@ -23,11 +22,8 @@ body {
23
22
  height: 100%;
24
23
  padding: 5px;
25
24
  box-sizing: border-box;
26
- gap: 8px; /* Spazio costante tra tutti i blocchi della dashboard */
27
-
28
- /* LAYOUT LIQUIDO:
29
- I lati hanno un minimo vitale (180px) per proteggere i testi.
30
- Il centro (auto) si adatta millimetricamente al diametro dell'SVG. */
25
+ gap: 8px;
26
+ /* Rapporto Standard: Lati 1fr, Centro 1.5fr (Bilanciato) */
31
27
  grid-template-columns: minmax(180px, 1fr) minmax(auto, 1.5fr) minmax(180px, 1fr);
32
28
  grid-template-rows: 100%;
33
29
  transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
@@ -35,7 +31,7 @@ body {
35
31
  }
36
32
 
37
33
  /* ==========================================================================
38
- 2. PANNELLI E DATA-BOX (DISTRIBUZIONE FR)
34
+ 2. PANNELLI E DATA-BOX
39
35
  ========================================================================== */
40
36
  .side-panel {
41
37
  display: flex;
@@ -48,10 +44,9 @@ body {
48
44
  }
49
45
 
50
46
  .left-panel { grid-column: 1; }
51
-
52
47
  .center-panel {
53
48
  grid-column: 2;
54
- position: relative; /* Indispensabile per posizionare #status al suo interno */
49
+ position: relative;
55
50
  display: flex;
56
51
  flex-direction: column;
57
52
  align-items: center;
@@ -59,7 +54,6 @@ body {
59
54
  height: 100%;
60
55
  overflow: hidden;
61
56
  }
62
-
63
57
  .right-panel {
64
58
  grid-column: 3;
65
59
  align-items: flex-end;
@@ -69,68 +63,83 @@ body {
69
63
  .data-box {
70
64
  position: relative;
71
65
  border-bottom: 1px solid #222;
72
- padding: 2px 4px; /* Padding ottimizzato per il recupero spazio verticale */
66
+ padding: 2px 4px;
73
67
  display: flex;
74
68
  flex-direction: column;
75
69
  width: 100%;
76
70
  box-sizing: border-box;
77
- container-type: size; /* Per unità cqh */
78
- flex: 1 1 0px; /* Divide lo spazio equamente in 5 parti */
71
+ container-type: size;
72
+ flex: 1 1 0px;
79
73
  min-height: 0;
80
74
  overflow: hidden;
75
+ transition: background-color 0.2s ease;
81
76
  }
82
77
 
83
- /* Allineamento Speculare: SX a sinistra, DX a destra del box */
84
78
  .left-panel .data-box { align-items: flex-start; text-align: left; }
85
79
  .right-panel .data-box { align-items: flex-end; text-align: right; }
86
80
 
87
81
  /* ==========================================================================
88
- 3. TACTICAL FOCUS MODE (AUTO-EXPANDING SPLIT / DUAL SCREEN)
82
+ 3. TACTICAL FOCUS MODE (DUAL SCREEN 60/40)
89
83
  ========================================================================== */
90
84
 
91
- /* Nasconde le colonne non focalizzate */
85
+ /* Nasconde i pannelli non attivi */
92
86
  .focus-active .side-panel:not(.has-focus) { display: none !important; }
93
87
 
94
- /* In Focus Mode Left: [Pannello Sinistro Largo | Centro Stretto] */
88
+ /* Split 60/40: Grafico a Sinistra */
95
89
  .focus-active.focus-side-left {
96
- grid-template-columns: 2fr auto !important;
90
+ grid-template-columns: 3fr 2fr !important;
97
91
  }
98
92
  .focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
99
- .focus-active.focus-side-left .center-panel { grid-column: 2 !important; justify-content: flex-start; }
93
+ .focus-active.focus-side-left .center-panel {
94
+ grid-column: 2 !important;
95
+ justify-content: center !important;
96
+ align-items: center !important;
97
+ }
100
98
 
101
- /* In Focus Mode Right: [Centro Stretto | Pannello Destro Largo] */
99
+ /* Split 60/40: Grafico a Destra */
102
100
  .focus-active.focus-side-right {
103
- grid-template-columns: auto 2fr !important;
101
+ grid-template-columns: 2fr 3fr !important;
102
+ }
103
+ .focus-active.focus-side-right .center-panel {
104
+ grid-column: 1 !important;
105
+ justify-content: center !important;
106
+ align-items: center !important;
104
107
  }
105
- .focus-active.focus-side-right .center-panel { grid-column: 1 !important; justify-content: flex-start; }
106
108
  .focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
107
109
 
108
- /* Styling del box in Focus Mode */
110
+ /* Espansione Box Focalizzato */
109
111
  .focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
110
112
  .focus-active .has-focus .data-box.is-focused {
111
113
  height: 100vh !important;
112
114
  border: none;
113
115
  background: rgba(255, 255, 255, 0.05);
114
- padding: 20px;
116
+ padding: 2vw 3vw !important;
117
+ display: flex;
118
+ flex-direction: column;
115
119
  }
116
120
 
117
- /* Tipografia massiccia in Focus Mode */
118
- .focus-active .is-focused .value { font-size: clamp(4rem, 25cqh, 4rem) !important; margin-top: 15px; }
119
- .focus-active .is-focused .scale-labels { font-size: 32px !important; min-width: 40px !important; line-height: 1.2; }
121
+ /* Tipografia Massiccia Focus Mode */
122
+ .focus-active .is-focused .value { font-size: clamp(4rem, 25cqh, 10rem) !important; margin-top: 15px; }
123
+ .focus-active .is-focused .scale-labels { font-size: clamp(14px, 1.8vw, 22px) !important; min-width: 50px !important; }
120
124
  .focus-active .is-focused .label-row .label { font-size: 2rem !important; }
121
125
  .focus-active .is-focused .label-row .unit { font-size: 2rem !important; }
122
126
 
123
- .focus-active .is-focused .sparkline path { stroke-width: 1px !important; }
127
+ .focus-active .is-focused .sparkline path { stroke-width: 2.5px !important; }
128
+
129
+ /* Protezione Wind Gauge nel 40% di spazio */
130
+ .focus-active .center-panel svg#wind-gauge {
131
+ max-width: 95% !important;
132
+ max-height: 85vh !important;
133
+ }
124
134
 
125
135
  /* ==========================================================================
126
- 4. TIPOGRAFIA DINAMICA (ELASTICA CQH)
136
+ 4. TIPOGRAFIA DINAMICA E ELASTICA
127
137
  ========================================================================== */
128
138
  .label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
129
139
 
130
140
  .label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
131
141
  .unit { color: #888; font-size: 0.6rem; font-weight: bold; }
132
142
 
133
- /* Numero standard (22% altezza box) */
134
143
  .value {
135
144
  color: #fff;
136
145
  font-size: clamp(1.2rem, 22cqh, 3rem);
@@ -143,14 +152,13 @@ body {
143
152
  .value-large, .dual-value-container, .value-with-compass { margin-top: auto; margin-bottom: auto; }
144
153
  .value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
145
154
 
146
- /* TACK Layout (Mure opposte) */
147
155
  .dual-value-container { display: flex; justify-content: space-between; width: 100%; }
148
156
  .dual-value-col { display: flex; flex-direction: column; width: 48%; }
149
157
  .dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
150
158
  .value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
151
159
 
152
160
  /* ==========================================================================
153
- 5. BUSSOLA TWD DINAMICA (WIDGET)
161
+ 5. WIDGETS (BUSSOLA TWD)
154
162
  ========================================================================== */
155
163
  .value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 5px; }
156
164
 
@@ -167,7 +175,7 @@ body {
167
175
  }
168
176
 
169
177
  /* ==========================================================================
170
- 6. GRAFICI E SCALE (OTTIMIZZAZIONE ORIZZONTALE)
178
+ 6. GRAFICI E SCALE
171
179
  ========================================================================== */
172
180
  .graph-wrapper {
173
181
  position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px;
@@ -175,7 +183,6 @@ body {
175
183
  border-radius: 4px; gap: 0px !important;
176
184
  }
177
185
 
178
- /* Allargamento verso il centro */
179
186
  .left-panel .graph-wrapper { margin-right: -6px; }
180
187
  .right-panel .graph-wrapper { margin-left: -6px; }
181
188
 
@@ -188,20 +195,18 @@ body {
188
195
  height: 100%; line-height: 1; padding: 0; border: none !important;
189
196
  }
190
197
 
191
- /* Simmetria scale */
192
198
  .left-panel .scale-labels { order: 2; text-align: left; padding-left: 4px; }
193
199
  .left-panel .sparkline { order: 1; }
194
200
  .right-panel .scale-labels { order: 1; text-align: right; padding-right: 4px; }
195
201
  .right-panel .sparkline { order: 2; }
196
202
 
197
- /* Colori grafici */
198
203
  #stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
199
204
  #sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
200
205
  #depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
201
206
  #tws-graph { stroke: #ffffff; fill: rgba(255, 255, 255, 0.08); }
202
207
 
203
208
  /* ==========================================================================
204
- 7. HERCULES MODE (ZOOM E VISUALS)
209
+ 7. HERCULES MODE
205
210
  ========================================================================== */
206
211
  .line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
207
212
  .box-hercules { background: rgba(255, 0, 0, 0.08) !important; }
@@ -215,20 +220,14 @@ body {
215
220
  .right-panel .box-hercules .label:only-child::after { content: " HERCULES"; }
216
221
 
217
222
  /* ==========================================================================
218
- 8. STATI, STATUS E ANIMAZIONI
223
+ 8. STATUS E ANIMAZIONI
219
224
  ========================================================================== */
220
225
  #status {
221
- position: absolute;
222
- top: 10px;
223
- right: 10px;
224
- font-size: 0.6rem;
225
- font-weight: 900;
226
- text-transform: uppercase;
227
- z-index: 1000;
228
- letter-spacing: 1px;
229
- background: rgba(0,0,0,0.4);
230
- padding: 2px 6px;
231
- border-radius: 4px;
226
+ position: absolute; top: 10px; right: 10px;
227
+ font-size: 0.6rem; font-weight: 900;
228
+ text-transform: uppercase; z-index: 1000;
229
+ letter-spacing: 1px; background: rgba(0,0,0,0.4);
230
+ padding: 2px 6px; border-radius: 4px;
232
231
  }
233
232
  .online { color: #2ecc71; opacity: 0.5; }
234
233
  .offline { color: #e74c3c; font-weight: bold; }
@@ -245,11 +244,11 @@ body {
245
244
  #wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
246
245
 
247
246
  /* ==========================================================================
248
- 9. RESPONSIVE (PORTRAIT MODE)
247
+ 9. RESPONSIVE (PORTRAIT)
249
248
  ========================================================================== */
250
249
  @media (max-aspect-ratio: 0.9 / 1) {
251
250
  .main-container { grid-template-columns: 1fr 1fr !important; grid-template-rows: 45vh calc(55vh - 8px) !important; }
252
- .center-panel { grid-row: 1 !important; grid-column: 1 / span 2 !important; padding: 5px 0; }
251
+ .center-panel { grid-row: 1 !important; grid-column: 1 / span 2 !important; }
253
252
  .left-panel { grid-row: 2 !important; grid-column: 1 !important; }
254
253
  .right-panel { grid-row: 2 !important; grid-column: 2 !important; }
255
254
  .main-container.focus-active { display: flex !important; flex-direction: column !important; }
@@ -259,7 +258,7 @@ body {
259
258
  }
260
259
 
261
260
  /* ==========================================================================
262
- 10. NIGHT MODE (RED ON BLACK - TACTICAL)
261
+ 10. NIGHT MODE (TACTICAL RED)
263
262
  ========================================================================== */
264
263
  body.night-mode { background-color: #000 !important; color: #ff0000 !important; }
265
264
  .night-mode .side-panel { background: rgba(20, 0, 0, 0.4); border: 1px solid #330000; }
@@ -274,19 +273,30 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
274
273
  .night-mode #tws-graph line:not([stroke*="rgba"]) { stroke: #ff3333 !important; stroke-width: 1.8px !important; }
275
274
  .night-mode .scale-labels { color: #660000 !important; }
276
275
 
277
- .night-mode .box-hercules { background: rgba(60, 0, 0, 0.2) !important; }
278
- .night-mode .line-hercules { filter: drop-shadow(0 0 6px #ff0000) !important; }
279
-
280
276
  .night-mode #wind-gauge circle { stroke: #330000; }
281
277
  .night-mode #ticks line { stroke: #4d0000 !important; }
282
278
  .night-mode #tick-labels { fill: #800000 !important; }
283
279
  .night-mode #boat-icon { fill: #330000 !important; opacity: 0.6; }
284
280
  .night-mode #aws-val-svg { fill: #ff3333 !important; }
281
+ .night-mode #aws-display-group text { fill: #ff3333 !important; }
282
+ .night-mode #center-glow feDropShadow { flood-color: #ff0000 !important; flood-opacity: 0.6 !important; }
283
+
284
+ /* Settori Vento Night */
285
+ .night-mode #wind-gauge path[stroke="#ff0000"] { stroke: #660000 !important; opacity: 0.8; }
286
+ .night-mode #wind-gauge path[stroke="#00ff00"] { stroke: #660000 !important; stroke-dasharray: 4, 3; opacity: 0.8; }
287
+ .night-mode #wind-gauge path[stroke="#ff8800"] { stroke: #330000 !important; stroke-width: 8; }
288
+
289
+ /* Lancette Night */
285
290
  .night-mode #awa-pointer path { fill: #ff0000; stroke: #000; }
286
291
  .night-mode #twa-pointer path { fill: #800000; stroke: #000; }
287
292
  .night-mode #track-pointer path { fill: #ff0000; stroke: #fff; stroke-width: 0.5; }
288
- .night-mode #aws-display-group text { fill: #ff3333 !important; }
289
- .night-mode #center-glow feDropShadow { flood-color: #ff0000 !important; flood-opacity: 0.6 !important; }
293
+
294
+ /* Leeway Night */
295
+ .night-mode rect[fill="url(#leeway-grad)"] { fill: url(#leeway-night-grad) !important; }
296
+ .night-mode #leeway-val { fill: #ff3333 !important; }
297
+ .night-mode g[stroke="#555"] line { stroke: #4d0000 !important; }
298
+ .night-mode g[fill="#555"] text { fill: #660000 !important; }
299
+ .night-mode rect[fill="#222"] { fill: #0a0000 !important; stroke: #200000; }
290
300
 
291
301
  /* Bussola TWD Night */
292
302
  .night-mode .mini-compass { border-color: #330000; background: #000; }
@@ -304,11 +314,4 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
304
314
  transition: opacity 0.3s ease;
305
315
  }
306
316
 
307
- .is-trending { animation: blink-trend 1s infinite !important; }
308
-
309
- /* Allarme Strambata: Rosso fisso con bagliore neon */
310
- .is-gybing {
311
- opacity: 1 !important;
312
- animation: none !important;
313
- filter: drop-shadow(0 0 8px #ff0000) !important;
314
- }
317
+ .is-trending { a