@sailingrotevista/rotevista-dash 1.0.23 → 1.0.25

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 +78 -118
  2. package/index.html +33 -64
  3. package/package.json +1 -1
  4. package/style.css +125 -270
package/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ==========================================================================
2
- // 1. CONFIGURAZIONE E DEFAULT
2
+ // 1. CONFIGURAZIONE E STATO
3
3
  // ==========================================================================
4
4
  let CONFIG = {
5
5
  alarms: { depthDanger: 2.5, depthWarning: 5.0 },
@@ -18,13 +18,11 @@ const RENDER_INTERVAL_MS = 1000;
18
18
  const TIMEOUT_MS = 5000;
19
19
  const SIM_SAMPLE_INTERVAL = 1000;
20
20
 
21
- // ==========================================================================
22
- // 2. STATO GLOBALE E UI
23
- // ==========================================================================
24
21
  let simulationMode = false;
25
22
  let socket, renderInterval, simInterval;
26
23
  let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
27
24
  let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
25
+ let pressTimer, isFocusActive = false, blockNextClick = false;
28
26
 
29
27
  const graphModes = {
30
28
  stw: localStorage.getItem('mode_stw') || 'standard',
@@ -49,13 +47,13 @@ const ui = {
49
47
  };
50
48
 
51
49
  // ==========================================================================
52
- // 3. CARICAMENTO CONFIGURAZIONE
50
+ // 3. LOGICA DI COMUNICAZIONE E CALCOLO
53
51
  // ==========================================================================
52
+
54
53
  async function fetchServerConfig() {
55
54
  if (!window.location.protocol.includes("http")) return;
56
55
  const pluginID = 'rotevista-dash';
57
56
  const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
58
-
59
57
  for (let url of possibleUrls) {
60
58
  try {
61
59
  const response = await fetch(url);
@@ -63,28 +61,19 @@ async function fetchServerConfig() {
63
61
  const data = await response.json();
64
62
  const actual = data.configuration || data;
65
63
  if (actual && typeof actual === 'object') {
66
- const parseNumbers = (obj) => {
67
- for (let k in obj) {
68
- if (typeof obj[k] === 'object') parseNumbers(obj[k]);
69
- else if (!isNaN(obj[k]) && obj[k] !== "") obj[k] = parseFloat(obj[k]);
70
- }
71
- };
64
+ const parseNumbers = (obj) => { for (let k in obj) { if (typeof obj[k] === 'object') parseNumbers(obj[k]); else if (!isNaN(obj[k]) && obj[k] !== "") obj[k] = parseFloat(obj[k]); } };
72
65
  parseNumbers(actual);
73
66
  if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
74
67
  if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
75
- if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging }; // Mapping averaging -> averages
68
+ if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
76
69
  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);
78
- return;
70
+ console.log("Dashboard: Configurazione caricata."); return;
79
71
  }
80
72
  }
81
73
  } catch (e) { }
82
74
  }
83
75
  }
84
76
 
85
- // ==========================================================================
86
- // 4. MATEMATICA VETTORIALE
87
- // ==========================================================================
88
77
  function radToDeg(rad) { return rad * (180 / Math.PI); }
89
78
  function degToRad(deg) { return deg * (Math.PI / 180); }
90
79
  function msToKts(ms) { return ms * 1.94384; }
@@ -92,43 +81,33 @@ function ktsToMs(kts) { return kts / 1.94384; }
92
81
  function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
93
82
 
94
83
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
95
- const now = Date.now();
96
- const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
84
+ const now = Date.now(); const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
97
85
  if (validData.length === 0) return null;
98
- let sSin = 0, sCos = 0;
99
- validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
86
+ let sSin = 0, sCos = 0; validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
100
87
  let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
101
- let timeSpan = validData[validData.length - 1].time - validData[0].time;
102
- let isStable = (timeSpan >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
88
+ let isStable = (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
103
89
  let avgDeg = Math.round(radToDeg(Math.atan2(sSin, sCos)));
104
90
  return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
105
91
  }
106
92
 
107
- // ==========================================================================
108
- // 5. GESTIONE DATI IN INGRESSO
109
- // ==========================================================================
110
93
  function processIncomingData(path, val) {
111
94
  const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
112
95
  if (path === "navigation.headingTrue") { store.smoothBuf.hdg.push({ val: val, time: now }); store.longBuf.hdg.push({ val: val, time: now }); }
113
96
  if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
114
97
  if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
115
98
  if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
116
-
117
99
  if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
118
- let twdRad = 0;
119
- if (path === "environment.wind.directionTrue") twdRad = val;
120
- else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) {
121
- twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
122
- if (twdRad < 0) twdRad += (2 * Math.PI);
123
- } else return;
124
- store.smoothBuf.twd.push({ val: twdRad, time: now });
125
- store.longBuf.twd.push({ val: twdRad, time: now });
100
+ let twdRad = 0; if (path === "environment.wind.directionTrue") twdRad = val;
101
+ else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) { twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI); if (twdRad < 0) twdRad += (2 * Math.PI); }
102
+ else return;
103
+ store.smoothBuf.twd.push({ val: twdRad, time: now }); store.longBuf.twd.push({ val: twdRad, time: now });
126
104
  }
127
105
  }
128
106
 
129
107
  // ==========================================================================
130
- // 6. RENDER LOOP
108
+ // 4. RENDERING LOOP E GRAFICA
131
109
  // ==========================================================================
110
+
132
111
  function startDisplayLoop() {
133
112
  renderInterval = setInterval(() => {
134
113
  const now = Date.now();
@@ -159,23 +138,12 @@ function startDisplayLoop() {
159
138
  awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true),
160
139
  twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true),
161
140
  twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
162
-
163
- const upUI = (el, obj) => {
164
- if (!obj) { el.innerHTML = "---&deg;"; el.classList.remove('unstable-data'); }
165
- else {
166
- el.innerHTML = `${obj.val.toString().padStart(3, '0')}&deg;`;
167
- if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data'); else el.classList.add('unstable-data');
168
- }
169
- };
141
+ const upUI = (el, obj) => { if (!obj) { el.innerHTML = "---&deg;"; el.classList.remove('unstable-data'); } else { el.innerHTML = `${obj.val.toString().padStart(3, '0')}&deg;`; if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data'); else el.classList.add('unstable-data'); } };
170
142
  upUI(ui.hdg, hObj); upUI(ui.cog, cObj); upUI(ui.awaAvg, awObj); upUI(ui.twaAvg, twObj); upUI(ui.twdAvg, twdObj);
171
-
172
143
  if (hObj && twObj && hObj.val !== null) {
173
- let tA = twObj.val * 2;
174
- ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
144
+ let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
175
145
  if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
176
- let tStable = (hObj.stable && twObj.stable) || (curSog < CONFIG.averages.minSpeed);
177
- if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
178
- else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
146
+ if (hObj.stable && twObj.stable || curSog < CONFIG.averages.minSpeed) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); } else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
179
147
  }
180
148
  if (twdObj) { curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); }
181
149
  lastAvgUIUpdate = now;
@@ -185,37 +153,13 @@ function startDisplayLoop() {
185
153
  }, RENDER_INTERVAL_MS);
186
154
  }
187
155
 
188
- // ==========================================================================
189
- // 7. CONNESSIONE SIGNALK (subscribe=self)
190
- // ==========================================================================
191
- function connect() {
192
- if (simulationMode) return;
193
- let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
194
- try {
195
- socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
196
- socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; };
197
- socket.onmessage = (e) => { const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
198
- socket.onclose = () => !simulationMode && setTimeout(connect, 5000);
199
- } catch (e) { setTimeout(connect, 5000); }
200
- }
201
-
202
- // ==========================================================================
203
- // 8. FUNZIONI GRAFICHE (Hercules Mode)
204
- // ==========================================================================
205
156
  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
157
 
207
158
  function manageHistory(t, v) {
208
159
  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);
160
+ const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
161
+ 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; }
162
+ const mode = graphModes[t], cfg = calculateScale(t, store.histories[t], mode);
219
163
  const box = document.getElementById(t + '-graph').closest('.data-box');
220
164
  if (mode === 'hercules') box.classList.add('box-hercules'); else box.classList.remove('box-hercules');
221
165
  updateScaleLabels(t, cfg.min, cfg.max);
@@ -226,14 +170,9 @@ function calculateScale(type, data, mode) {
226
170
  const s = CONFIG.scales[type] || { stdMax: 12, hercSpan: 4, step: 2 };
227
171
  let aMin = Math.min(...data), aMax = Math.max(...data);
228
172
  if (mode === 'hercules') {
229
- let avg = (aMin + aMax) / 2;
230
- let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin));
231
- if (span % 2 !== 0) span += 1;
232
- let min = Math.max(0, Math.floor(avg - (span / 2)));
233
- return { min, max: min + span };
234
- } else {
235
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
236
- }
173
+ let avg = (aMin + aMax) / 2; let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin)); if (span % 2 !== 0) span += 1;
174
+ let min = Math.max(0, Math.floor(avg - (span / 2))); return { min, max: min + span };
175
+ } else return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
237
176
  }
238
177
 
239
178
  function updateScaleLabels(t, min, max) {
@@ -242,64 +181,85 @@ function updateScaleLabels(t, min, max) {
242
181
  }
243
182
 
244
183
  function drawGraph(d, id, min, max, isTws, isHercules) {
245
- const svg = document.getElementById(id); if (!svg || d.length < 2) return;
184
+ const svg = document.getElementById(id);
185
+ if (!svg || d.length < 2) return;
186
+
246
187
  const w = 200, h = 40, range = max - min || 1;
188
+ const lClass = isHercules ? 'line-hercules' : '';
189
+
190
+ // 1. Griglie
247
191
  let grids = "";
248
192
  [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;
193
+ for (let m = 1; m < CONFIG.graphs.historyMinutes; m++) {
194
+ const x = w - (m / CONFIG.graphs.historyMinutes) * w;
253
195
  grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(255,255,255,0.05)" stroke-width="0.5" />`;
254
196
  }
255
-
197
+
198
+ // 2. Calcolo Percorsi
256
199
  let pD = "", cS = "";
257
200
  d.forEach((v, i) => {
258
- 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} `;
201
+ const x = (i/(CONFIG.graphs.samples-1))*w, y = h-(Math.max(0,Math.min(1,(v-min)/range))*h);
202
+ pD += `${i===0?'M':'L'} ${x} ${y} `;
203
+
204
+ // Logica segmenti colorati per TWS
259
205
  if (isTws && i > 0) {
260
206
  const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
261
- let c = "#f1c40f"; if (v >= CONFIG.graphs.reef2) c = "#e74c3c"; else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
262
- cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" stroke-width="${isHercules?3:2}" class="${isHercules?'line-hercules':''}" />`;
207
+ let c = "#f1c40f";
208
+ if (v >= CONFIG.graphs.reef2) c = "#e74c3c";
209
+ else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
210
+ // Lo spessore ora è rimosso da qui e gestito dal CSS tramite la classe
211
+ cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${lClass}" />`;
263
212
  }
264
213
  });
265
214
 
266
- const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#f1c40f' };
215
+ // 3. Rendering finale
267
216
  const aP = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`;
268
- 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" />`;
217
+ const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#f1c40f' };
218
+
219
+ if (isTws) {
220
+ svg.innerHTML = `${grids}<path d="${aP}" fill="rgba(241,196,15,0.12)" stroke="none" />${cS}`;
221
+ } else {
222
+ svg.innerHTML = `${grids}<path d="${aP}" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${lClass}" fill="none" stroke="${clrs[id]}" />`;
223
+ }
269
224
  }
270
225
 
271
226
  // ==========================================================================
272
- // 9. EVENTI E INIZIALIZZAZIONE
227
+ // 5. EVENTI E INTERAZIONI
273
228
  // ==========================================================================
229
+
230
+ function toggleFocusMode(type, element) {
231
+ const container = document.querySelector('.main-container');
232
+ const parentPanel = element.closest('.side-panel');
233
+ const isLeft = parentPanel.classList.contains('left-panel');
234
+ isFocusActive = !isFocusActive;
235
+ if (isFocusActive) { container.classList.add('focus-active'); container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right'); parentPanel.classList.add('has-focus'); element.classList.add('is-focused'); blockNextClick = true; }
236
+ else { container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right'); document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus')); document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused')); }
237
+ }
238
+
274
239
  ['stw', 'sog', 'tws', 'depth'].forEach(type => {
275
240
  const el = document.getElementById(type + '-graph').closest('.data-box');
276
- el.addEventListener('dblclick', (e) => {
277
- e.preventDefault(); graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
278
- localStorage.setItem('mode_' + type, graphModes[type]);
279
- el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
280
- });
241
+ el.addEventListener('dblclick', (e) => { if (isFocusActive) return; e.preventDefault(); graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200); });
242
+ const startPress = (e) => { if (!isFocusActive) pressTimer = setTimeout(() => { toggleFocusMode(type, el); if (e.cancelable) e.preventDefault(); }, 1000); };
243
+ const cancelPress = () => { clearTimeout(pressTimer); };
244
+ el.addEventListener('mousedown', startPress); el.addEventListener('touchstart', startPress, {passive: false});
245
+ ['mouseup', 'mouseleave', 'touchend'].forEach(evt => el.addEventListener(evt, cancelPress));
246
+ el.addEventListener('click', (e) => { if (blockNextClick) { blockNextClick = false; return; } if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); });
247
+ el.addEventListener('contextmenu', (e) => { if (isFocusActive) e.preventDefault(); });
281
248
  });
282
249
 
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
- }
290
-
291
- (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); } } })();
250
+ 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(); } }); }
292
251
 
252
+ // ==========================================================================
253
+ // 6. INIZIALIZZAZIONE
254
+ // ==========================================================================
255
+ (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); } } })();
256
+ function connect() { if (simulationMode) return; let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp; try { socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`); socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; }; socket.onmessage = (e) => { const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); }; socket.onclose = () => !simulationMode && setTimeout(connect, 5000); } catch (e) { setTimeout(connect, 5000); } }
293
257
  async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
294
258
  window.addEventListener('load', init);
295
-
296
259
  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
260
  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
261
 
262
+ // Simulatore (Triple click su Depth)
299
263
  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
- };
264
+ 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
265
  })());
package/index.html CHANGED
@@ -16,44 +16,41 @@
16
16
 
17
17
  <div class="main-container">
18
18
 
19
- <!-- ======================================================= -->
20
- <!-- COLONNA SINISTRA: Dati Rotta e Velocità -->
21
- <!-- ======================================================= -->
19
+ <!-- COLONNA SINISTRA: Dati Rotta e Velocità -->
22
20
  <div class="side-panel left-panel">
23
-
24
- <!-- STW: Velocità attraverso l'acqua -->
21
+ <!-- STW con Sparkline -->
25
22
  <div class="data-box">
26
23
  <div class="label-row"><span class="label">STW</span><span class="unit">kts</span></div>
27
24
  <span class="value" id="stw">0.0</span>
28
25
  <div class="graph-wrapper">
29
26
  <svg class="sparkline" id="stw-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
30
- <div class="scale-labels right" id="stw-scale"></div>
27
+ <div class="scale-labels" id="stw-scale"></div>
31
28
  </div>
32
29
  </div>
33
30
 
34
- <!-- SOG: Velocità sul fondo (GPS) -->
31
+ <!-- SOG con Sparkline -->
35
32
  <div class="data-box">
36
33
  <div class="label-row"><span class="label">SOG</span><span class="unit">kts</span></div>
37
34
  <span class="value" id="sog">0.0</span>
38
35
  <div class="graph-wrapper">
39
36
  <svg class="sparkline" id="sog-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
40
- <div class="scale-labels right" id="sog-scale"></div>
37
+ <div class="scale-labels" id="sog-scale"></div>
41
38
  </div>
42
39
  </div>
43
40
 
44
- <!-- HEADING: Prua Bussola (Media 60s) -->
41
+ <!-- HEADING MEAN -->
45
42
  <div class="data-box">
46
43
  <div class="label-row"><span class="label">HEADING MEAN</span></div>
47
44
  <span class="value value-large" id="hdg">000&deg;</span>
48
45
  </div>
49
46
 
50
- <!-- COG: Rotta sul fondo (Media 60s) -->
47
+ <!-- COG MEAN -->
51
48
  <div class="data-box">
52
49
  <div class="label-row"><span class="label">COG MEAN</span></div>
53
50
  <span class="value value-large" id="cog">000&deg;</span>
54
51
  </div>
55
52
 
56
- <!-- TACK: Calcolo mure opposte (HDG e COG previsti) -->
53
+ <!-- TACK: Previsione mure opposte -->
57
54
  <div class="data-box">
58
55
  <div class="label-row"><span class="label">TACK</span></div>
59
56
  <div class="dual-value-container">
@@ -69,135 +66,107 @@
69
66
  </div>
70
67
  </div>
71
68
 
72
- <!-- ======================================================= -->
73
- <!-- CENTRO: Strumento Vento SVG (Ingrandito e Ottimizzato) -->
74
- <!-- ======================================================= -->
69
+ <!-- CENTRO: Strumento Vento SVG -->
75
70
  <div class="center-panel">
76
- <!-- ViewBox ottimizzato (35 38 330 375) per ingrandire il diametro del quadrante -->
77
71
  <svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
78
72
  <defs>
79
- <!-- Gradienti e Maschere -->
80
73
  <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" /><stop offset="100%" style="stop-color:#888888;stop-opacity:1" /></linearGradient>
81
74
  <linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" /><stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" /><stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" /><stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" /><stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" /></linearGradient>
82
75
  <clipPath id="leeway-clip"><rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" /></clipPath>
83
-
84
- <!-- Filtro Glow per l'area di interazione centrale -->
85
- <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
86
- <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
87
- </filter>
76
+ <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" /></filter>
88
77
  </defs>
89
78
 
90
- <!-- Sfondo circolare del quadrante -->
91
- <circle cx="200" cy="200" r="160" fill="#050505" />
92
- <circle cx="200" cy="200" r="125" fill="#121212" />
93
-
94
- <!-- Settori Vento (Rosso/Verde/Arancio) -->
79
+ <circle cx="200" cy="200" r="160" fill="#050505" /><circle cx="200" cy="200" r="125" fill="#121212" />
95
80
  <path d="M 82.0 101.0 A 154 154 0 0 1 142.3 57.2" fill="none" stroke="#ff0000" stroke-width="12" opacity="1"/>
96
81
  <path d="M 257.7 57.2 A 154 154 0 0 1 318.0 101.0" fill="none" stroke="#00ff00" stroke-width="12" opacity="1"/>
97
82
  <path d="M 265.1 339.6 A 154 154 0 0 1 134.9 339.6" fill="none" stroke="#ff8800" stroke-width="12" opacity="1"/>
98
83
 
99
- <!-- Tacche (Generate da Javascript) e Etichette Gradi -->
100
84
  <g id="ticks"></g>
101
85
  <g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="hanging" font-family="Arial" font-weight="bold">
102
- <text font-size="16" transform="translate(200, 65)">0</text>
103
- <text font-size="16" transform="translate(335, 200) rotate(90)">90</text>
104
- <text font-size="16" transform="translate(65, 200) rotate(-90)">90</text>
105
- <text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
86
+ <text font-size="16" transform="translate(200, 65)">0</text><text font-size="16" transform="translate(335, 200) rotate(90)">90</text><text font-size="16" transform="translate(65, 200) rotate(-90)">90</text><text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
106
87
  <text font-size="11" transform="translate(267.5, 83) rotate(30)">30</text><text font-size="11" transform="translate(317, 132.5) rotate(60)">60</text>
107
88
  <text font-size="11" transform="translate(317, 267.5) rotate(120)">120</text><text font-size="11" transform="translate(267.5, 317) rotate(150)">150</text>
108
89
  <text font-size="11" transform="translate(132.5, 83) rotate(-30)">30</text><text font-size="11" transform="translate(83, 132.5) rotate(-60)">60</text>
109
90
  <text font-size="11" transform="translate(83, 267.5) rotate(-120)">120</text><text font-size="11" transform="translate(132.5, 317) rotate(-150)">150</text>
110
91
  </g>
111
92
 
112
- <!-- Puntatore Track (Blu) -->
113
93
  <g id="track-pointer" transform="rotate(0, 200, 200)"><path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" /></g>
114
-
115
- <!-- Pulsante Fullscreen Hotspot (Disco cliccabile al centro) -->
116
94
  <circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
117
-
118
- <!-- Icona Barca (Centrata visivamente con traslazione Y+5) -->
119
95
  <path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z" fill="url(#axiom-grad)" transform="translate(0, 5)" style="pointer-events: none;" />
120
96
 
121
- <!-- Display Velocità Vento Apparente (Al centro sotto la barca) -->
122
97
  <g id="aws-display-group" transform="translate(200, 265)">
123
98
  <text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
124
99
  <text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
125
100
  </g>
126
-
127
- <!-- Lancette AWA (A) e TWA (T) -->
128
101
  <g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.85"><path d="M200,80 L211,95 L200,145 L189,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" /><text x="200" y="102" fill="#000" font-size="11" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text></g>
129
102
  <g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.85"><path d="M200,90 L206,98 L200,125 L194,98 Z" fill="#ffff00" stroke="#000" stroke-width="0.8" /><text x="200" y="104" fill="#000" font-size="8" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text></g>
130
103
 
131
- <!-- Barra LEEWAY / DRIFT (Posizionata più in alto a Y=372) -->
132
104
  <g transform="translate(75, 395)">
133
105
  <text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0&deg;</text>
134
- <rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
135
- <rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
106
+ <rect x="0" y="0" width="250" height="12" fill="#222" rx="3" /><rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
136
107
  <g stroke="#555" stroke-width="1">
137
- <line x1="0" y1="-2" x2="0" y2="14" /><line x1="31.25" y1="2" x2="31.25" y2="10" /><line x1="62.5" y1="2" x2="62.5" y2="10" />
138
- <line x1="93.75" y1="3" x2="93.75" y2="9" /><line x1="125" y1="-2" x2="125" y2="14" /><line x1="156.25" y1="3" x2="156.25" y2="9" />
139
- <line x1="187.5" y1="2" x2="187.5" y2="10" /><line x1="218.75" y1="2" x2="218.75" y2="10" /><line x1="250" y1="-2" x2="250" y2="14" />
140
- </g>
141
- <g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
142
- <text x="0" y="24">-20&deg;</text><text x="62.5" y="24">-10</text><text x="125" y="24">0&deg;</text><text x="187.5" y="24">10</text><text x="250" y="24">20&deg;</text>
108
+ <line x1="0" y1="-2" x2="0" y2="14" /><line x1="31.25" y1="2" x2="31.25" y2="10" /><line x1="62.5" y1="2" x2="62.5" y2="10" /><line x1="93.75" y1="3" x2="93.75" y2="9" /><line x1="125" y1="-2" x2="125" y2="14" /><line x1="156.25" y1="3" x2="156.25" y2="9" /><line x1="187.5" y1="2" x2="187.5" y2="10" /><line x1="218.75" y1="2" x2="218.75" y2="10" /><line x1="250" y1="-2" x2="250" y2="14" />
143
109
  </g>
110
+ <g fill="#555" font-size="8" text-anchor="middle" font-weight="bold"><text x="0" y="24">-20&deg;</text><text x="62.5" y="24">-10</text><text x="125" y="24">0&deg;</text><text x="187.5" y="24">10</text><text x="250" y="24">20&deg;</text></g>
144
111
  </g>
145
112
  </svg>
146
113
  </div>
147
114
 
148
- <!-- ======================================================= -->
149
- <!-- COLONNA DESTRA: Profondità e Vento Reale -->
150
- <!-- ======================================================= -->
115
+ <!-- COLONNA DESTRA: Dati Vento e Profondità -->
151
116
  <div class="side-panel right-panel">
152
-
153
- <!-- DEPTH: Profondità sotto il trasduttore -->
117
+ <!-- DEPTH -->
154
118
  <div class="data-box">
155
119
  <div class="label-row"><span class="unit">m</span><span class="label">DEPTH</span></div>
156
120
  <span class="value" id="depth">--.-</span>
157
121
  <div class="graph-wrapper">
158
- <div class="scale-labels left" id="depth-scale"></div>
122
+ <div class="scale-labels" id="depth-scale"></div>
159
123
  <svg class="sparkline" id="depth-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
160
124
  </div>
161
125
  </div>
162
126
 
163
- <!-- TWS: Velocità del vento reale -->
127
+ <!-- TWS -->
164
128
  <div class="data-box">
165
129
  <div class="label-row"><span class="unit">kts</span><span class="label">TWS</span></div>
166
130
  <span class="value" id="tws">0.0</span>
167
131
  <div class="graph-wrapper">
168
- <div class="scale-labels left" id="tws-scale"></div>
132
+ <div class="scale-labels" id="tws-scale"></div>
169
133
  <svg class="sparkline" id="tws-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
170
134
  </div>
171
135
  </div>
172
136
 
173
- <!-- TWA: Angolo del vento reale (Media 60s) -->
137
+ <!-- TWA MEAN -->
174
138
  <div class="data-box">
175
139
  <div class="label-row"><span class="label">TWA MEAN</span></div>
176
140
  <span class="value value-large" id="twa-avg">---&deg;</span>
177
141
  </div>
178
142
 
179
- <!-- AWA: Angolo del vento apparente (Media 60s) -->
143
+ <!-- AWA MEAN -->
180
144
  <div class="data-box">
181
145
  <div class="label-row"><span class="label">AWA MEAN</span></div>
182
146
  <span class="value value-large" id="awa-avg">---&deg;</span>
183
147
  </div>
184
148
 
185
- <!-- TWD: Direzione reale del vento (Media 60s + Bussola) -->
149
+ <!-- TWD MEAN con Bussola -->
186
150
  <div class="data-box">
187
151
  <div class="label-row"><span class="label">TWD MEAN</span></div>
188
152
  <div class="value-with-compass">
189
153
  <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>
154
+ <circle cx="20" cy="20" r="19" fill="#151515" stroke="#444" stroke-width="1.5"/>
155
+ <text x="20" y="8" fill="#e74c3c" font-size="6" text-anchor="middle" font-weight="900">N</text>
156
+ <g stroke="#555" stroke-width="0.8">
157
+ <line x1="20" y1="11" x2="20" y2="13"/><line x1="30" y1="20" x2="27" y2="20"/><line x1="20" y1="30" x2="20" y2="27"/><line x1="10" y1="20" x2="13" y2="20"/>
158
+ </g>
159
+ <g id="twd-arrow" transform="rotate(0, 20, 20)">
160
+ <path d="M20,10 L16,26 L20,23 L24,26 Z" fill="#ffff00" stroke="#000" stroke-width="0.5"/>
161
+ </g>
162
+ <circle cx="20" cy="20" r="1.5" fill="#555" />
193
163
  </svg>
194
164
  <span class="value value-large" id="twd-avg">---&deg;</span>
195
165
  </div>
196
166
  </div>
197
167
  </div>
168
+
198
169
  </div>
199
-
200
170
  <script src="app.js"></script>
201
171
  </body>
202
172
  </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.25",
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,19 @@
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
+ /* Impedisce selezioni accidentali su tutto il corpo dell'app */
14
+ -webkit-user-select: none;
15
+ user-select: none;
16
+ }
5
17
 
6
18
  .main-container {
7
19
  display: grid;
@@ -9,68 +21,39 @@ body { background-color: #000; color: #fff; font-family: -apple-system, BlinkMac
9
21
  height: 100%;
10
22
  padding: 5px;
11
23
  box-sizing: border-box;
12
- /* Default Desktop (es. 4:3 o schermi medi) */
13
- grid-template-columns: 1fr 3.5fr 1fr;
14
- grid-template-rows: 100%;
15
24
  gap: 8px;
16
- transition: grid-template-columns 0.4s ease;
25
+ /* Desktop Default: 3 colonne */
26
+ grid-template-columns: 1.5fr 3fr 1.5fr;
27
+ grid-template-rows: 100%;
28
+ transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
17
29
  }
18
30
 
19
- /* OTTIMIZZAZIONE PER SCHERMI LARGHI (16:10, 16:9, ecc.) */
20
- @media (min-aspect-ratio: 1.6) {
31
+ /* Ottimizzazione per schermi molto larghi (16:10, 16:9) */
32
+ @media (min-aspect-ratio: 1.5) {
21
33
  .main-container {
22
- /* Allarghiamo le colonne laterali per sfruttare lo spazio orizzontale */
23
- grid-template-columns: 1.4fr 3fr 1.4fr;
24
- gap: 15px; /* Aumentiamo anche il distacco per eleganza */
34
+ grid-template-columns: 2fr 3fr 2fr;
35
+ gap: 15px;
25
36
  }
26
37
  }
27
38
 
28
39
  /* ==========================================================================
29
- 2. PANNELLI LATERALI E RIQUADRI DATI
40
+ 2. PANNELLI E DATA-BOX
30
41
  ========================================================================== */
31
42
  .side-panel {
32
43
  display: flex;
33
44
  flex-direction: column;
34
- background: rgba(255, 255, 255, 0.02);
35
- border-radius: 10px;
45
+ background: rgba(255, 255, 255, 0.03);
46
+ border-radius: 12px;
36
47
  z-index: 10;
37
48
  height: 100%;
49
+ overflow: hidden;
38
50
  }
39
51
 
40
- .right-panel {
41
- align-items: flex-end;
42
- text-align: right;
43
- }
44
-
45
- /* Riquadro singolo: agisce da contenitore per i font dinamici (cqh) */
46
- .data-box {
47
- position: relative;
48
- border-bottom: 1px solid #222;
49
- padding: 8px 10px;
50
- display: flex;
51
- flex-direction: column;
52
- width: 100%;
53
- box-sizing: border-box;
54
- 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
- }
63
-
64
- .data-box:nth-child(3),
65
- .data-box:nth-child(4),
66
- .data-box:nth-child(5) {
67
- height: 16.666vh;
68
- }
52
+ .left-panel { grid-column: 1; }
53
+ .right-panel { grid-column: 3; align-items: flex-end; text-align: right; }
69
54
 
70
- /* ==========================================================================
71
- 3. STRUMENTO CENTRALE (WIND GAUGE)
72
- ========================================================================== */
73
55
  .center-panel {
56
+ grid-column: 2;
74
57
  display: flex;
75
58
  flex-direction: column;
76
59
  align-items: center;
@@ -79,278 +62,150 @@ body { background-color: #000; color: #fff; font-family: -apple-system, BlinkMac
79
62
  overflow: hidden;
80
63
  }
81
64
 
82
- #wind-gauge {
65
+ .data-box {
66
+ position: relative;
67
+ border-bottom: 1px solid #222;
68
+ padding: 8px 12px;
69
+ display: flex;
70
+ flex-direction: column;
83
71
  width: 100%;
84
- height: 100%;
85
- max-height: 100%;
86
- object-fit: contain;
72
+ box-sizing: border-box;
73
+ container-type: size;
74
+ flex: 1;
75
+ overflow: hidden; /* Evita sconfinamenti su iPhone */
87
76
  }
88
77
 
78
+ /* Altezze Proporzionali Desktop */
79
+ .data-box:nth-child(1), .data-box:nth-child(2) { height: 25vh; }
80
+ .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 16.666vh; }
81
+
89
82
  /* ==========================================================================
90
- 4. TIPOGRAFIA DINAMICA (Basata su altezza riquadro - cqh)
83
+ 3. TACTICAL FOCUS MODE (50/50 SPLIT)
91
84
  ========================================================================== */
92
- .label-row {
93
- display: flex;
94
- justify-content: space-between;
95
- align-items: baseline;
96
- margin-bottom: 2px;
97
- width: 100%;
98
- }
85
+ .focus-active { grid-template-columns: 1fr 1fr !important; }
86
+ .focus-active .side-panel:not(.has-focus) { display: none !important; }
99
87
 
100
- .label {
101
- color: #666;
102
- font-size: 0.6rem;
103
- font-weight: bold;
104
- text-transform: uppercase;
105
- }
88
+ /* Simmetria: se il focus è a sx, il vento va a dx e viceversa */
89
+ .focus-active.focus-side-left .side-panel.has-focus { grid-column: 1; }
90
+ .focus-active.focus-side-left .center-panel { grid-column: 2; }
91
+ .focus-active.focus-side-right .center-panel { grid-column: 1; }
92
+ .focus-active.focus-side-right .side-panel.has-focus { grid-column: 2; }
106
93
 
107
- .unit {
108
- color: #888;
109
- font-size: 0.55rem;
110
- font-weight: bold;
94
+ /* Box Focalizzato a tutto schermo */
95
+ .focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
96
+ .focus-active .has-focus .data-box.is-focused {
97
+ height: 100vh !important;
98
+ border: none;
99
+ background: rgba(255, 255, 255, 0.05);
111
100
  }
112
101
 
113
- /* Valore standard con grafico (STW, SOG, Depth, TWS) */
114
- .value {
115
- color: #fff;
116
- /* 22% dell'altezza del box, minimo 1.2rem */
117
- font-size: clamp(1.2rem, 22cqh, 3rem);
118
- font-weight: 600;
119
- line-height: 0.9;
120
- letter-spacing: -1px;
121
- transition: color 0.3s ease;
122
- padding-bottom: 5px;
123
- }
102
+ .focus-active .is-focused .value { font-size: clamp(3rem, 18cqh, 8rem) !important; margin-top: 10px; }
103
+ .focus-active .is-focused .scale-labels { font-size: 22px !important; min-width: 40px !important; }
124
104
 
125
- /* Valore grande senza grafico (Heading, COG, Mean) centrato verticalmente */
126
- .value-large {
127
- margin-top: auto;
128
- margin-bottom: auto;
129
- /* 35% dell'altezza del box, minimo 1.5rem */
130
- font-size: clamp(1.5rem, 35cqh, 4rem);
131
- line-height: 0.85;
132
- }
105
+ /* Grafica Linee Sottili in Focus */
106
+ .focus-active .is-focused .sparkline path { stroke-width: 0.8px !important; }
133
107
 
134
108
  /* ==========================================================================
135
- 5. LAYOUT SPECIALI (TACK E BUSSOLINA TWD)
109
+ 4. TIPOGRAFIA DINAMICA (ELASTICA CQH)
136
110
  ========================================================================== */
137
- /* Box TACK (Doppio valore HDG/COG) */
138
- .dual-value-container {
139
- display: flex;
140
- justify-content: space-between;
141
- align-items: flex-end;
142
- width: 100%;
143
- margin-top: auto;
144
- margin-bottom: auto; /* Centratura verticale */
145
- padding-bottom: 5px;
146
- }
111
+ .label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
112
+ .label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
113
+ .unit { color: #888; font-size: 0.6rem; font-weight: bold; }
147
114
 
148
- .dual-value-col {
149
- display: flex;
150
- flex-direction: column;
151
- justify-content: space-between;
152
- width: 48%;
153
- height: 100%;
154
- }
155
-
156
- .dual-value-col.right-col {
157
- align-items: flex-end;
158
- text-align: right;
159
- }
115
+ .value { color: #fff; font-size: clamp(1.2rem, 22cqh, 3rem); font-weight: 600; line-height: 0.9; letter-spacing: -1px; transition: color 0.3s ease; padding-bottom: 5px; }
160
116
 
161
- .dual-label {
162
- color: #666;
163
- font-size: 0.55rem;
164
- font-weight: bold;
165
- text-transform: uppercase;
117
+ /* Allineamento centrato per box senza grafico */
118
+ .value-large, .dual-value-container, .value-with-compass {
119
+ margin-top: auto;
166
120
  margin-bottom: auto;
167
121
  }
168
122
 
169
- .value.dual-val {
170
- font-size: clamp(1rem, 20cqh, 1.8rem);
171
- padding-bottom: 0;
172
- }
123
+ .value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
173
124
 
174
- /* Box TWD con Mini-Compass */
175
- .value-with-compass {
176
- display: flex;
177
- justify-content: space-between;
178
- align-items: center;
179
- width: 100%;
180
- margin-top: auto;
181
- margin-bottom: auto; /* Centratura verticale */
182
- }
125
+ /* TACK E TWD BUSSOLA */
126
+ .dual-value-container { display: flex; justify-content: space-between; width: 100%; }
127
+ .dual-value-col { display: flex; flex-direction: column; width: 48%; }
128
+ .dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
129
+ .value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
183
130
 
131
+ .value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 10px; }
184
132
  .mini-compass {
185
- width: 38px;
186
- height: 38px;
187
- background: rgba(0,0,0,0.2);
188
- border-radius: 50%;
189
- flex-shrink: 0;
190
- }
191
-
192
- .value-with-compass .value-large {
193
- margin-top: 0;
194
- margin-bottom: 0;
133
+ width: clamp(40px, 60cqh, 120px); height: clamp(40px, 60cqh, 120px);
134
+ background: #000; border-radius: 50%; flex-shrink: 0; border: 1.5px solid #333;
135
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.8); transition: all 0.4s ease;
195
136
  }
137
+ .value-with-compass .value-large { margin: 0; flex-grow: 1; text-align: right; font-size: clamp(1.2rem, 35cqh, 4rem); }
196
138
 
197
139
  /* ==========================================================================
198
- 6. GRAFICI STORICI (SPARKLINES)
140
+ 5. GRAFICI E SCALE
199
141
  ========================================================================== */
200
- .graph-wrapper {
201
- position: relative;
202
- width: 100%;
203
- flex-grow: 1;
204
- min-height: 20px;
205
- margin-top: 4px;
206
- display: flex;
207
- 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;
216
- }
142
+ .graph-wrapper { position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px; display: flex; align-items: stretch; gap: 6px; }
143
+ .sparkline { flex-grow: 1; height: 100%; background: rgba(255, 255, 255, 0.03); border-radius: 4px; }
217
144
 
218
- .scale-labels {
219
- display: flex;
220
- flex-direction: column;
221
- justify-content: space-between;
222
- font-size: 10px; /* Aumentato di ~1.5pt */
223
- color: #888; /* Leggermente più chiaro per leggibilità */
224
- font-weight: bold;
225
- min-width: 16px; /* Allargato per il nuovo font */
226
- height: 100%;
227
- line-height: 1;
145
+ /* Uniforma lo spessore di TUTTI i tipi di linea nei grafici (Standard) */
146
+ .sparkline path,
147
+ .sparkline line {
148
+ stroke-width: 1px !important;
149
+ transition: stroke-width 0.3s ease;
228
150
  }
229
151
 
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
- }
152
+ .scale-labels { display: flex; flex-direction: column; justify-content: space-between; font-size: 10px; color: #777; font-weight: bold; min-width: 16px; height: 100%; line-height: 1; transition: all 0.3s ease; }
241
153
 
242
- .right-panel .scale-labels {
243
- order: 1;
244
- text-align: right;
245
- }
246
- .right-panel .sparkline {
247
- order: 2;
248
- }
154
+ /* Simmetria interna */
155
+ .left-panel .scale-labels { order: 2; text-align: left; }
156
+ .left-panel .sparkline { order: 1; }
157
+ .right-panel .scale-labels { order: 1; text-align: right; }
158
+ .right-panel .sparkline { order: 2; }
249
159
 
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); }
160
+ #stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
161
+ #sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
162
+ #depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
163
+ #tws-graph { stroke: #f1c40f; fill: rgba(241, 196, 15, 0.12); }
255
164
 
256
- /* Effetto bagliore ROSSO per la linea Hercules */
165
+ /* HERCULES MODE */
257
166
  .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
- transition: all 0.3s ease;
267
- }
268
-
269
- .box-hercules .sparkline {
270
- background: rgba(255, 0, 0, 0.05); /* Sfondo grafico leggermente rosso */
271
- }
272
-
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;
167
+ filter: drop-shadow(0 0 5px #ff0000);
168
+ stroke-width: 1.8px !important;
284
169
  }
170
+ .box-hercules { background: rgba(255, 0, 0, 0.08) !important; transition: all 0.3s ease; }
171
+ .box-hercules::after { content: "HERCULES"; position: absolute; bottom: 4px; right: 35px; font-size: 7px; color: #ff4444; font-weight: 900; opacity: 0.8; }
285
172
 
286
173
  /* ==========================================================================
287
- 7. STATI, ALLARMI E ANIMAZIONI
174
+ 6. STATI E ANIMAZIONI
288
175
  ========================================================================== */
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
- }
297
-
176
+ #status { position: absolute; top: 5px; right: 15px; font-size: 0.5rem; text-transform: uppercase; z-index: 1000; }
298
177
  .online { color: #2ecc71; opacity: 0.5; }
299
178
  .offline { color: #e74c3c; font-weight: bold; }
300
-
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
-
179
+ #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow { transition: all 0.6s cubic-bezier(0.1, 0.7, 0.1, 1); }
306
180
  #leeway-mask-rect { transition: none; }
307
-
308
- /* Allarmi Profondità */
309
181
  .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
- }
322
-
323
- .unstable-data {
324
- animation: blink-unstable 1.5s infinite ease-in-out;
325
- color: #f39c12 !important;
326
- }
182
+ .alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
183
+ @keyframes blink-unstable { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
184
+ .unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
185
+ #wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
327
186
 
328
187
  /* ==========================================================================
329
- 8. RESPONSIVE LAYOUT (max-aspect-ratio: 40/50)
188
+ 7. RESPONSIVE (PORTRAIT)
330
189
  ========================================================================== */
331
190
  @media (max-aspect-ratio: 0.9 / 1) {
332
191
  .main-container {
333
- grid-template-columns: 1fr 1fr;
334
- grid-template-rows: 1fr 1fr;
192
+ grid-template-columns: 1fr 1fr !important;
193
+ grid-template-rows: 45vh 55vh !important; /* Calibrato per iPhone */
335
194
  }
336
195
 
337
- /* Vento sopra a tutta larghezza */
338
- .center-panel {
339
- grid-column: 1 / span 2;
340
- grid-row: 1;
341
- height: 50vh;
342
- }
196
+ .center-panel { grid-row: 1; grid-column: 1 / span 2; padding: 5px 0; }
197
+ .left-panel { grid-row: 2; grid-column: 1; }
198
+ .right-panel { grid-row: 2; grid-column: 2; }
343
199
 
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; }
200
+ /* Focus Mode Verticale */
201
+ .main-container.focus-active { grid-template-columns: 100% !important; }
202
+ .focus-active .center-panel { grid-row: 1; height: 45vh !important; }
203
+ .focus-active .side-panel.has-focus { grid-row: 2; height: 55vh !important; }
347
204
 
348
- #wind-gauge { max-height: 100%; width: auto; }
205
+ /* Altezze box iPhone */
206
+ .data-box:nth-child(1), .data-box:nth-child(2) { height: 13.5vh; }
207
+ .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 9.3vh; }
349
208
 
350
- /* Altezze box dimezzate in modalità responsive */
351
- .data-box:nth-child(1), .data-box:nth-child(2) { height: 12.5vh; }
352
- .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
- .value-large { font-size: clamp(1.2rem, 35cqh, 2.5rem); }
209
+ .value-large { font-size: clamp(1.2rem, 35cqh, 2.5rem); margin: auto 0; }
210
+ .value.dual-val { font-size: clamp(0.9rem, 30cqh, 1.4rem); }
356
211
  }