@sailingrotevista/rotevista-dash 1.0.23 → 1.0.24

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 +107 -63
  2. package/index.html +21 -4
  3. package/package.json +1 -1
  4. package/style.css +109 -205
package/app.js CHANGED
@@ -1,17 +1,33 @@
1
1
  // ==========================================================================
2
- // 1. CONFIGURAZIONE E DEFAULT
2
+ // 1. CONFIGURAZIONE E DEFAULT (Sincronizzato con il Plugin SignalK)
3
3
  // ==========================================================================
4
4
  let CONFIG = {
5
- alarms: { depthDanger: 2.5, depthWarning: 5.0 },
6
- averages: { smoothWindow: 2000, longWindow: 60000, stabilityTolerance: 2000, stabilityThreshold: 0.90, minSpeed: 0.5 },
7
- graphs: { reef1: 15.0, reef2: 20.0, historyMinutes: 5, samples: 60 },
5
+ alarms: {
6
+ depthDanger: 2.5,
7
+ depthWarning: 5.0
8
+ },
9
+ averages: {
10
+ smoothWindow: 2000, // Media veloce per le lancette (ms)
11
+ longWindow: 60000, // Media lunga per i dati MEAN (ms)
12
+ stabilityTolerance: 2000, // Tolleranza riempimento buffer (ms)
13
+ stabilityThreshold: 0.90, // Indice R minimo per dati stabili
14
+ minSpeed: 0.5 // Velocità sotto la quale non lampeggia nulla (kts)
15
+ },
16
+ graphs: {
17
+ reef1: 15.0, // Soglia arancio TWS
18
+ reef2: 20.0, // Soglia rossa TWS
19
+ historyMinutes: 5, // Durata temporale dei grafici (minuti)
20
+ samples: 60 // Numero di punti disegnati nel grafico
21
+ },
8
22
  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 },
23
+ stw: { stdMax: 12, hercSpan: 4, step: 2 },
24
+ sog: { stdMax: 12, hercSpan: 4, step: 2 },
25
+ tws: { stdMax: 25, hercSpan: 10, step: 5 },
12
26
  depth: { stdMax: 20, hercSpan: 10, step: 10 }
13
27
  },
14
- server: { fallbackIp: "192.168.111.240:3000" }
28
+ server: {
29
+ fallbackIp: "192.168.111.240:3000"
30
+ }
15
31
  };
16
32
 
17
33
  const RENDER_INTERVAL_MS = 1000;
@@ -19,13 +35,17 @@ const TIMEOUT_MS = 5000;
19
35
  const SIM_SAMPLE_INTERVAL = 1000;
20
36
 
21
37
  // ==========================================================================
22
- // 2. STATO GLOBALE E UI
38
+ // 2. STATO GLOBALE E RIFERIMENTI UI
23
39
  // ==========================================================================
24
40
  let simulationMode = false;
25
41
  let socket, renderInterval, simInterval;
26
42
  let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
27
43
  let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
28
44
 
45
+ // Gestione Focus e Interazioni
46
+ let pressTimer, isFocusActive = false, blockNextClick = false;
47
+
48
+ // Modalità Scale (Standard/Hercules) salvate nel browser
29
49
  const graphModes = {
30
50
  stw: localStorage.getItem('mode_stw') || 'standard',
31
51
  sog: localStorage.getItem('mode_sog') || 'standard',
@@ -33,7 +53,14 @@ const graphModes = {
33
53
  depth: localStorage.getItem('mode_depth') || 'standard'
34
54
  };
35
55
 
36
- const store = { raw: {}, timestamps: {}, smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] }, longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] }, histories: { stw: [], sog: [], depth: [], tws: [] }, lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 } };
56
+ const store = {
57
+ raw: {},
58
+ timestamps: {},
59
+ smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
60
+ longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
61
+ histories: { stw: [], sog: [], depth: [], tws: [] },
62
+ lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 }
63
+ };
37
64
 
38
65
  const ui = {
39
66
  stw: document.getElementById('stw'), sog: document.getElementById('sog'),
@@ -49,7 +76,7 @@ const ui = {
49
76
  };
50
77
 
51
78
  // ==========================================================================
52
- // 3. CARICAMENTO CONFIGURAZIONE
79
+ // 3. CARICAMENTO CONFIGURAZIONE DAL SERVER
53
80
  // ==========================================================================
54
81
  async function fetchServerConfig() {
55
82
  if (!window.location.protocol.includes("http")) return;
@@ -72,9 +99,11 @@ async function fetchServerConfig() {
72
99
  parseNumbers(actual);
73
100
  if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
74
101
  if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
75
- if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging }; // Mapping averaging -> averages
76
- if (actual.scales) { for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; } }
77
- console.log("SUCCESS: Config loaded from Server:", CONFIG);
102
+ if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
103
+ if (actual.scales) {
104
+ for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; }
105
+ }
106
+ console.log("Dashboard: Configurazione caricata dal server.");
78
107
  return;
79
108
  }
80
109
  }
@@ -83,7 +112,7 @@ async function fetchServerConfig() {
83
112
  }
84
113
 
85
114
  // ==========================================================================
86
- // 4. MATEMATICA VETTORIALE
115
+ // 4. MATEMATICA E GESTIONE DATI
87
116
  // ==========================================================================
88
117
  function radToDeg(rad) { return rad * (180 / Math.PI); }
89
118
  function degToRad(deg) { return deg * (Math.PI / 180); }
@@ -91,6 +120,7 @@ function msToKts(ms) { return ms * 1.94384; }
91
120
  function ktsToMs(kts) { return kts / 1.94384; }
92
121
  function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
93
122
 
123
+ // Calcolo media circolare vettoriale con rilevamento stabilità
94
124
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
95
125
  const now = Date.now();
96
126
  const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
@@ -104,9 +134,6 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
104
134
  return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
105
135
  }
106
136
 
107
- // ==========================================================================
108
- // 5. GESTIONE DATI IN INGRESSO
109
- // ==========================================================================
110
137
  function processIncomingData(path, val) {
111
138
  const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
112
139
  if (path === "navigation.headingTrue") { store.smoothBuf.hdg.push({ val: val, time: now }); store.longBuf.hdg.push({ val: val, time: now }); }
@@ -114,6 +141,7 @@ function processIncomingData(path, val) {
114
141
  if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
115
142
  if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
116
143
 
144
+ // Calcolo TWD (True Wind Direction)
117
145
  if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
118
146
  let twdRad = 0;
119
147
  if (path === "environment.wind.directionTrue") twdRad = val;
@@ -127,7 +155,7 @@ function processIncomingData(path, val) {
127
155
  }
128
156
 
129
157
  // ==========================================================================
130
- // 6. RENDER LOOP
158
+ // 5. MOTORE DI RENDERING PRINCIPALE
131
159
  // ==========================================================================
132
160
  function startDisplayLoop() {
133
161
  renderInterval = setInterval(() => {
@@ -146,6 +174,7 @@ function startDisplayLoop() {
146
174
  const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
147
175
  if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
148
176
 
177
+ // Calcolo Drift Reale (COG-HDG)
149
178
  if (store.raw["navigation.courseOverGroundTrue"] && store.raw["navigation.headingTrue"]) {
150
179
  let drift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
151
180
  if (curSog < CONFIG.averages.minSpeed) drift = 0;
@@ -186,7 +215,7 @@ function startDisplayLoop() {
186
215
  }
187
216
 
188
217
  // ==========================================================================
189
- // 7. CONNESSIONE SIGNALK (subscribe=self)
218
+ // 6. CONNESSIONE SIGNALK
190
219
  // ==========================================================================
191
220
  function connect() {
192
221
  if (simulationMode) return;
@@ -200,22 +229,15 @@ function connect() {
200
229
  }
201
230
 
202
231
  // ==========================================================================
203
- // 8. FUNZIONI GRAFICHE (Hercules Mode)
232
+ // 7. FUNZIONI GRAFICHE E SCALATURA
204
233
  // ==========================================================================
205
234
  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)}°`; }
206
235
 
207
236
  function manageHistory(t, v) {
208
237
  const n = Date.now();
209
- const dynamicInterval = (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
210
- const interval = simulationMode ? SIM_SAMPLE_INTERVAL : dynamicInterval;
211
-
212
- if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
213
- store.histories[t].push(v);
214
- if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
215
- store.lastUpdates[t] = n;
216
- }
217
- const mode = graphModes[t];
218
- const cfg = calculateScale(t, store.histories[t], mode);
238
+ const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
239
+ 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; }
240
+ const mode = graphModes[t], cfg = calculateScale(t, store.histories[t], mode);
219
241
  const box = document.getElementById(t + '-graph').closest('.data-box');
220
242
  if (mode === 'hercules') box.classList.add('box-hercules'); else box.classList.remove('box-hercules');
221
243
  updateScaleLabels(t, cfg.min, cfg.max);
@@ -231,9 +253,7 @@ function calculateScale(type, data, mode) {
231
253
  if (span % 2 !== 0) span += 1;
232
254
  let min = Math.max(0, Math.floor(avg - (span / 2)));
233
255
  return { min, max: min + span };
234
- } else {
235
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
236
- }
256
+ } else return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
237
257
  }
238
258
 
239
259
  function updateScaleLabels(t, min, max) {
@@ -244,15 +264,8 @@ function updateScaleLabels(t, min, max) {
244
264
  function drawGraph(d, id, min, max, isTws, isHercules) {
245
265
  const svg = document.getElementById(id); if (!svg || d.length < 2) return;
246
266
  const w = 200, h = 40, range = max - min || 1;
247
- let grids = "";
248
- [0.25, 0.5, 0.75].forEach(p => { grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />`; });
249
-
250
- const minutes = CONFIG.graphs.historyMinutes;
251
- for (let m = 1; m < minutes; m++) {
252
- const x = w - (m / minutes) * w;
253
- grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(255,255,255,0.05)" stroke-width="0.5" />`;
254
- }
255
-
267
+ let grids = ""; [0.25, 0.5, 0.75].forEach(p => { grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />`; });
268
+ for (let m = 1; m < CONFIG.graphs.historyMinutes; m++) { const x = w - (m / CONFIG.graphs.historyMinutes) * w; grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(255,255,255,0.05)" stroke-width="0.5" />`; }
256
269
  let pD = "", cS = "";
257
270
  d.forEach((v, i) => {
258
271
  const x = (i/(CONFIG.graphs.samples-1))*w, y = h-(Math.max(0,Math.min(1,(v-min)/range))*h); pD += `${i===0?'M':'L'} ${x} ${y} `;
@@ -262,44 +275,75 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
262
275
  cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" stroke-width="${isHercules?3:2}" class="${isHercules?'line-hercules':''}" />`;
263
276
  }
264
277
  });
265
-
266
- const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#f1c40f' };
267
- const aP = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`;
278
+ const aP = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`, clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#f1c40f' };
268
279
  svg.innerHTML = isTws ? `${grids}<path d="${aP}" fill="rgba(241,196,15,0.12)" stroke="none" />${cS}` : `${grids}<path d="${aP}" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${clrs[id]}" stroke-width="1.5" />`;
269
280
  }
270
281
 
271
282
  // ==========================================================================
272
- // 9. EVENTI E INIZIALIZZAZIONE
283
+ // 8. EVENTI, INTERAZIONI E FOCUS MODE
273
284
  // ==========================================================================
285
+
286
+ function toggleFocusMode(type, element) {
287
+ const container = document.querySelector('.main-container');
288
+ const parentPanel = element.closest('.side-panel');
289
+ const isLeft = parentPanel.classList.contains('left-panel');
290
+
291
+ isFocusActive = !isFocusActive;
292
+
293
+ if (isFocusActive) {
294
+ container.classList.add('focus-active');
295
+ container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right');
296
+ parentPanel.classList.add('has-focus');
297
+ element.classList.add('is-focused');
298
+ blockNextClick = true; // Impedisce al click di rilascio del tocco lungo di chiudere subito
299
+ } else {
300
+ container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
301
+ document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus'));
302
+ document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
303
+ }
304
+ }
305
+
274
306
  ['stw', 'sog', 'tws', 'depth'].forEach(type => {
275
307
  const el = document.getElementById(type + '-graph').closest('.data-box');
308
+
309
+ // Doppio Click -> Toggle Hercules Zoom
276
310
  el.addEventListener('dblclick', (e) => {
277
- e.preventDefault(); graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
311
+ if (isFocusActive) return;
312
+ e.preventDefault();
313
+ graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
278
314
  localStorage.setItem('mode_' + type, graphModes[type]);
279
315
  el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
280
316
  });
317
+
318
+ // Gestione Pressione Prolungata (1s) -> Tactical Focus
319
+ const startPress = () => { if (!isFocusActive) pressTimer = setTimeout(() => toggleFocusMode(type, el), 1000); };
320
+ const cancelPress = () => { clearTimeout(pressTimer); };
321
+ el.addEventListener('mousedown', startPress); el.addEventListener('touchstart', startPress, {passive: true});
322
+ ['mouseup', 'mouseleave', 'touchend'].forEach(evt => el.addEventListener(evt, cancelPress));
323
+
324
+ // Click per uscire o per catturare il rilascio del tocco lungo
325
+ el.addEventListener('click', (e) => {
326
+ if (blockNextClick) { blockNextClick = false; return; }
327
+ if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el);
328
+ });
281
329
  });
282
330
 
283
- if (ui.hotspot) {
284
- ui.hotspot.addEventListener('click', (e) => {
285
- e.preventDefault(); const doc = document.documentElement, isF = document.fullscreenElement || document.webkitFullscreenElement;
286
- if (!isF) { if (doc.requestFullscreen) doc.requestFullscreen(); else if (doc.webkitRequestFullscreen) doc.webkitRequestFullscreen(); }
287
- else { if (document.exitFullscreen) document.exitFullscreen(); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); }
288
- });
289
- }
331
+ // Fullscreen (Hotspot)
332
+ if (ui.hotspot) { ui.hotspot.addEventListener('click', () => { const doc = document.documentElement, isF = document.fullscreenElement || document.webkitFullscreenElement; if (!isF) { if (doc.requestFullscreen) doc.requestFullscreen(); else if (doc.webkitRequestFullscreen) doc.webkitRequestFullscreen(); } else { if (document.exitFullscreen) document.exitFullscreen(); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); } }); }
290
333
 
334
+ // Allarmi Audio e Depth
335
+ 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'); }
336
+ function playBingBing() { if (!audioCtx) return; 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); }
337
+
338
+ // ==========================================================================
339
+ // 9. INIZIALIZZAZIONE
340
+ // ==========================================================================
291
341
  (function generateTicks() { 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); } } })();
292
342
 
293
343
  async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
294
344
  window.addEventListener('load', init);
295
345
 
296
- 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'); }
297
- function playBingBing() { if (!audioCtx) return; 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); }
298
-
346
+ // Simulatore (Triple click su Depth)
299
347
  ui.depth.closest('.data-box').addEventListener('click', (function() {
300
- let dC = 0, lC = 0;
301
- return function() {
302
- const n = Date.now(); if (n - lC < 500) dC++; else dC = 1; lC = n;
303
- if (dC === 3) { simulationMode = !simulationMode; if (simulationMode) { if (socket) socket.close(); ui.status.innerText = "SIM ATTIVO"; simInterval = setInterval(() => { processIncomingData("navigation.headingTrue", degToRad(Math.random()*360)); processIncomingData("navigation.speedOverGround", ktsToMs(8)); }, 200); } else location.reload(); dC = 0; }
304
- };
348
+ 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(); ui.status.innerText = "SIM ATTIVO"; simInterval = setInterval(() => { processIncomingData("navigation.headingTrue", degToRad(Math.random()*360)); processIncomingData("navigation.speedOverGround", ktsToMs(8)); }, 200); } else location.reload(); dC = 0; } };
305
349
  })());
package/index.html CHANGED
@@ -187,9 +187,27 @@
187
187
  <div class="label-row"><span class="label">TWD MEAN</span></div>
188
188
  <div class="value-with-compass">
189
189
  <svg class="mini-compass" viewBox="0 0 40 40">
190
- <circle cx="20" cy="20" r="18" fill="none" stroke="#333" stroke-width="2"/>
191
- <g stroke="#555" stroke-width="1"><line x1="20" y1="2" x2="20" y2="6"/><line x1="38" y1="20" x2="34" y2="20"/><line x1="20" y1="38" x2="20" y2="34"/><line x1="2" y1="20" x2="6" y2="20"/></g>
192
- <g id="twd-arrow" transform="rotate(0, 20, 20)"><path d="M20,5 L17,12 L23,12 Z" fill="#ffff00" /><line x1="20" y1="5" x2="20" y2="20" stroke="#ffff00" stroke-width="1" /></g>
190
+ <!-- Sfondo e bordo bussola -->
191
+ <circle cx="20" cy="20" r="19" fill="#151515" stroke="#444" stroke-width="1.5"/>
192
+
193
+ <!-- Etichetta Nord (N) -->
194
+ <text x="20" y="8" fill="#e74c3c" font-size="6" text-anchor="middle" font-weight="900" font-family="Arial">N</text>
195
+
196
+ <!-- Tacche cardinali (E, S, W) -->
197
+ <g stroke="#555" stroke-width="0.8">
198
+ <line x1="20" y1="11" x2="20" y2="13"/> <!-- Nord marker -->
199
+ <line x1="30" y1="20" x2="27" y2="20"/> <!-- Est -->
200
+ <line x1="20" y1="30" x2="20" y2="27"/> <!-- Sud -->
201
+ <line x1="10" y1="20" x2="13" y2="20"/> <!-- Ovest -->
202
+ </g>
203
+
204
+ <!-- Freccia TWD (Gialla, forma a punta di freccia aeronautica) -->
205
+ <g id="twd-arrow" transform="rotate(0, 20, 20)">
206
+ <path d="M20,10 L16,26 L20,23 L24,26 Z" fill="#ffff00" stroke="#000" stroke-width="0.5"/>
207
+ </g>
208
+
209
+ <!-- Perno centrale -->
210
+ <circle cx="20" cy="20" r="1.5" fill="#555" />
193
211
  </svg>
194
212
  <span class="value value-large" id="twd-avg">---&deg;</span>
195
213
  </div>
@@ -200,4 +218,3 @@
200
218
  <script src="app.js"></script>
201
219
  </body>
202
220
  </html>
203
- ---Fine Codice ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/style.css CHANGED
@@ -1,7 +1,16 @@
1
1
  /* ==========================================================================
2
- 1. BASE E STRUTTURA (GRID LAYOUT)
2
+ 1. BASE E STRUTTURA GENERALE (GRID LAYOUT)
3
3
  ========================================================================== */
4
- body { background-color: #000; color: #fff; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; margin: 0; padding: 0; height: 100vh; width: 100vw; overflow: hidden; }
4
+ body {
5
+ background-color: #000;
6
+ color: #fff;
7
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
8
+ margin: 0;
9
+ padding: 0;
10
+ height: 100vh;
11
+ width: 100vw;
12
+ overflow: hidden;
13
+ }
5
14
 
6
15
  .main-container {
7
16
  display: grid;
@@ -9,24 +18,23 @@ body { background-color: #000; color: #fff; font-family: -apple-system, BlinkMac
9
18
  height: 100%;
10
19
  padding: 5px;
11
20
  box-sizing: border-box;
12
- /* Default Desktop (es. 4:3 o schermi medi) */
21
+ gap: 8px;
22
+ /* Proporzioni Desktop Standard */
13
23
  grid-template-columns: 1fr 3.5fr 1fr;
14
24
  grid-template-rows: 100%;
15
- gap: 8px;
16
- transition: grid-template-columns 0.4s ease;
25
+ transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
17
26
  }
18
27
 
19
- /* OTTIMIZZAZIONE PER SCHERMI LARGHI (16:10, 16:9, ecc.) */
28
+ /* Ottimizzazione per schermi molto larghi (es. 16:10, 21:9) */
20
29
  @media (min-aspect-ratio: 1.6) {
21
30
  .main-container {
22
- /* Allarghiamo le colonne laterali per sfruttare lo spazio orizzontale */
23
31
  grid-template-columns: 1.4fr 3fr 1.4fr;
24
- gap: 15px; /* Aumentiamo anche il distacco per eleganza */
32
+ gap: 15px;
25
33
  }
26
34
  }
27
35
 
28
36
  /* ==========================================================================
29
- 2. PANNELLI LATERALI E RIQUADRI DATI
37
+ 2. PANNELLI E DATA-BOX
30
38
  ========================================================================== */
31
39
  .side-panel {
32
40
  display: flex;
@@ -37,12 +45,19 @@ body { background-color: #000; color: #fff; font-family: -apple-system, BlinkMac
37
45
  height: 100%;
38
46
  }
39
47
 
40
- .right-panel {
41
- align-items: flex-end;
42
- text-align: right;
48
+ .left-panel { grid-column: 1; }
49
+ .right-panel { grid-column: 3; align-items: flex-end; text-align: right; }
50
+
51
+ .center-panel {
52
+ grid-column: 2;
53
+ display: flex;
54
+ flex-direction: column;
55
+ align-items: center;
56
+ justify-content: center;
57
+ height: 100%;
58
+ overflow: hidden;
43
59
  }
44
60
 
45
- /* Riquadro singolo: agisce da contenitore per i font dinamici (cqh) */
46
61
  .data-box {
47
62
  position: relative;
48
63
  border-bottom: 1px solid #222;
@@ -51,69 +66,49 @@ body { background-color: #000; color: #fff; font-family: -apple-system, BlinkMac
51
66
  flex-direction: column;
52
67
  width: 100%;
53
68
  box-sizing: border-box;
69
+ container-type: size; /* Cruciale per il calcolo cqh */
54
70
  flex: 1;
55
- container-type: size;
56
- }
57
-
58
- /* Definizione altezze proporzionali (25% grafici, 16.6% dati semplici) */
59
- .data-box:nth-child(1),
60
- .data-box:nth-child(2) {
61
- height: 25vh;
62
71
  }
63
72
 
64
- .data-box:nth-child(3),
65
- .data-box:nth-child(4),
66
- .data-box:nth-child(5) {
67
- height: 16.666vh;
68
- }
73
+ /* Definizione altezze percentuali basate sul Viewport */
74
+ .data-box:nth-child(1), .data-box:nth-child(2) { height: 25vh; }
75
+ .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 16.666vh; }
69
76
 
70
77
  /* ==========================================================================
71
- 3. STRUMENTO CENTRALE (WIND GAUGE)
78
+ 3. TACTICAL FOCUS MODE (50/50 SPLIT)
72
79
  ========================================================================== */
73
- .center-panel {
74
- display: flex;
75
- flex-direction: column;
76
- align-items: center;
77
- justify-content: center;
78
- height: 100%;
79
- overflow: hidden;
80
- }
80
+ .focus-active { grid-template-columns: 1fr 1fr !important; }
81
+ .focus-active .side-panel:not(.has-focus) { display: none !important; }
81
82
 
82
- #wind-gauge {
83
- width: 100%;
84
- height: 100%;
85
- max-height: 100%;
86
- object-fit: contain;
87
- }
83
+ .focus-active.focus-side-left .side-panel.has-focus { grid-column: 1; }
84
+ .focus-active.focus-side-left .center-panel { grid-column: 2; }
85
+ .focus-active.focus-side-right .center-panel { grid-column: 1; }
86
+ .focus-active.focus-side-right .side-panel.has-focus { grid-column: 2; }
88
87
 
89
- /* ==========================================================================
90
- 4. TIPOGRAFIA DINAMICA (Basata su altezza riquadro - cqh)
91
- ========================================================================== */
92
- .label-row {
93
- display: flex;
94
- justify-content: space-between;
95
- align-items: baseline;
96
- margin-bottom: 2px;
97
- width: 100%;
98
- }
88
+ .focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
89
+ .focus-active .has-focus .data-box.is-focused { height: 100vh !important; border: none; background: rgba(255, 255, 255, 0.04); }
99
90
 
100
- .label {
101
- color: #666;
102
- font-size: 0.6rem;
103
- font-weight: bold;
104
- text-transform: uppercase;
105
- }
91
+ .focus-active .is-focused .value { font-size: clamp(3rem, 18cqh, 8rem) !important; margin-top: 10px; }
92
+ .focus-active .is-focused .scale-labels { font-size: 22px !important; min-width: 35px !important; }
106
93
 
107
- .unit {
108
- color: #888;
109
- font-size: 0.55rem;
110
- font-weight: bold;
94
+ /* Bussola Esplosa in Focus Mode */
95
+ .focus-active .is-focused .mini-compass {
96
+ width: 45vh !important;
97
+ height: 45vh !important;
111
98
  }
112
99
 
113
- /* Valore standard con grafico (STW, SOG, Depth, TWS) */
100
+ .focus-active .is-focused .sparkline path { stroke-width: 0.8px !important; }
101
+ .focus-active .is-focused .line-hercules { stroke-width: 1.2px !important; filter: drop-shadow(0 0 3px #ff0000); }
102
+
103
+ /* ==========================================================================
104
+ 4. TIPOGRAFIA DINAMICA (ELASTICA CQH)
105
+ ========================================================================== */
106
+ .label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
107
+ .label { color: #666; font-size: 0.6rem; font-weight: bold; text-transform: uppercase; }
108
+ .unit { color: #888; font-size: 0.55rem; font-weight: bold; }
109
+
114
110
  .value {
115
111
  color: #fff;
116
- /* 22% dell'altezza del box, minimo 1.2rem */
117
112
  font-size: clamp(1.2rem, 22cqh, 3rem);
118
113
  font-weight: 600;
119
114
  line-height: 0.9;
@@ -122,40 +117,30 @@ body { background-color: #000; color: #fff; font-family: -apple-system, BlinkMac
122
117
  padding-bottom: 5px;
123
118
  }
124
119
 
125
- /* Valore grande senza grafico (Heading, COG, Mean) centrato verticalmente */
126
120
  .value-large {
127
121
  margin-top: auto;
128
122
  margin-bottom: auto;
129
- /* 35% dell'altezza del box, minimo 1.5rem */
130
123
  font-size: clamp(1.5rem, 35cqh, 4rem);
131
124
  line-height: 0.85;
132
125
  }
133
126
 
134
- /* ==========================================================================
135
- 5. LAYOUT SPECIALI (TACK E BUSSOLINA TWD)
136
- ========================================================================== */
137
- /* Box TACK (Doppio valore HDG/COG) */
127
+ /* TACK Layout - Centratura verticale e compattezza */
138
128
  .dual-value-container {
139
129
  display: flex;
140
130
  justify-content: space-between;
141
- align-items: flex-end;
131
+ align-items: center;
142
132
  width: 100%;
133
+ /* La "molla" auto sopra e sotto lo tiene al centro esatto del box */
143
134
  margin-top: auto;
144
- margin-bottom: auto; /* Centratura verticale */
135
+ margin-bottom: auto;
145
136
  padding-bottom: 5px;
146
137
  }
147
138
 
148
139
  .dual-value-col {
149
140
  display: flex;
150
141
  flex-direction: column;
151
- justify-content: space-between;
152
142
  width: 48%;
153
- height: 100%;
154
- }
155
-
156
- .dual-value-col.right-col {
157
- align-items: flex-end;
158
- text-align: right;
143
+ /* Rimosso height 100% per non farlo allungare fino al titolo */
159
144
  }
160
145
 
161
146
  .dual-label {
@@ -163,39 +148,47 @@ body { background-color: #000; color: #fff; font-family: -apple-system, BlinkMac
163
148
  font-size: 0.55rem;
164
149
  font-weight: bold;
165
150
  text-transform: uppercase;
166
- margin-bottom: auto;
151
+ margin-bottom: 2px; /* Distanza fissa e piccola dal numero sottostante */
167
152
  }
168
153
 
169
154
  .value.dual-val {
170
- font-size: clamp(1rem, 20cqh, 1.8rem);
155
+ font-size: clamp(1rem, 22cqh, 1.8rem);
171
156
  padding-bottom: 0;
157
+ line-height: 0.9;
172
158
  }
173
159
 
174
- /* Box TWD con Mini-Compass */
160
+ /* BUSSOLA TWD: Centrata e proporzionata */
175
161
  .value-with-compass {
176
162
  display: flex;
177
163
  justify-content: space-between;
178
164
  align-items: center;
179
165
  width: 100%;
180
166
  margin-top: auto;
181
- margin-bottom: auto; /* Centratura verticale */
167
+ margin-bottom: auto;
168
+ gap: 8px;
182
169
  }
183
170
 
184
171
  .mini-compass {
185
- width: 38px;
186
- height: 38px;
187
- background: rgba(0,0,0,0.2);
172
+ /* Impostata a 60% dell'altezza come da tua prova */
173
+ width: clamp(45px, 60cqh, 120px);
174
+ height: clamp(45px, 60cqh, 120px);
175
+ background: #000;
188
176
  border-radius: 50%;
189
177
  flex-shrink: 0;
178
+ border: 1.5px solid #333;
179
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.8);
180
+ transition: all 0.4s ease;
190
181
  }
191
182
 
192
183
  .value-with-compass .value-large {
193
- margin-top: 0;
194
- margin-bottom: 0;
184
+ margin: 0;
185
+ flex-grow: 1;
186
+ text-align: right;
187
+ font-size: clamp(1.2rem, 35cqh, 4rem);
195
188
  }
196
189
 
197
190
  /* ==========================================================================
198
- 6. GRAFICI STORICI (SPARKLINES)
191
+ 5. GRAFICI E SCALE
199
192
  ========================================================================== */
200
193
  .graph-wrapper {
201
194
  position: relative;
@@ -205,152 +198,63 @@ body { background-color: #000; color: #fff; font-family: -apple-system, BlinkMac
205
198
  margin-top: 4px;
206
199
  display: flex;
207
200
  align-items: stretch;
208
- gap: 4px; /* Ridotto: spazio minimo tra numeri e grafico */
209
- }
210
-
211
- .sparkline {
212
- flex-grow: 1;
213
- height: 100%;
214
- background: rgba(255, 255, 255, 0.03);
215
- border-radius: 4px;
201
+ gap: 4px;
216
202
  }
203
+ .sparkline { flex-grow: 1; height: 100%; background: rgba(255, 255, 255, 0.03); border-radius: 4px; }
204
+ .sparkline path { stroke-width: 1px !important; }
217
205
 
218
206
  .scale-labels {
219
207
  display: flex;
220
208
  flex-direction: column;
221
209
  justify-content: space-between;
222
- font-size: 10px; /* Aumentato di ~1.5pt */
223
- color: #888; /* Leggermente più chiaro per leggibilità */
210
+ font-size: 10px;
211
+ color: #666;
224
212
  font-weight: bold;
225
- min-width: 16px; /* Allargato per il nuovo font */
213
+ min-width: 16px;
226
214
  height: 100%;
227
215
  line-height: 1;
228
- }
229
-
230
- /* SIMMETRIA INTERNA:
231
- Pannello SX -> Scala a DESTRA (vicino al vento)
232
- Pannello DX -> Scala a SINISTRA (vicino al vento)
233
- */
234
- .left-panel .scale-labels {
235
- order: 2;
236
- text-align: left;
237
- }
238
- .left-panel .sparkline {
239
- order: 1;
240
- }
241
-
242
- .right-panel .scale-labels {
243
- order: 1;
244
- text-align: right;
245
- }
246
- .right-panel .sparkline {
247
- order: 2;
248
- }
249
-
250
- /* Colori linee grafici */
251
- #stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.15); }
252
- #sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.15); }
253
- #depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.15); }
254
- #tws-graph { stroke: #f1c40f; fill: rgba(241, 196, 15, 0.15); }
255
-
256
- /* Effetto bagliore ROSSO per la linea Hercules */
257
- .line-hercules {
258
- filter: drop-shadow(0 0 5px #ff0000); /* Glow Rosso */
259
- stroke-width: 2.2px !important;
260
- }
261
-
262
- /* Sfondo speciale ROSSO SCURO per il box in modalità Hercules */
263
- .box-hercules {
264
- background: rgba(255, 0, 0, 0.08) !important; /* Una punta di rosso nel fondo */
265
- border-radius: 10px;
266
216
  transition: all 0.3s ease;
267
217
  }
268
218
 
269
- .box-hercules .sparkline {
270
- background: rgba(255, 0, 0, 0.05); /* Sfondo grafico leggermente rosso */
271
- }
219
+ .left-panel .scale-labels { order: 2; text-align: left; }
220
+ .left-panel .sparkline { order: 1; }
221
+ .right-panel .scale-labels { order: 1; text-align: right; }
222
+ .right-panel .sparkline { order: 2; }
272
223
 
273
- /* Indicatore "H" più evidente */
274
- .box-hercules::after {
275
- content: "HERCULES";
276
- position: absolute;
277
- bottom: 4px;
278
- right: 35px;
279
- font-size: 7px;
280
- color: #ff4444;
281
- font-weight: 900;
282
- letter-spacing: 1px;
283
- opacity: 0.8;
284
- }
224
+ #stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
225
+ #sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
226
+ #depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
227
+ #tws-graph { stroke: #f1c40f; fill: rgba(241, 196, 15, 0.12); }
285
228
 
286
229
  /* ==========================================================================
287
- 7. STATI, ALLARMI E ANIMAZIONI
230
+ 6. HERCULES E ANIMAZIONI
288
231
  ========================================================================== */
289
- #status {
290
- position: absolute;
291
- top: 5px;
292
- right: 15px;
293
- font-size: 0.5rem;
294
- text-transform: uppercase;
295
- z-index: 1000;
296
- }
232
+ .line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
233
+ .box-hercules { background: rgba(255, 0, 0, 0.08) !important; transition: all 0.3s ease; }
234
+ .box-hercules::after { content: "HERCULES"; position: absolute; bottom: 4px; right: 35px; font-size: 7px; color: #ff4444; font-weight: 900; letter-spacing: 1px; opacity: 0.8; }
297
235
 
236
+ #status { position: absolute; top: 5px; right: 15px; font-size: 0.5rem; text-transform: uppercase; z-index: 1000; }
298
237
  .online { color: #2ecc71; opacity: 0.5; }
299
238
  .offline { color: #e74c3c; font-weight: bold; }
300
239
 
301
- /* Fluidità lancette SVG */
302
- #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow {
303
- transition: all 0.6s cubic-bezier(0.1, 0.7, 0.1, 1);
304
- }
305
-
240
+ #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow { transition: all 0.6s cubic-bezier(0.1, 0.7, 0.1, 1); }
306
241
  #leeway-mask-rect { transition: none; }
307
-
308
- /* Allarmi Profondità */
309
242
  .alarm-warning { color: #f1c40f !important; }
310
- .alarm-danger {
311
- color: #e74c3c !important;
312
- font-weight: 900;
313
- animation: blink-unstable 1s infinite;
314
- }
315
-
316
- /* Lampeggio dati instabili (MEAN) */
317
- @keyframes blink-unstable {
318
- 0% { opacity: 1; }
319
- 50% { opacity: 0.3; }
320
- 100% { opacity: 1; }
321
- }
243
+ .alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
244
+ @keyframes blink-unstable { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
245
+ .unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
322
246
 
323
- .unstable-data {
324
- animation: blink-unstable 1.5s infinite ease-in-out;
325
- color: #f39c12 !important;
326
- }
247
+ #wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
327
248
 
328
249
  /* ==========================================================================
329
- 8. RESPONSIVE LAYOUT (max-aspect-ratio: 40/50)
250
+ 7. RESPONSIVE (PORTRAIT)
330
251
  ========================================================================== */
331
252
  @media (max-aspect-ratio: 0.9 / 1) {
332
- .main-container {
333
- grid-template-columns: 1fr 1fr;
334
- grid-template-rows: 1fr 1fr;
335
- }
336
-
337
- /* Vento sopra a tutta larghezza */
338
- .center-panel {
339
- grid-column: 1 / span 2;
340
- grid-row: 1;
341
- height: 50vh;
342
- }
343
-
344
- /* Colonne dati sotto affiancate */
345
- .side-panel.left-panel { grid-column: 1; grid-row: 2; height: 50vh; }
346
- .side-panel.right-panel { grid-column: 2; grid-row: 2; height: 50vh; }
347
-
348
- #wind-gauge { max-height: 100%; width: auto; }
349
-
350
- /* Altezze box dimezzate in modalità responsive */
253
+ .main-container { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
254
+ .center-panel { grid-column: 1 / span 2; grid-row: 1; height: 50vh; }
255
+ .left-panel { grid-column: 1; grid-row: 2; height: 50vh; }
256
+ .right-panel { grid-column: 2; grid-row: 2; height: 50vh; }
351
257
  .data-box:nth-child(1), .data-box:nth-child(2) { height: 12.5vh; }
352
258
  .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 8.333vh; }
353
-
354
- /* Font più protettivi per schermi piccoli */
355
259
  .value-large { font-size: clamp(1.2rem, 35cqh, 2.5rem); }
356
260
  }