@sailingrotevista/rotevista-dash 1.0.24 → 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 +74 -158
  2. package/index.html +26 -74
  3. package/package.json +1 -1
  4. package/style.css +78 -127
package/app.js CHANGED
@@ -1,51 +1,29 @@
1
1
  // ==========================================================================
2
- // 1. CONFIGURAZIONE E DEFAULT (Sincronizzato con il Plugin SignalK)
2
+ // 1. CONFIGURAZIONE E STATO
3
3
  // ==========================================================================
4
4
  let CONFIG = {
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
- },
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 },
22
8
  scales: {
23
- stw: { stdMax: 12, hercSpan: 4, step: 2 },
24
- sog: { stdMax: 12, hercSpan: 4, step: 2 },
25
- tws: { stdMax: 25, hercSpan: 10, step: 5 },
9
+ stw: { stdMax: 12, hercSpan: 4, step: 2 },
10
+ sog: { stdMax: 12, hercSpan: 4, step: 2 },
11
+ tws: { stdMax: 25, hercSpan: 10, step: 5 },
26
12
  depth: { stdMax: 20, hercSpan: 10, step: 10 }
27
13
  },
28
- server: {
29
- fallbackIp: "192.168.111.240:3000"
30
- }
14
+ server: { fallbackIp: "192.168.111.240:3000" }
31
15
  };
32
16
 
33
17
  const RENDER_INTERVAL_MS = 1000;
34
18
  const TIMEOUT_MS = 5000;
35
19
  const SIM_SAMPLE_INTERVAL = 1000;
36
20
 
37
- // ==========================================================================
38
- // 2. STATO GLOBALE E RIFERIMENTI UI
39
- // ==========================================================================
40
21
  let simulationMode = false;
41
22
  let socket, renderInterval, simInterval;
42
23
  let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
43
24
  let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
44
-
45
- // Gestione Focus e Interazioni
46
25
  let pressTimer, isFocusActive = false, blockNextClick = false;
47
26
 
48
- // Modalità Scale (Standard/Hercules) salvate nel browser
49
27
  const graphModes = {
50
28
  stw: localStorage.getItem('mode_stw') || 'standard',
51
29
  sog: localStorage.getItem('mode_sog') || 'standard',
@@ -53,14 +31,7 @@ const graphModes = {
53
31
  depth: localStorage.getItem('mode_depth') || 'standard'
54
32
  };
55
33
 
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
- };
34
+ 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 } };
64
35
 
65
36
  const ui = {
66
37
  stw: document.getElementById('stw'), sog: document.getElementById('sog'),
@@ -76,13 +47,13 @@ const ui = {
76
47
  };
77
48
 
78
49
  // ==========================================================================
79
- // 3. CARICAMENTO CONFIGURAZIONE DAL SERVER
50
+ // 3. LOGICA DI COMUNICAZIONE E CALCOLO
80
51
  // ==========================================================================
52
+
81
53
  async function fetchServerConfig() {
82
54
  if (!window.location.protocol.includes("http")) return;
83
55
  const pluginID = 'rotevista-dash';
84
56
  const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
85
-
86
57
  for (let url of possibleUrls) {
87
58
  try {
88
59
  const response = await fetch(url);
@@ -90,46 +61,31 @@ async function fetchServerConfig() {
90
61
  const data = await response.json();
91
62
  const actual = data.configuration || data;
92
63
  if (actual && typeof actual === 'object') {
93
- const parseNumbers = (obj) => {
94
- for (let k in obj) {
95
- if (typeof obj[k] === 'object') parseNumbers(obj[k]);
96
- else if (!isNaN(obj[k]) && obj[k] !== "") obj[k] = parseFloat(obj[k]);
97
- }
98
- };
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]); } };
99
65
  parseNumbers(actual);
100
66
  if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
101
67
  if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
102
68
  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.");
107
- return;
69
+ if (actual.scales) { for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; } }
70
+ console.log("Dashboard: Configurazione caricata."); return;
108
71
  }
109
72
  }
110
73
  } catch (e) { }
111
74
  }
112
75
  }
113
76
 
114
- // ==========================================================================
115
- // 4. MATEMATICA E GESTIONE DATI
116
- // ==========================================================================
117
77
  function radToDeg(rad) { return rad * (180 / Math.PI); }
118
78
  function degToRad(deg) { return deg * (Math.PI / 180); }
119
79
  function msToKts(ms) { return ms * 1.94384; }
120
80
  function ktsToMs(kts) { return kts / 1.94384; }
121
81
  function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
122
82
 
123
- // Calcolo media circolare vettoriale con rilevamento stabilità
124
83
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
125
- const now = Date.now();
126
- const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
84
+ const now = Date.now(); const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
127
85
  if (validData.length === 0) return null;
128
- let sSin = 0, sCos = 0;
129
- 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); });
130
87
  let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
131
- let timeSpan = validData[validData.length - 1].time - validData[0].time;
132
- 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);
133
89
  let avgDeg = Math.round(radToDeg(Math.atan2(sSin, sCos)));
134
90
  return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
135
91
  }
@@ -140,23 +96,18 @@ function processIncomingData(path, val) {
140
96
  if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
141
97
  if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
142
98
  if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
143
-
144
- // Calcolo TWD (True Wind Direction)
145
99
  if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
146
- let twdRad = 0;
147
- if (path === "environment.wind.directionTrue") twdRad = val;
148
- else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) {
149
- twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
150
- if (twdRad < 0) twdRad += (2 * Math.PI);
151
- } else return;
152
- store.smoothBuf.twd.push({ val: twdRad, time: now });
153
- 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 });
154
104
  }
155
105
  }
156
106
 
157
107
  // ==========================================================================
158
- // 5. MOTORE DI RENDERING PRINCIPALE
108
+ // 4. RENDERING LOOP E GRAFICA
159
109
  // ==========================================================================
110
+
160
111
  function startDisplayLoop() {
161
112
  renderInterval = setInterval(() => {
162
113
  const now = Date.now();
@@ -174,7 +125,6 @@ function startDisplayLoop() {
174
125
  const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
175
126
  if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
176
127
 
177
- // Calcolo Drift Reale (COG-HDG)
178
128
  if (store.raw["navigation.courseOverGroundTrue"] && store.raw["navigation.headingTrue"]) {
179
129
  let drift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
180
130
  if (curSog < CONFIG.averages.minSpeed) drift = 0;
@@ -188,23 +138,12 @@ function startDisplayLoop() {
188
138
  awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true),
189
139
  twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true),
190
140
  twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
191
-
192
- const upUI = (el, obj) => {
193
- if (!obj) { el.innerHTML = "---&deg;"; el.classList.remove('unstable-data'); }
194
- else {
195
- el.innerHTML = `${obj.val.toString().padStart(3, '0')}&deg;`;
196
- if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data'); else el.classList.add('unstable-data');
197
- }
198
- };
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'); } };
199
142
  upUI(ui.hdg, hObj); upUI(ui.cog, cObj); upUI(ui.awaAvg, awObj); upUI(ui.twaAvg, twObj); upUI(ui.twdAvg, twdObj);
200
-
201
143
  if (hObj && twObj && hObj.val !== null) {
202
- let tA = twObj.val * 2;
203
- 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;`;
204
145
  if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
205
- let tStable = (hObj.stable && twObj.stable) || (curSog < CONFIG.averages.minSpeed);
206
- if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
207
- 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'); }
208
147
  }
209
148
  if (twdObj) { curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); }
210
149
  lastAvgUIUpdate = now;
@@ -214,23 +153,6 @@ function startDisplayLoop() {
214
153
  }, RENDER_INTERVAL_MS);
215
154
  }
216
155
 
217
- // ==========================================================================
218
- // 6. CONNESSIONE SIGNALK
219
- // ==========================================================================
220
- function connect() {
221
- if (simulationMode) return;
222
- let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
223
- try {
224
- socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
225
- socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; };
226
- 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))); };
227
- socket.onclose = () => !simulationMode && setTimeout(connect, 5000);
228
- } catch (e) { setTimeout(connect, 5000); }
229
- }
230
-
231
- // ==========================================================================
232
- // 7. FUNZIONI GRAFICHE E SCALATURA
233
- // ==========================================================================
234
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)}°`; }
235
157
 
236
158
  function manageHistory(t, v) {
@@ -248,11 +170,8 @@ function calculateScale(type, data, mode) {
248
170
  const s = CONFIG.scales[type] || { stdMax: 12, hercSpan: 4, step: 2 };
249
171
  let aMin = Math.min(...data), aMax = Math.max(...data);
250
172
  if (mode === 'hercules') {
251
- let avg = (aMin + aMax) / 2;
252
- let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin));
253
- if (span % 2 !== 0) span += 1;
254
- let min = Math.max(0, Math.floor(avg - (span / 2)));
255
- return { min, max: min + span };
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 };
256
175
  } else return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
257
176
  }
258
177
 
@@ -262,86 +181,83 @@ function updateScaleLabels(t, min, max) {
262
181
  }
263
182
 
264
183
  function drawGraph(d, id, min, max, isTws, isHercules) {
265
- 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
+
266
187
  const w = 200, h = 40, range = max - min || 1;
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" />`; }
188
+ const lClass = isHercules ? 'line-hercules' : '';
189
+
190
+ // 1. Griglie
191
+ let grids = "";
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" />`; });
193
+ for (let m = 1; m < CONFIG.graphs.historyMinutes; m++) {
194
+ const x = w - (m / CONFIG.graphs.historyMinutes) * w;
195
+ grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(255,255,255,0.05)" stroke-width="0.5" />`;
196
+ }
197
+
198
+ // 2. Calcolo Percorsi
269
199
  let pD = "", cS = "";
270
200
  d.forEach((v, i) => {
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} `;
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
272
205
  if (isTws && i > 0) {
273
206
  const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
274
- let c = "#f1c40f"; if (v >= CONFIG.graphs.reef2) c = "#e74c3c"; else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
275
- 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}" />`;
276
212
  }
277
213
  });
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' };
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" />`;
214
+
215
+ // 3. Rendering finale
216
+ const aP = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`;
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
+ }
280
224
  }
281
225
 
282
226
  // ==========================================================================
283
- // 8. EVENTI, INTERAZIONI E FOCUS MODE
227
+ // 5. EVENTI E INTERAZIONI
284
228
  // ==========================================================================
285
229
 
286
230
  function toggleFocusMode(type, element) {
287
231
  const container = document.querySelector('.main-container');
288
232
  const parentPanel = element.closest('.side-panel');
289
233
  const isLeft = parentPanel.classList.contains('left-panel');
290
-
291
234
  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
- }
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')); }
304
237
  }
305
238
 
306
239
  ['stw', 'sog', 'tws', 'depth'].forEach(type => {
307
240
  const el = document.getElementById(type + '-graph').closest('.data-box');
308
-
309
- // Doppio Click -> Toggle Hercules Zoom
310
- el.addEventListener('dblclick', (e) => {
311
- if (isFocusActive) return;
312
- e.preventDefault();
313
- graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
314
- localStorage.setItem('mode_' + type, graphModes[type]);
315
- el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
316
- });
317
-
318
- // Gestione Pressione Prolungata (1s) -> Tactical Focus
319
- const startPress = () => { if (!isFocusActive) pressTimer = setTimeout(() => toggleFocusMode(type, el), 1000); };
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); };
320
243
  const cancelPress = () => { clearTimeout(pressTimer); };
321
- el.addEventListener('mousedown', startPress); el.addEventListener('touchstart', startPress, {passive: true});
244
+ el.addEventListener('mousedown', startPress); el.addEventListener('touchstart', startPress, {passive: false});
322
245
  ['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
- });
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(); });
329
248
  });
330
249
 
331
- // Fullscreen (Hotspot)
332
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(); } }); }
333
251
 
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
252
  // ==========================================================================
339
- // 9. INIZIALIZZAZIONE
253
+ // 6. INIZIALIZZAZIONE
340
254
  // ==========================================================================
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); } } })();
342
-
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); } }
343
257
  async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
344
258
  window.addEventListener('load', init);
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'); }
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); }
345
261
 
346
262
  // Simulatore (Triple click su Depth)
347
263
  ui.depth.closest('.data-box').addEventListener('click', (function() {
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,152 +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
- <!-- Sfondo e bordo bussola -->
191
154
  <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) -->
155
+ <text x="20" y="8" fill="#e74c3c" font-size="6" text-anchor="middle" font-weight="900">N</text>
197
156
  <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 -->
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"/>
202
158
  </g>
203
-
204
- <!-- Freccia TWD (Gialla, forma a punta di freccia aeronautica) -->
205
159
  <g id="twd-arrow" transform="rotate(0, 20, 20)">
206
160
  <path d="M20,10 L16,26 L20,23 L24,26 Z" fill="#ffff00" stroke="#000" stroke-width="0.5"/>
207
161
  </g>
208
-
209
- <!-- Perno centrale -->
210
162
  <circle cx="20" cy="20" r="1.5" fill="#555" />
211
163
  </svg>
212
164
  <span class="value value-large" id="twd-avg">---&deg;</span>
213
165
  </div>
214
166
  </div>
215
167
  </div>
168
+
216
169
  </div>
217
-
218
170
  <script src="app.js"></script>
219
171
  </body>
220
172
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "1.0.24",
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
@@ -10,6 +10,9 @@ body {
10
10
  height: 100vh;
11
11
  width: 100vw;
12
12
  overflow: hidden;
13
+ /* Impedisce selezioni accidentali su tutto il corpo dell'app */
14
+ -webkit-user-select: none;
15
+ user-select: none;
13
16
  }
14
17
 
15
18
  .main-container {
@@ -19,16 +22,16 @@ body {
19
22
  padding: 5px;
20
23
  box-sizing: border-box;
21
24
  gap: 8px;
22
- /* Proporzioni Desktop Standard */
23
- grid-template-columns: 1fr 3.5fr 1fr;
25
+ /* Desktop Default: 3 colonne */
26
+ grid-template-columns: 1.5fr 3fr 1.5fr;
24
27
  grid-template-rows: 100%;
25
28
  transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
26
29
  }
27
30
 
28
- /* Ottimizzazione per schermi molto larghi (es. 16:10, 21:9) */
29
- @media (min-aspect-ratio: 1.6) {
31
+ /* Ottimizzazione per schermi molto larghi (16:10, 16:9) */
32
+ @media (min-aspect-ratio: 1.5) {
30
33
  .main-container {
31
- grid-template-columns: 1.4fr 3fr 1.4fr;
34
+ grid-template-columns: 2fr 3fr 2fr;
32
35
  gap: 15px;
33
36
  }
34
37
  }
@@ -39,10 +42,11 @@ body {
39
42
  .side-panel {
40
43
  display: flex;
41
44
  flex-direction: column;
42
- background: rgba(255, 255, 255, 0.02);
43
- border-radius: 10px;
45
+ background: rgba(255, 255, 255, 0.03);
46
+ border-radius: 12px;
44
47
  z-index: 10;
45
48
  height: 100%;
49
+ overflow: hidden;
46
50
  }
47
51
 
48
52
  .left-panel { grid-column: 1; }
@@ -61,16 +65,17 @@ body {
61
65
  .data-box {
62
66
  position: relative;
63
67
  border-bottom: 1px solid #222;
64
- padding: 8px 10px;
68
+ padding: 8px 12px;
65
69
  display: flex;
66
70
  flex-direction: column;
67
71
  width: 100%;
68
72
  box-sizing: border-box;
69
- container-type: size; /* Cruciale per il calcolo cqh */
73
+ container-type: size;
70
74
  flex: 1;
75
+ overflow: hidden; /* Evita sconfinamenti su iPhone */
71
76
  }
72
77
 
73
- /* Definizione altezze percentuali basate sul Viewport */
78
+ /* Altezze Proporzionali Desktop */
74
79
  .data-box:nth-child(1), .data-box:nth-child(2) { height: 25vh; }
75
80
  .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 16.666vh; }
76
81
 
@@ -80,142 +85,73 @@ body {
80
85
  .focus-active { grid-template-columns: 1fr 1fr !important; }
81
86
  .focus-active .side-panel:not(.has-focus) { display: none !important; }
82
87
 
88
+ /* Simmetria: se il focus è a sx, il vento va a dx e viceversa */
83
89
  .focus-active.focus-side-left .side-panel.has-focus { grid-column: 1; }
84
90
  .focus-active.focus-side-left .center-panel { grid-column: 2; }
85
91
  .focus-active.focus-side-right .center-panel { grid-column: 1; }
86
92
  .focus-active.focus-side-right .side-panel.has-focus { grid-column: 2; }
87
93
 
94
+ /* Box Focalizzato a tutto schermo */
88
95
  .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); }
96
+ .focus-active .has-focus .data-box.is-focused {
97
+ height: 100vh !important;
98
+ border: none;
99
+ background: rgba(255, 255, 255, 0.05);
100
+ }
90
101
 
91
102
  .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; }
93
-
94
- /* Bussola Esplosa in Focus Mode */
95
- .focus-active .is-focused .mini-compass {
96
- width: 45vh !important;
97
- height: 45vh !important;
98
- }
103
+ .focus-active .is-focused .scale-labels { font-size: 22px !important; min-width: 40px !important; }
99
104
 
105
+ /* Grafica Linee Sottili in Focus */
100
106
  .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
107
 
103
108
  /* ==========================================================================
104
109
  4. TIPOGRAFIA DINAMICA (ELASTICA CQH)
105
110
  ========================================================================== */
106
111
  .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; }
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; }
109
114
 
110
- .value {
111
- color: #fff;
112
- font-size: clamp(1.2rem, 22cqh, 3rem);
113
- font-weight: 600;
114
- line-height: 0.9;
115
- letter-spacing: -1px;
116
- transition: color 0.3s ease;
117
- padding-bottom: 5px;
118
- }
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; }
119
116
 
120
- .value-large {
121
- margin-top: auto;
122
- margin-bottom: auto;
123
- font-size: clamp(1.5rem, 35cqh, 4rem);
124
- line-height: 0.85;
125
- }
126
-
127
- /* TACK Layout - Centratura verticale e compattezza */
128
- .dual-value-container {
129
- display: flex;
130
- justify-content: space-between;
131
- align-items: center;
132
- width: 100%;
133
- /* La "molla" auto sopra e sotto lo tiene al centro esatto del box */
117
+ /* Allineamento centrato per box senza grafico */
118
+ .value-large, .dual-value-container, .value-with-compass {
134
119
  margin-top: auto;
135
120
  margin-bottom: auto;
136
- padding-bottom: 5px;
137
121
  }
138
122
 
139
- .dual-value-col {
140
- display: flex;
141
- flex-direction: column;
142
- width: 48%;
143
- /* Rimosso height 100% per non farlo allungare fino al titolo */
144
- }
123
+ .value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
145
124
 
146
- .dual-label {
147
- color: #666;
148
- font-size: 0.55rem;
149
- font-weight: bold;
150
- text-transform: uppercase;
151
- margin-bottom: 2px; /* Distanza fissa e piccola dal numero sottostante */
152
- }
153
-
154
- .value.dual-val {
155
- font-size: clamp(1rem, 22cqh, 1.8rem);
156
- padding-bottom: 0;
157
- line-height: 0.9;
158
- }
159
-
160
- /* BUSSOLA TWD: Centrata e proporzionata */
161
- .value-with-compass {
162
- display: flex;
163
- justify-content: space-between;
164
- align-items: center;
165
- width: 100%;
166
- margin-top: auto;
167
- margin-bottom: auto;
168
- gap: 8px;
169
- }
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; }
170
130
 
131
+ .value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 10px; }
171
132
  .mini-compass {
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;
176
- border-radius: 50%;
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;
181
- }
182
-
183
- .value-with-compass .value-large {
184
- margin: 0;
185
- flex-grow: 1;
186
- text-align: right;
187
- font-size: clamp(1.2rem, 35cqh, 4rem);
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;
188
136
  }
137
+ .value-with-compass .value-large { margin: 0; flex-grow: 1; text-align: right; font-size: clamp(1.2rem, 35cqh, 4rem); }
189
138
 
190
139
  /* ==========================================================================
191
140
  5. GRAFICI E SCALE
192
141
  ========================================================================== */
193
- .graph-wrapper {
194
- position: relative;
195
- width: 100%;
196
- flex-grow: 1;
197
- min-height: 20px;
198
- margin-top: 4px;
199
- display: flex;
200
- align-items: stretch;
201
- gap: 4px;
202
- }
142
+ .graph-wrapper { position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px; display: flex; align-items: stretch; gap: 6px; }
203
143
  .sparkline { flex-grow: 1; height: 100%; background: rgba(255, 255, 255, 0.03); border-radius: 4px; }
204
- .sparkline path { stroke-width: 1px !important; }
205
144
 
206
- .scale-labels {
207
- display: flex;
208
- flex-direction: column;
209
- justify-content: space-between;
210
- font-size: 10px;
211
- color: #666;
212
- font-weight: bold;
213
- min-width: 16px;
214
- height: 100%;
215
- line-height: 1;
216
- transition: all 0.3s ease;
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;
217
150
  }
218
151
 
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; }
153
+
154
+ /* Simmetria interna */
219
155
  .left-panel .scale-labels { order: 2; text-align: left; }
220
156
  .left-panel .sparkline { order: 1; }
221
157
  .right-panel .scale-labels { order: 1; text-align: right; }
@@ -226,35 +162,50 @@ body {
226
162
  #depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
227
163
  #tws-graph { stroke: #f1c40f; fill: rgba(241, 196, 15, 0.12); }
228
164
 
229
- /* ==========================================================================
230
- 6. HERCULES E ANIMAZIONI
231
- ========================================================================== */
232
- .line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
165
+ /* HERCULES MODE */
166
+ .line-hercules {
167
+ filter: drop-shadow(0 0 5px #ff0000);
168
+ stroke-width: 1.8px !important;
169
+ }
233
170
  .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; }
171
+ .box-hercules::after { content: "HERCULES"; position: absolute; bottom: 4px; right: 35px; font-size: 7px; color: #ff4444; font-weight: 900; opacity: 0.8; }
235
172
 
173
+ /* ==========================================================================
174
+ 6. STATI E ANIMAZIONI
175
+ ========================================================================== */
236
176
  #status { position: absolute; top: 5px; right: 15px; font-size: 0.5rem; text-transform: uppercase; z-index: 1000; }
237
177
  .online { color: #2ecc71; opacity: 0.5; }
238
178
  .offline { color: #e74c3c; font-weight: bold; }
239
-
240
179
  #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow { transition: all 0.6s cubic-bezier(0.1, 0.7, 0.1, 1); }
241
180
  #leeway-mask-rect { transition: none; }
242
181
  .alarm-warning { color: #f1c40f !important; }
243
182
  .alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
244
183
  @keyframes blink-unstable { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
245
184
  .unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
246
-
247
185
  #wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
248
186
 
249
187
  /* ==========================================================================
250
188
  7. RESPONSIVE (PORTRAIT)
251
189
  ========================================================================== */
252
190
  @media (max-aspect-ratio: 0.9 / 1) {
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; }
257
- .data-box:nth-child(1), .data-box:nth-child(2) { height: 12.5vh; }
258
- .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 8.333vh; }
259
- .value-large { font-size: clamp(1.2rem, 35cqh, 2.5rem); }
191
+ .main-container {
192
+ grid-template-columns: 1fr 1fr !important;
193
+ grid-template-rows: 45vh 55vh !important; /* Calibrato per iPhone */
194
+ }
195
+
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; }
199
+
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; }
204
+
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; }
208
+
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); }
260
211
  }