@sailingrotevista/rotevista-dash 2.0.4 → 2.0.5

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 +64 -109
  2. package/index.html +172 -52
  3. package/package.json +1 -1
  4. package/style.css +1 -1
package/app.js CHANGED
@@ -7,17 +7,17 @@ let CONFIG = {
7
7
  depthWarning: 5.0
8
8
  },
9
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)
10
+ smoothWindow: 2000,
11
+ longWindow: 60000,
12
+ stabilityTolerance: 2000,
13
+ stabilityThreshold: 0.90,
14
+ minSpeed: 0.5
15
15
  },
16
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
17
+ reef1: 15.0,
18
+ reef2: 20.0,
19
+ historyMinutes: 5,
20
+ samples: 60
21
21
  },
22
22
  scales: {
23
23
  stw: { stdMax: 12, hercSpan: 4, step: 2 },
@@ -42,10 +42,8 @@ let socket, renderInterval, simInterval;
42
42
  let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
43
43
  let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
44
44
 
45
- // Gestione Interazioni (Long Press, Focus, Ghost Clicks)
46
45
  let pressTimer, isFocusActive = false, blockNextClick = false;
47
46
 
48
- // Modalità Scale (Standard/Hercules) salvate nel browser
49
47
  const graphModes = {
50
48
  stw: localStorage.getItem('mode_stw') || 'standard',
51
49
  sog: localStorage.getItem('mode_sog') || 'standard',
@@ -69,19 +67,20 @@ const ui = {
69
67
  tws: document.getElementById('tws'), depth: document.getElementById('depth'),
70
68
  twaAvg: document.getElementById('twa-avg'), awaAvg: document.getElementById('awa-avg'),
71
69
  twdAvg: document.getElementById('twd-avg'), twdArrow: document.getElementById('twd-arrow'),
70
+ twdBoat: document.getElementById('twd-boat-wrap'),
71
+ twdChevron: document.getElementById('twd-wind-chevron'),
72
72
  leewayMask: document.getElementById('leeway-mask-rect'), leewayVal: document.getElementById('leeway-val'),
73
73
  tackHdg: document.getElementById('tack-hdg'), tackCog: document.getElementById('tack-cog'),
74
74
  status: document.getElementById('status'), hotspot: document.getElementById('fullscreen-hotspot')
75
75
  };
76
76
 
77
77
  // ==========================================================================
78
- // 3. COMUNICAZIONE CON IL SERVER (FETCH CONFIG)
78
+ // 3. CARICAMENTO CONFIGURAZIONE DAL SERVER
79
79
  // ==========================================================================
80
80
  async function fetchServerConfig() {
81
81
  if (!window.location.protocol.includes("http")) return;
82
82
  const pluginID = 'rotevista-dash';
83
83
  const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
84
-
85
84
  for (let url of possibleUrls) {
86
85
  try {
87
86
  const response = await fetch(url);
@@ -89,23 +88,13 @@ async function fetchServerConfig() {
89
88
  const data = await response.json();
90
89
  const actual = data.configuration || data;
91
90
  if (actual && typeof actual === 'object') {
92
- // Normalizzazione dati (conversione stringhe -> numeri)
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
- };
91
+ 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
92
  parseNumbers(actual);
100
- // Merge nel sistema locale
101
93
  if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
102
94
  if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
103
95
  if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
104
- if (actual.scales) {
105
- for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; }
106
- }
107
- console.log("Dashboard: Configurazione caricata dal server.");
108
- return;
96
+ if (actual.scales) { for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; } }
97
+ console.log("SUCCESS: Config loaded."); return;
109
98
  }
110
99
  }
111
100
  } catch (e) { }
@@ -121,16 +110,12 @@ function msToKts(ms) { return ms * 1.94384; }
121
110
  function ktsToMs(kts) { return kts / 1.94384; }
122
111
  function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
123
112
 
124
- // Calcolo media circolare vettoriale
125
113
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
126
- const now = Date.now();
127
- const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
114
+ const now = Date.now(); const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
128
115
  if (validData.length === 0) return null;
129
- let sSin = 0, sCos = 0;
130
- validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
116
+ let sSin = 0, sCos = 0; validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
131
117
  let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
132
- let timeSpan = validData[validData.length - 1].time - validData[0].time;
133
- let isStable = (timeSpan >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
118
+ let isStable = (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
134
119
  let avgDeg = Math.round(radToDeg(Math.atan2(sSin, sCos)));
135
120
  return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
136
121
  }
@@ -141,45 +126,47 @@ function processIncomingData(path, val) {
141
126
  if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
142
127
  if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
143
128
  if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
144
-
145
- // Calcolo TWD (Vento Reale Direzione Geografica)
146
129
  if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
147
- let twdRad = 0;
148
- if (path === "environment.wind.directionTrue") twdRad = val;
130
+ let twdRad = 0; if (path === "environment.wind.directionTrue") twdRad = val;
149
131
  else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) {
150
132
  twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
151
133
  if (twdRad < 0) twdRad += (2 * Math.PI);
152
134
  } else return;
153
- store.smoothBuf.twd.push({ val: twdRad, time: now });
154
- store.longBuf.twd.push({ val: twdRad, time: now });
135
+ store.smoothBuf.twd.push({ val: twdRad, time: now }); store.longBuf.twd.push({ val: twdRad, time: now });
155
136
  }
156
137
  }
157
138
 
158
139
  // ==========================================================================
159
- // 5. MOTORE DI RENDERING PRINCIPALE
140
+ // 5. MOTORE RENDERING PRINCIPALE
160
141
  // ==========================================================================
161
142
  function startDisplayLoop() {
162
143
  renderInterval = setInterval(() => {
163
144
  const now = Date.now();
164
-
165
- // 5.1 Watchdog
166
145
  const pathsToWatch = { "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog, "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog, "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth, "environment.wind.speedTrue": ui.tws };
167
146
  for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { pathsToWatch[p][pathsToWatch[p] === ui.awsSvg ? 'textContent' : 'innerText'] = "---"; delete store.raw[p]; } }
168
147
 
169
- // 5.2 Renders Istantanei
170
148
  if (store.raw["navigation.speedThroughWater"] !== undefined) { const v = msToKts(store.raw["navigation.speedThroughWater"]); ui.stw.innerText = v.toFixed(1); manageHistory('stw', v); }
171
149
  let curSog = 0; if (store.raw["navigation.speedOverGround"] !== undefined) { curSog = msToKts(store.raw["navigation.speedOverGround"]); ui.sog.innerText = curSog.toFixed(1); manageHistory('sog', curSog); }
172
150
  if (store.raw["environment.depth.belowTransducer"] !== undefined) { const d = store.raw["environment.depth.belowTransducer"]; ui.depth.innerText = d.toFixed(1); checkDepthAlarm(d); manageHistory('depth', d); }
173
- if (store.raw["environment.wind.speedTrue"] !== undefined) { const w = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = w.toFixed(1); manageHistory('tws', w); }
151
+
152
+ // --- 1. RENDERING TWS CON COLORE DINAMICO (BIANCO -> ARANCIO -> ROSSO) ---
153
+ if (store.raw["environment.wind.speedTrue"] !== undefined) {
154
+ const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
155
+ ui.tws.innerText = twsKts.toFixed(1);
156
+ let twsColor = "#fff"; // Default: Bianco
157
+ if (twsKts >= CONFIG.graphs.reef2) twsColor = "#e74c3c"; // Rosso
158
+ else if (twsKts >= CONFIG.graphs.reef1) twsColor = "#e67e22"; // Arancio
159
+ ui.tws.style.color = twsColor;
160
+ manageHistory('tws', twsKts);
161
+ }
174
162
  if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
175
163
 
176
- // 5.3 Render Quadrante (2s smoothing)
164
+ // Render Quadrante
177
165
  const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
178
166
  if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, smAwa.val); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
179
167
  const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
180
168
  if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
181
169
 
182
- // Drift Reale (COG-HDG)
183
170
  if (store.raw["navigation.courseOverGroundTrue"] && store.raw["navigation.headingTrue"]) {
184
171
  let drift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
185
172
  if (curSog < CONFIG.averages.minSpeed) drift = 0;
@@ -187,7 +174,7 @@ function startDisplayLoop() {
187
174
  let ds = drift > 180 ? drift - 360 : drift; updateLeewayDisplay(Math.max(-20, Math.min(20, ds)));
188
175
  } else updateLeewayDisplay(0);
189
176
 
190
- // 5.4 Render Medie Lunghe (Ogni 3s)
177
+ // --- 2. RENDERING MEDIE E BUSSOLA TATTICA ---
191
178
  if (now - lastAvgUIUpdate > 3000) {
192
179
  let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
193
180
  cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
@@ -204,7 +191,6 @@ function startDisplayLoop() {
204
191
  };
205
192
  upUI(ui.hdg, hObj); upUI(ui.cog, cObj); upUI(ui.awaAvg, awObj); upUI(ui.twaAvg, twObj); upUI(ui.twdAvg, twdObj);
206
193
 
207
- // TACK
208
194
  if (hObj && twObj && hObj.val !== null) {
209
195
  let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
210
196
  if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
@@ -212,7 +198,19 @@ function startDisplayLoop() {
212
198
  if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
213
199
  else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
214
200
  }
215
- if (twdObj) { curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); }
201
+
202
+ // Rotazione Bussola Tattica e Colore Sincronizzato
203
+ if (twdObj && hObj) {
204
+ curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val);
205
+ ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`);
206
+ ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`);
207
+
208
+ let currentTwsKts = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
209
+ let reefColor = "#fff";
210
+ if (currentTwsKts >= CONFIG.graphs.reef2) reefColor = "#e74c3c";
211
+ else if (currentTwsKts >= CONFIG.graphs.reef1) reefColor = "#e67e22";
212
+ if (ui.twdChevron) ui.twdChevron.setAttribute('stroke', reefColor);
213
+ }
216
214
  lastAvgUIUpdate = now;
217
215
  }
218
216
  for (let b in store.smoothBuf) { while (store.smoothBuf[b].length > 0 && (now - store.smoothBuf[b][0].time) > CONFIG.averages.smoothWindow) store.smoothBuf[b].shift(); }
@@ -221,7 +219,7 @@ function startDisplayLoop() {
221
219
  }
222
220
 
223
221
  // ==========================================================================
224
- // 6. CONNESSIONE SIGNALK (subscribe=self)
222
+ // 6. CONNESSIONE SIGNALK
225
223
  // ==========================================================================
226
224
  function connect() {
227
225
  if (simulationMode) return;
@@ -229,13 +227,13 @@ function connect() {
229
227
  try {
230
228
  socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
231
229
  socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; };
232
- 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))); };
230
+ socket.onmessage = (e) => { if (simulationMode) return; const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
233
231
  socket.onclose = () => !simulationMode && setTimeout(connect, 5000);
234
232
  } catch (e) { setTimeout(connect, 5000); }
235
233
  }
236
234
 
237
235
  // ==========================================================================
238
- // 7. FUNZIONI GRAFICHE E SCALATURA
236
+ // 7. FUNZIONI GRAFICHE E DISEGNO (BIANCO -> ARANCIO -> ROSSO)
239
237
  // ==========================================================================
240
238
  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)}°`; }
241
239
 
@@ -274,33 +272,31 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
274
272
  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} `;
275
273
  if (isTws && i > 0) {
276
274
  const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
277
- let c = "#f1c40f"; if (v >= CONFIG.graphs.reef2) c = "#e74c3c"; else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
275
+ let c = "#fff"; // Default: Bianco
276
+ if (v >= CONFIG.graphs.reef2) c = "#e74c3c"; else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
278
277
  cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
279
278
  }
280
279
  });
281
- 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' };
282
- 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]}" />`;
280
+ 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': '#ffffff' };
281
+ svg.innerHTML = isTws ? `${grids}<path d="${aP}" fill="rgba(255,255,255,0.08)" 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]}" />`;
283
282
  }
284
283
 
285
284
  // ==========================================================================
286
- // 8. EVENTI E INTERAZIONI (Smart Touch Manager)
285
+ // 8. EVENTI E INTERAZIONI (SMART TOUCH MANAGER)
287
286
  // ==========================================================================
288
-
289
- // Uccide il menu contestuale di Android/iOS
290
287
  window.addEventListener('contextmenu', e => e.preventDefault(), true);
291
288
 
292
289
  function toggleFocusMode(type, element) {
293
290
  const container = document.querySelector('.main-container');
294
291
  const parentPanel = element.closest('.side-panel');
295
292
  const isLeft = parentPanel.classList.contains('left-panel');
296
-
297
293
  isFocusActive = !isFocusActive;
298
-
299
294
  if (isFocusActive) {
300
295
  container.classList.add('focus-active');
301
296
  container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right');
302
297
  parentPanel.classList.add('has-focus');
303
298
  element.classList.add('is-focused');
299
+ blockNextClick = true;
304
300
  } else {
305
301
  container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
306
302
  document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus'));
@@ -308,85 +304,44 @@ function toggleFocusMode(type, element) {
308
304
  }
309
305
  }
310
306
 
311
- // Configurazione Interazioni per i 4 grafici principali
312
307
  ['stw', 'sog', 'tws', 'depth'].forEach(type => {
313
308
  const el = document.getElementById(type + '-graph').closest('.data-box');
314
-
315
- let lastTapTime = 0;
316
- let tapTimeout;
309
+ let lastTapTime = 0, tapTimeout;
317
310
 
318
311
  const handleInteraction = (e) => {
319
- // Impedisce al browser di fare qualsiasi cosa (zoom, menu, click fantasma)
320
312
  if (e.cancelable) e.preventDefault();
321
-
322
313
  const currentTime = new Date().getTime();
323
314
  const tapDelay = currentTime - lastTapTime;
324
315
 
325
- // --- 1. RILEVAMENTO LONG PRESS ---
326
- // Avviamo un timer per la pressione lunga
327
- pressTimer = setTimeout(() => {
328
- if (!isFocusActive) {
329
- toggleFocusMode(type, el);
330
- lastTapTime = 0; // Reset per non innescare click singoli al rilascio
331
- }
332
- }, 800); // 800ms per attivare il Focus
316
+ pressTimer = setTimeout(() => { if (!isFocusActive) { toggleFocusMode(type, el); lastTapTime = 0; } }, 1000);
333
317
 
334
- // --- 2. GESTIONE DOPPIO E SINGOLO TOCCO ---
335
- // Se tocchiamo di nuovo entro 300ms è un DOUBLE TAP
336
318
  if (tapDelay < 300 && tapDelay > 0) {
337
- clearTimeout(tapTimeout); // Cancella l'azione del singolo tap
319
+ clearTimeout(tapTimeout);
338
320
  if (!isFocusActive) {
339
- // Toggle Hercules Mode
340
321
  graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
341
322
  localStorage.setItem('mode_' + type, graphModes[type]);
342
323
  el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
343
324
  }
344
325
  lastTapTime = 0;
345
326
  } else {
346
- // Se è passato più tempo, potrebbe essere un SINGOLO TAP
347
327
  lastTapTime = currentTime;
348
- tapTimeout = setTimeout(() => {
349
- // Se siamo in focus mode, il singolo tap esce
350
- if (isFocusActive && el.classList.contains('is-focused')) {
351
- toggleFocusMode(type, el);
352
- }
353
- }, 350); // Attesa per vedere se arriva il secondo tap
328
+ tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); }, 350);
354
329
  }
355
330
  };
356
-
357
- const stopInteraction = () => {
358
- clearTimeout(pressTimer);
359
- };
360
-
361
- // Usiamo PointerEvents: funzionano identici per Mouse e Touch
362
331
  el.addEventListener('pointerdown', handleInteraction);
363
- el.addEventListener('pointerup', stopInteraction);
364
- el.addEventListener('pointerleave', stopInteraction);
332
+ el.addEventListener('pointerup', () => clearTimeout(pressTimer));
333
+ el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
365
334
  });
366
335
 
367
- // Fullscreen via Hotspot (Click Singolo)
368
- if (ui.hotspot) {
369
- ui.hotspot.addEventListener('click', (e) => {
370
- e.preventDefault();
371
- const doc = document.documentElement, isF = document.fullscreenElement || document.webkitFullscreenElement;
372
- if (!isF) { if (doc.requestFullscreen) doc.requestFullscreen(); else if (doc.webkitRequestFullscreen) doc.webkitRequestFullscreen(); }
373
- else { if (document.exitFullscreen) document.exitFullscreen(); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); }
374
- });
375
- }
376
-
377
- // ==========================================================================
378
- // 9. INIZIALIZZAZIONE
379
- // ==========================================================================
336
+ 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(); } }); }
380
337
  (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); } } })();
381
338
 
382
339
  async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
383
340
  window.addEventListener('load', init);
384
-
385
- // Allarmi e Audio
386
341
  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'); }
387
342
  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); }
388
343
 
389
- // Simulatore (Triple click su Depth)
344
+ // Simulatore
390
345
  ui.depth.closest('.data-box').addEventListener('click', (function() {
391
346
  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; } };
392
347
  })());
package/index.html CHANGED
@@ -16,11 +16,17 @@
16
16
 
17
17
  <div class="main-container">
18
18
 
19
- <!-- COLONNA SINISTRA: Dati Rotta e Velocità -->
19
+ <!-- ======================================================= -->
20
+ <!-- COLONNA SINISTRA: Dati Rotta e Velocità -->
21
+ <!-- ======================================================= -->
20
22
  <div class="side-panel left-panel">
21
- <!-- STW con Sparkline -->
23
+
24
+ <!-- STW: Velocità attraverso l'acqua -->
22
25
  <div class="data-box">
23
- <div class="label-row"><span class="label">STW</span><span class="unit">kts</span></div>
26
+ <div class="label-row">
27
+ <span class="label">STW</span>
28
+ <span class="unit">kts</span>
29
+ </div>
24
30
  <span class="value" id="stw">0.0</span>
25
31
  <div class="graph-wrapper">
26
32
  <svg class="sparkline" id="stw-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
@@ -28,9 +34,12 @@
28
34
  </div>
29
35
  </div>
30
36
 
31
- <!-- SOG con Sparkline -->
37
+ <!-- SOG: Velocità sul fondo (GPS) -->
32
38
  <div class="data-box">
33
- <div class="label-row"><span class="label">SOG</span><span class="unit">kts</span></div>
39
+ <div class="label-row">
40
+ <span class="label">SOG</span>
41
+ <span class="unit">kts</span>
42
+ </div>
34
43
  <span class="value" id="sog">0.0</span>
35
44
  <div class="graph-wrapper">
36
45
  <svg class="sparkline" id="sog-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
@@ -38,21 +47,27 @@
38
47
  </div>
39
48
  </div>
40
49
 
41
- <!-- HEADING MEAN -->
50
+ <!-- HEADING: Prua Bussola (Media 60s) -->
42
51
  <div class="data-box">
43
- <div class="label-row"><span class="label">HEADING (MEAN)</span></div>
52
+ <div class="label-row">
53
+ <span class="label">HEADING (MEAN)</span>
54
+ </div>
44
55
  <span class="value value-large" id="hdg">000&deg;</span>
45
56
  </div>
46
57
 
47
- <!-- COG MEAN -->
58
+ <!-- COG: Rotta sul fondo (Media 60s) -->
48
59
  <div class="data-box">
49
- <div class="label-row"><span class="label">COG (MEAN)</span></div>
60
+ <div class="label-row">
61
+ <span class="label">COG (MEAN)</span>
62
+ </div>
50
63
  <span class="value value-large" id="cog">000&deg;</span>
51
64
  </div>
52
65
 
53
- <!-- TACK: Previsione mure opposte -->
66
+ <!-- TACK: Calcolo mure opposte (Previsione) -->
54
67
  <div class="data-box">
55
- <div class="label-row"><span class="label">TACK</span></div>
68
+ <div class="label-row">
69
+ <span class="label">TACK</span>
70
+ </div>
56
71
  <div class="dual-value-container">
57
72
  <div class="dual-value-col">
58
73
  <span class="dual-label">HDG</span>
@@ -64,59 +79,136 @@
64
79
  </div>
65
80
  </div>
66
81
  </div>
82
+
67
83
  </div>
68
84
 
69
- <!-- CENTRO: Strumento Vento SVG -->
85
+ <!-- ======================================================= -->
86
+ <!-- CENTRO: Strumento Vento SVG (Ingrandito e Ottimizzato) -->
87
+ <!-- ======================================================= -->
70
88
  <div class="center-panel">
89
+ <!-- ViewBox ottimizzato per ingrandire il diametro (Zoom in) -->
71
90
  <svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
72
91
  <defs>
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>
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>
75
- <clipPath id="leeway-clip"><rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" /></clipPath>
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>
92
+ <!-- Gradienti e Maschere per i settori del vento -->
93
+ <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
94
+ <stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
95
+ <stop offset="100%" style="stop-color:#888888;stop-opacity:1" />
96
+ </linearGradient>
97
+ <linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%">
98
+ <stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
99
+ <stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" />
100
+ <stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" />
101
+ <stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" />
102
+ <stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
103
+ </linearGradient>
104
+ <clipPath id="leeway-clip">
105
+ <rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" />
106
+ </clipPath>
107
+
108
+ <!-- Filtro Glow per l'area di interazione (Hotspot) centrale -->
109
+ <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
110
+ <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
111
+ </filter>
77
112
  </defs>
78
113
 
79
- <circle cx="200" cy="200" r="160" fill="#050505" /><circle cx="200" cy="200" r="125" fill="#121212" />
114
+ <!-- Sfondo circolare del quadrante -->
115
+ <circle cx="200" cy="200" r="160" fill="#050505" />
116
+ <circle cx="200" cy="200" r="125" fill="#121212" />
117
+
118
+ <!-- Settori Vento (Rosso/Sinitra, Verde/Dritta, Arancio/Poppa) -->
80
119
  <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"/>
81
120
  <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"/>
82
121
  <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"/>
83
122
 
123
+ <!-- Tacche Gradate (Generate dinamicamente da Javascript) -->
84
124
  <g id="ticks"></g>
125
+
126
+ <!-- Etichette fisse dei Gradi -->
85
127
  <g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="hanging" font-family="Arial" font-weight="bold">
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>
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>
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>
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>
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>
128
+ <text font-size="16" transform="translate(200, 65)">0</text>
129
+ <text font-size="16" transform="translate(335, 200) rotate(90)">90</text>
130
+ <text font-size="16" transform="translate(65, 200) rotate(-90)">90</text>
131
+ <text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
132
+
133
+ <!-- Dettagli 30-150 Gradi -->
134
+ <text font-size="11" transform="translate(267.5, 83) rotate(30)">30</text>
135
+ <text font-size="11" transform="translate(317, 132.5) rotate(60)">60</text>
136
+ <text font-size="11" transform="translate(317, 267.5) rotate(120)">120</text>
137
+ <text font-size="11" transform="translate(267.5, 317) rotate(150)">150</text>
138
+ <text font-size="11" transform="translate(132.5, 83) rotate(-30)">30</text>
139
+ <text font-size="11" transform="translate(83, 132.5) rotate(-60)">60</text>
140
+ <text font-size="11" transform="translate(83, 267.5) rotate(-120)">120</text>
141
+ <text font-size="11" transform="translate(132.5, 317) rotate(-150)">150</text>
91
142
  </g>
92
143
 
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>
144
+ <!-- Puntatore Track / COG (Blu) -->
145
+ <g id="track-pointer" transform="rotate(0, 200, 200)">
146
+ <path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" />
147
+ </g>
148
+
149
+ <!-- Grande area sensibile al tocco (Hotspot) con Glow per Fullscreen -->
94
150
  <circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
151
+
152
+ <!-- Icona Barca Centrale (Spinta Y+5 per centratura visiva perfetta) -->
95
153
  <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;" />
96
154
 
155
+ <!-- Display Centrale Numerico: Vento Apparente -->
97
156
  <g id="aws-display-group" transform="translate(200, 265)">
98
157
  <text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
99
158
  <text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
100
159
  </g>
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>
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>
160
+
161
+ <!-- Lancette Dinamiche: AWA (Apparente) e TWA (Reale) -->
162
+ <g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
163
+ <path d="M200,80 L211,95 L200,145 L189,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" />
164
+ <text x="200" y="102" fill="#000" font-size="11" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
165
+ </g>
166
+
167
+ <g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
168
+ <path d="M200,90 L206,98 L200,125 L194,98 Z" fill="#ffff00" stroke="#000" stroke-width="0.8" />
169
+ <text x="200" y="104" fill="#000" font-size="8" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
170
+ </g>
103
171
 
172
+ <!-- Barra LEEWAY / SCARROCCIO Inferiore -->
104
173
  <g transform="translate(75, 395)">
105
174
  <text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0&deg;</text>
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" />
175
+ <rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
176
+ <rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
177
+
178
+ <!-- Scala gradi Leeway -->
107
179
  <g stroke="#555" stroke-width="1">
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" />
180
+ <line x1="0" y1="-2" x2="0" y2="14" />
181
+ <line x1="31.25" y1="2" x2="31.25" y2="10" />
182
+ <line x1="62.5" y1="2" x2="62.5" y2="10" />
183
+ <line x1="93.75" y1="3" x2="93.75" y2="9" />
184
+ <line x1="125" y1="-2" x2="125" y2="14" />
185
+ <line x1="156.25" y1="3" x2="156.25" y2="9" />
186
+ <line x1="187.5" y1="2" x2="187.5" y2="10" />
187
+ <line x1="218.75" y1="2" x2="218.75" y2="10" />
188
+ <line x1="250" y1="-2" x2="250" y2="14" />
189
+ </g>
190
+ <g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
191
+ <text x="0" y="24">-20&deg;</text>
192
+ <text x="62.5" y="24">-10</text>
193
+ <text x="125" y="24">0&deg;</text>
194
+ <text x="187.5" y="24">10</text>
195
+ <text x="250" y="24">20&deg;</text>
109
196
  </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>
111
197
  </g>
112
198
  </svg>
113
199
  </div>
114
200
 
115
- <!-- COLONNA DESTRA: Dati Vento e Profondità -->
201
+ <!-- ======================================================= -->
202
+ <!-- COLONNA DESTRA: Profondità e Vento Reale -->
203
+ <!-- ======================================================= -->
116
204
  <div class="side-panel right-panel">
117
- <!-- DEPTH -->
205
+
206
+ <!-- DEPTH: Profondità sotto il trasduttore -->
118
207
  <div class="data-box">
119
- <div class="label-row"><span class="unit">m</span><span class="label">DEPTH</span></div>
208
+ <div class="label-row">
209
+ <span class="unit">m</span>
210
+ <span class="label">DEPTH</span>
211
+ </div>
120
212
  <span class="value" id="depth">--.-</span>
121
213
  <div class="graph-wrapper">
122
214
  <div class="scale-labels" id="depth-scale"></div>
@@ -124,9 +216,12 @@
124
216
  </div>
125
217
  </div>
126
218
 
127
- <!-- TWS -->
219
+ <!-- TWS: Velocità del vento reale -->
128
220
  <div class="data-box">
129
- <div class="label-row"><span class="unit">kts</span><span class="label">TWS</span></div>
221
+ <div class="label-row">
222
+ <span class="unit">kts</span>
223
+ <span class="label">TWS</span>
224
+ </div>
130
225
  <span class="value" id="tws">0.0</span>
131
226
  <div class="graph-wrapper">
132
227
  <div class="scale-labels" id="tws-scale"></div>
@@ -134,39 +229,64 @@
134
229
  </div>
135
230
  </div>
136
231
 
137
- <!-- TWA MEAN -->
232
+ <!-- TWA: Angolo del vento reale -->
138
233
  <div class="data-box">
139
- <div class="label-row"><span class="label">TWA (MEAN)</span></div>
234
+ <div class="label-row">
235
+ <span class="label">TWA (MEAN)</span>
236
+ </div>
140
237
  <span class="value value-large" id="twa-avg">---&deg;</span>
141
238
  </div>
142
239
 
143
- <!-- AWA MEAN -->
240
+ <!-- AWA: Angolo del vento apparente -->
144
241
  <div class="data-box">
145
- <div class="label-row"><span class="label">AWA (MEAN)</span></div>
242
+ <div class="label-row">
243
+ <span class="label">AWA (MEAN)</span>
244
+ </div>
146
245
  <span class="value value-large" id="awa-avg">---&deg;</span>
147
246
  </div>
148
247
 
149
- <!-- TWD MEAN con Bussola -->
248
+ <!-- TWD: Direzione geografica reale del vento (con Bussola) -->
150
249
  <div class="data-box">
151
- <div class="label-row"><span class="label">TWD (MEAN)</span></div>
250
+ <div class="label-row">
251
+ <span class="label">TWD (MEAN)</span>
252
+ </div>
152
253
  <div class="value-with-compass">
153
254
  <svg class="mini-compass" viewBox="0 0 40 40">
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" />
163
- </svg>
255
+ <!-- Cerchio esterno fisso -->
256
+ <circle cx="20" cy="20" r="18.5" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1.2"/>
257
+
258
+ <!-- Punti Cardinali Nord/Sud -->
259
+ <text x="20" y="8" fill="#e74c3c" font-size="5.5" text-anchor="middle" font-weight="900">N</text>
260
+ <text x="20" y="36" fill="rgba(255,255,255,0.3)" font-size="5.5" text-anchor="middle" font-weight="900">S</text>
261
+
262
+ <!-- Tacche Orizzontali (Est / Ovest) -->
263
+ <g stroke="rgba(255,255,255,0.2)" stroke-width="1">
264
+ <line x1="2" y1="20" x2="5" y2="20" /> <!-- Ovest -->
265
+ <line x1="35" y1="20" x2="38" y2="20" /> <!-- Est -->
266
+ </g>
267
+
268
+ <!-- 1. SAGOMA BARCA: Ruota con l'Heading (id: twd-boat-wrap) -->
269
+ <g id="twd-boat-wrap" transform="rotate(0, 20, 20)">
270
+ <path d="M 20,17 L 17,26 L 20,24.5 L 23,26 Z" fill="white" opacity="0.2" />
271
+ </g>
272
+
273
+ <!-- 2. INDICATORE VENTO: Ruota con il TWD (id: twd-arrow) -->
274
+ <g id="twd-arrow" transform="rotate(0, 20, 20)">
275
+ <path id="twd-wind-chevron" d="M 17,5 L 20,7.5 L 23,5"
276
+ fill="none"
277
+ stroke="#ffff00"
278
+ stroke-width="2.2"
279
+ stroke-linecap="round"
280
+ stroke-linejoin="round" />
281
+ </g>
282
+ </svg>
164
283
  <span class="value value-large" id="twd-avg">---&deg;</span>
165
284
  </div>
166
285
  </div>
167
- </div>
168
-
169
- </div>
286
+
287
+ </div> <!-- /Destra -->
288
+ </div> <!-- /Main Container -->
289
+
170
290
  <script src="app.js"></script>
171
291
  </body>
172
292
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/style.css CHANGED
@@ -154,7 +154,7 @@ body {
154
154
  #status { position: absolute; top: 5px; right: 15px; font-size: 0.5rem; text-transform: uppercase; z-index: 1000; }
155
155
  .online { color: #2ecc71; opacity: 0.5; }
156
156
  .offline { color: #e74c3c; font-weight: bold; }
157
- #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow { transition: all 0.6s cubic-bezier(0.1, 0.7, 0.1, 1); }
157
+ #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow, #twd-boat-wrap {transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);}
158
158
  #leeway-mask-rect { transition: none; }
159
159
  .alarm-warning { color: #f1c40f !important; }
160
160
  .alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }