@sailingrotevista/rotevista-dash 2.0.19 → 2.0.21

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.
package/app.js CHANGED
@@ -1,9 +1,16 @@
1
1
  // ==========================================================================
2
- // 1. CONFIGURAZIONE E DEFAULT (Sincronizzato con il Plugin SignalK)
2
+ // 1. CONFIGURAZIONE E DEFAULT
3
3
  // ==========================================================================
4
4
  let CONFIG = {
5
5
  alarms: { depthDanger: 2.5, depthWarning: 5.0 },
6
- averages: { smoothWindow: 2000, longWindow: 60000, stabilityTolerance: 2000, stabilityThreshold: 0.90, minSpeed: 0.5 },
6
+ // Ottimizzato per mare formato: 30s di media e soglia stabilità 85%
7
+ averages: {
8
+ smoothWindow: 2000,
9
+ longWindow: 30000,
10
+ stabilityTolerance: 2000,
11
+ stabilityThreshold: 0.85,
12
+ minSpeed: 0.5
13
+ },
7
14
  graphs: { reef1: 15.0, reef2: 20.0, historyMinutes: 5, samples: 60 },
8
15
  scales: {
9
16
  stw: { stdMax: 12, hercSpan: 4, step: 2 },
@@ -22,13 +29,13 @@ const SIM_SAMPLE_INTERVAL = 1000;
22
29
  // 2. STATO GLOBALE E RIFERIMENTI UI
23
30
  // ==========================================================================
24
31
  let simulationMode = false;
25
- let displayModeSog = 'SOG'; // 'SOG' o 'VMG'
32
+ let displayModeSog = 'SOG';
26
33
  let socket, renderInterval, simInterval;
27
34
  let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
28
35
  let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
29
36
  let curBoatCompassRot = 0, curWindCompassRot = 0;
30
37
 
31
- let smoothedLeeway = 0, rotationTrend = 0;
38
+ let smoothedLeeway = 0, rotationTrend = 0, meteoTrend = 0;
32
39
  let lastShortAvgVal = null, lastInstantTwa = null;
33
40
  let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
34
41
  let twDirty = false, isNavigating = false, reconnectDelay = 1000;
@@ -47,6 +54,8 @@ const store = {
47
54
  smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
48
55
  longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
49
56
  histories: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
57
+ // Buffer temporaneo per calcolare la media dell'intervallo nei grafici
58
+ graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
50
59
  lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0 }
51
60
  };
52
61
 
@@ -66,7 +75,7 @@ const ui = {
66
75
  };
67
76
 
68
77
  // ==========================================================================
69
- // 3. UTILITIES DI SISTEMA (MATEMATICA E BUFFER)
78
+ // 3. UTILITIES (MATEMATICA, BUFFER, AUDIO)
70
79
  // ==========================================================================
71
80
  function radToDeg(rad) { return rad * (180 / Math.PI); }
72
81
  function degToRad(deg) { return deg * (Math.PI / 180); }
@@ -74,17 +83,11 @@ function msToKts(ms) { return ms * 1.94384; }
74
83
  function ktsToMs(kts) { return kts / 1.94384; }
75
84
  function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
76
85
 
77
- /**
78
- * Gestione sicura dei buffer per prevenire memory leak (O(1) complexity).
79
- */
80
86
  function safePush(buffer, val, time, maxLen = 200) {
81
87
  buffer.push({ val: val, time: time });
82
88
  if (buffer.length > maxLen) { buffer.shift(); }
83
89
  }
84
90
 
85
- /**
86
- * Calcola medie circolari restituendo Radianti.
87
- */
88
91
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
89
92
  const now = Date.now();
90
93
  const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
@@ -97,14 +100,35 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
97
100
  return { val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI), stable: isStable };
98
101
  }
99
102
 
103
+ function playBingBing() {
104
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
105
+ const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n;
106
+ 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); }
107
+ b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6);
108
+ }
109
+
110
+ function playGybeAlarm() {
111
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
112
+ const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
113
+ function note(f, s, d) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.type = 'square'; o.frequency.value = f; g.gain.setValueAtTime(0.05, s); g.gain.exponentialRampToValueAtTime(0.001, s + d); o.start(s); o.stop(s + d); }
114
+ for (let i = 0; i < 4; i++) { note(1800, audioCtx.currentTime + (i * 0.15), 0.1); note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1); }
115
+ }
116
+
117
+ function checkDepthAlarm(m) {
118
+ ui.depth.classList.remove('alarm-warning', 'alarm-danger');
119
+ if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); }
120
+ else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning');
121
+ }
122
+
123
+ function updateLeewayDisplay(deg) {
124
+ const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125);
125
+ ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w);
126
+ ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`;
127
+ }
128
+
100
129
  // ==========================================================================
101
- // 4. LOGICA FISICA E CALCOLI
130
+ // 4. MOTORE DI CALCOLO VENTO E DATA ROUTING
102
131
  // ==========================================================================
103
-
104
- /**
105
- * Calcola il Vento Reale partendo dagli Apparenti.
106
- * Logica Acqua (Vele) vs Terra (Meteo).
107
- */
108
132
  function computeTrueWind() {
109
133
  const aws = store.raw["environment.wind.speedApparent"];
110
134
  let awa = store.raw["environment.wind.angleApparent"];
@@ -114,21 +138,27 @@ function computeTrueWind() {
114
138
  if (aws === undefined || awa === undefined) return;
115
139
  if (awa > Math.PI) awa -= 2 * Math.PI;
116
140
 
117
- // Vento Reale su ACQUA (TWA/TWS)
118
141
  const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
119
- const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y), twa_water = Math.atan2(tw_water_y, tw_water_x);
142
+ const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
120
143
 
121
- // Vento Reale su TERRA (TWD)
122
144
  const drift_angle = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
123
145
  const sog_vec_x = sog * Math.cos(drift_angle), sog_vec_y = sog * Math.sin(drift_angle);
124
146
  const tw_ground_x = aws * Math.cos(awa) - sog_vec_x, tw_ground_y = aws * Math.sin(awa) - sog_vec_y;
125
- let twd_ground = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
126
-
147
+ const tws_ground = Math.sqrt(tw_ground_x * tw_ground_x + tw_ground_y * tw_ground_y);
148
+
127
149
  const now = Date.now();
128
- store.raw["environment.wind.speedTrue"] = tws_water; store.raw["environment.wind.angleTrueWater"] = twa_water; store.raw["environment.wind.directionTrue"] = twd_ground;
150
+ store.raw["environment.wind.speedTrue"] = tws_water;
129
151
 
130
- safePush(store.smoothBuf.twa, twa_water, now); safePush(store.longBuf.twa, twa_water, now);
131
- safePush(store.smoothBuf.twd, twd_ground, now); safePush(store.longBuf.twd, twd_ground, now);
152
+ if (tws_water > 0.05) {
153
+ const twa_water = Math.atan2(tw_water_y, tw_water_x);
154
+ store.raw["environment.wind.angleTrueWater"] = twa_water;
155
+ safePush(store.smoothBuf.twa, twa_water, now); safePush(store.longBuf.twa, twa_water, now);
156
+ }
157
+ if (tws_ground > 0.05) {
158
+ let twd_ground = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
159
+ store.raw["environment.wind.directionTrue"] = twd_ground;
160
+ safePush(store.smoothBuf.twd, twd_ground, now); safePush(store.longBuf.twd, twd_ground, now);
161
+ }
132
162
  }
133
163
 
134
164
  function processIncomingData(path, val) {
@@ -136,7 +166,6 @@ function processIncomingData(path, val) {
136
166
  if (path === "navigation.position") store.raw["navigation.position"] = val;
137
167
  if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
138
168
 
139
- // Dirty flag + Debounce 100ms
140
169
  const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
141
170
  if (twPaths.includes(path)) twDirty = true;
142
171
  if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
@@ -146,18 +175,16 @@ function processIncomingData(path, val) {
146
175
  }
147
176
 
148
177
  // ==========================================================================
149
- // 5. AUDIO E ALLARMI
150
- // ==========================================================================
151
- function playBingBing() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
152
- function playGybeAlarm() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n; function note(f, s, d) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.type = 'square'; o.frequency.value = f; g.gain.setValueAtTime(0.05, s); g.gain.exponentialRampToValueAtTime(0.001, s + d); o.start(s); o.stop(s + d); } for (let i = 0; i < 4; i++) { note(1800, audioCtx.currentTime + (i * 0.15), 0.1); note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1); } }
153
- function checkDepthAlarm(m) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
154
-
155
- // ==========================================================================
156
- // 6. TREND VENTO E TATTICA
178
+ // 5. TREND VENTO E SICUREZZA
157
179
  // ==========================================================================
180
+ /**
181
+ * Analizza i trend di rotazione del vento su due scale temporali:
182
+ * 1. Tattica (veloce): per la regolazione delle vele (sulla lancetta TWA)
183
+ * 2. Strategica (lenta): per le previsioni meteo a lungo termine (bussola TWD)
184
+ */
158
185
  function updateWindTrend() {
159
186
  const now = Date.now();
160
- const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true);
187
+ const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 30000, true);
161
188
  const shortAvg = getCircularAverageFromBuffer(store.longBuf.twd, 5000, false);
162
189
  const instantTwaRad = store.raw["environment.wind.angleTrueWater"];
163
190
 
@@ -167,54 +194,66 @@ function updateWindTrend() {
167
194
  if (lastShortAvgVal === null) { lastShortAvgVal = shortAvgDeg; lastInstantTwa = instantTwaDeg; return; }
168
195
  const dt = (now - lastTrendTime) / 1000; lastTrendTime = now;
169
196
 
170
- // Gybe Safety
197
+ // ALLARME STRAMBATA
171
198
  const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
172
199
  lastInstantTwa = instantTwaDeg;
173
- if (gybeDetected && isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
174
-
200
+
175
201
  const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
176
202
  const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
177
203
 
204
+ if (gybeDetected && isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
178
205
  if (now - lastGybeAlarmTime < 4000 && isNavigating) {
179
206
  [compassDots.cw, compassDots.ccw, gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.add('is-gybing'); el.classList.remove('is-trending'); el.setAttribute('fill', '#ff0000'); }});
180
207
  return;
181
208
  }
182
209
 
183
- // Trend calculation con Clamping e Alpha adattivo
210
+ // CALCOLO TREND
184
211
  let diff = (shortAvgDeg - lastShortAvgVal + 540) % 360 - 180; lastShortAvgVal = shortAvgDeg;
185
212
  if (dt > 0) {
186
213
  let rate = Math.max(-10, Math.min(10, diff / dt));
187
- const alpha = Math.min(1, dt / 5);
188
- rotationTrend = rotationTrend * (1 - alpha) + rate * alpha;
214
+ // Tattico (veloce ~15s)
215
+ const alphaT = Math.min(1, dt / 15);
216
+ rotationTrend = rotationTrend * (1 - alphaT) + rate * alphaT;
217
+ // Strategico (molto lento ~8-10min)
218
+ const alphaM = Math.min(1, dt / 500);
219
+ meteoTrend = meteoTrend * (1 - alphaM) + rate * alphaM;
189
220
  }
190
221
 
191
- if (Math.abs(rotationTrend) > 1.0) {
192
- const pos = store.raw["navigation.position"], isSouth = pos && pos.latitude < 0;
193
- let meteoColor = (!isSouth) ? (rotationTrend < 0 ? "#00ff00" : "#ff0000") : (rotationTrend > 0 ? "#00ff00" : "#ff0000");
194
- let isLift = (twaAvgDeg > 0) ? (rotationTrend > 0) : (rotationTrend < 0);
195
- const absTwa = Math.abs(twaAvgDeg);
196
- if (absTwa >= 90 && absTwa < 160) isLift = !isLift; else if (absTwa >= 160) isLift = false;
197
- const tacticColor = isLift ? "#00ff00" : "#ff0000";
222
+ // VISUALIZZAZIONE METEO (Bussola Centrale)
223
+ if (Math.abs(meteoTrend) > 0.2) {
224
+ const isSouth = store.raw["navigation.position"]?.latitude < 0;
225
+ let meteoColor = (!isSouth) ? (meteoTrend < 0 ? "#27ae60" : "#c0392b") : (meteoTrend > 0 ? "#27ae60" : "#c0392b");
226
+ if (meteoTrend > 0) {
227
+ if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
228
+ if (compassDots.ccw) { compassDots.ccw.classList.remove('is-trending'); compassDots.ccw.setAttribute('fill', '#bbb'); }
229
+ } else {
230
+ if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
231
+ if (compassDots.cw) { compassDots.cw.classList.remove('is-trending'); compassDots.cw.setAttribute('fill', '#bbb'); }
232
+ }
233
+ } else {
234
+ [compassDots.cw, compassDots.ccw].forEach(el => { if (el) { el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
235
+ }
198
236
 
237
+ // VISUALIZZAZIONE TATTICA (Lancetta Vento)
238
+ if (Math.abs(rotationTrend) > 3.0) {
239
+ let isLift = (twaAvgDeg > 0) ? (rotationTrend > 0) : (rotationTrend < 0);
240
+ if (Math.abs(twaAvgDeg) >= 90) isLift = !isLift;
241
+ const tacticColor = isLift ? "#27ae60" : "#c0392b";
199
242
  if (rotationTrend > 0) {
200
- if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
201
243
  if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
202
- [compassDots.ccw, gaugeDots.ccw].forEach(el => { if(el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
244
+ if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-trending'); gaugeDots.ccw.setAttribute('fill', '#bbb'); }
203
245
  } else {
204
- if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
205
246
  if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor); }
206
- [compassDots.cw, gaugeDots.cw].forEach(el => { if(el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
247
+ if (gaugeDots.cw) { gaugeDots.cw.classList.remove('is-trending'); gaugeDots.cw.setAttribute('fill', '#bbb'); }
207
248
  }
208
249
  } else {
209
- [compassDots.cw, compassDots.ccw, gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
250
+ [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
210
251
  }
211
252
  }
212
253
 
213
254
  // ==========================================================================
214
- // 7. RENDERING ENGINE (TIERED RENDERING)
255
+ // 6. RENDERING ENGINE (TIERED)
215
256
  // ==========================================================================
216
- function updateLeewayDisplay(deg) { const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125); ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w); ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`; }
217
-
218
257
  function refreshGraph(t) {
219
258
  const type = (t === 'vmg') ? 'sog' : t;
220
259
  const data = store.histories[t]; if (!data || data.length < 2) return;
@@ -229,51 +268,50 @@ function startDisplayLoop() {
229
268
  renderInterval = setInterval(() => {
230
269
  const now = Date.now(); tick++;
231
270
 
232
- // --- LIVE TIER (1s) ---
233
- const pathsToWatch = { "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog, "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog, "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth, "environment.wind.speedTrue": ui.tws };
234
- for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { if (pathsToWatch[p] === ui.awsSvg) ui.awsSvg.textContent = "---"; else pathsToWatch[p].innerText = "---"; delete store.raw[p]; } }
235
-
236
271
  const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0), sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
237
272
  isNavigating = stwKts > CONFIG.averages.minSpeed || sogKts > CONFIG.averages.minSpeed;
238
273
 
274
+ // LIVE TIER (1s)
275
+ 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 };
276
+ for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { if (pathsToWatch[p] === ui.awsSvg) ui.awsSvg.textContent = "---"; else pathsToWatch[p].innerText = "---"; delete store.raw[p]; } }
277
+
239
278
  if (store.raw["navigation.speedThroughWater"] !== undefined) { ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts); }
240
279
  if (store.raw["navigation.speedOverGround"] !== undefined) {
241
280
  const twaRad = store.raw["environment.wind.angleTrueWater"], vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
242
281
  manageHistory('vmg', vmgKts); manageHistory('sog', sogKts);
243
- if (displayModeSog === 'VMG') { ui.sog.innerText = vmgKts.toFixed(1); ui.sog.style.color = "#64ffda"; document.getElementById('sog-vmg-label').textContent = 'VMG'; }
244
- else { ui.sog.innerText = sogKts.toFixed(1); ui.sog.style.color = (sogKts-stwKts > 0.3) ? "#2ecc71" : (sogKts-stwKts < -0.3 ? "#e74c3c" : "#fff"); document.getElementById('sog-vmg-label').textContent = 'SOG'; }
282
+ if (displayModeSog === 'VMG') {
283
+ ui.sog.innerText = vmgKts.toFixed(1); ui.sog.style.color = "#16a085"; document.getElementById('sog-vmg-label').textContent = 'VMG';
284
+ } else {
285
+ ui.sog.innerText = sogKts.toFixed(1);
286
+ ui.sog.style.color = (sogKts - stwKts > 0.3) ? "#27ae60" : (sogKts - stwKts < -0.3 ? "#c0392b" : "#000");
287
+ document.getElementById('sog-vmg-label').textContent = 'SOG';
288
+ }
245
289
  }
246
290
  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); }
247
- if (store.raw["environment.wind.speedTrue"] !== undefined) { const twsKts = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = twsKts.toFixed(1); ui.tws.style.color = (twsKts >= CONFIG.graphs.reef2) ? "#e74c3c" : (twsKts >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff"); manageHistory('tws', twsKts); }
291
+ if (store.raw["environment.wind.speedTrue"] !== undefined) { ui.tws.innerText = msToKts(store.raw["environment.wind.speedTrue"]).toFixed(1); ui.tws.style.color = (msToKts(store.raw["environment.wind.speedTrue"]) >= CONFIG.graphs.reef2) ? "#e74c3c" : (msToKts(store.raw["environment.wind.speedTrue"]) >= CONFIG.graphs.reef1 ? "#e67e22" : "#000"); manageHistory('tws', msToKts(store.raw["environment.wind.speedTrue"])); }
248
292
  if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
249
293
 
250
- const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
294
+ const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
251
295
  if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
252
- const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
296
+ const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
253
297
  if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
254
298
 
255
299
  if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
256
300
  let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
257
301
  if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (driftDeg * 0.1);
258
302
  curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
259
- const isContaminated = Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7; ui.leewayVal.style.color = isContaminated ? "#f39c12" : "#fff";
303
+ ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#e67e22" : "#000";
260
304
  updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
261
305
  }
262
-
263
- updateWindTrend(); // Fast update
264
306
 
265
- // --- HEAVY TIER (2s) ---
266
- if (tick % 2 === 0) {
267
- refreshGraph('stw'); refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog'); refreshGraph('depth'); refreshGraph('tws');
268
- }
307
+ updateWindTrend();
308
+ if (tick % 2 === 0) { refreshGraph('stw'); refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog'); refreshGraph('depth'); refreshGraph('tws'); }
269
309
 
270
- // --- SLOW TIER (3s) ---
310
+ // SLOW TIER (3s)
271
311
  if (tick % 3 === 0) {
272
- let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
273
- cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
274
- awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true),
275
- twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true),
276
- twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
312
+ let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, 30000, false), cObj = getCircularAverageFromBuffer(store.longBuf.cog, 30000, false),
313
+ awObj = getCircularAverageFromBuffer(store.longBuf.awa, 30000, true), twObj = getCircularAverageFromBuffer(store.longBuf.twa, 30000, true),
314
+ twdObj = getCircularAverageFromBuffer(store.longBuf.twd, 30000, false);
277
315
 
278
316
  const upUI = (el, obj, isCompass = false) => {
279
317
  if (!obj || obj.val === null) { el.innerHTML = "---&deg;"; el.classList.remove('unstable-data'); }
@@ -283,11 +321,12 @@ function startDisplayLoop() {
283
321
  }
284
322
  };
285
323
  upUI(ui.hdg, hObj, true); upUI(ui.cog, cObj, true); upUI(ui.awaAvg, awObj, false); upUI(ui.twaAvg, twObj, false); upUI(ui.twdAvg, twdObj, true);
286
- if (hObj && twObj && hObj.val !== null) {
287
- const tackHdgDeg = radToDeg((hObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
324
+
325
+ if (hObj && twObj) {
326
+ const tackHdgDeg = radToDeg((hObj.val + twObj.val * 2 + Math.PI * 2) % (Math.PI * 2));
288
327
  ui.tackHdg.innerHTML = `${Math.round((tackHdgDeg + 360) % 360).toString().padStart(3, '0')}&deg;`;
289
328
  if (cObj) {
290
- const tackCogDeg = radToDeg((cObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
329
+ const tackCogDeg = radToDeg((cObj.val + twObj.val * 2 + Math.PI * 2) % (Math.PI * 2));
291
330
  ui.tackCog.innerHTML = `${Math.round((tackCogDeg + 360) % 360).toString().padStart(3, '0')}&deg;`;
292
331
  }
293
332
  }
@@ -296,13 +335,14 @@ function startDisplayLoop() {
296
335
  curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwd.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
297
336
  curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdg.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
298
337
  }
338
+ lastAvgUIUpdate = now;
299
339
  }
300
340
  if (tick % 60 === 0) tick = 0;
301
341
  }, RENDER_INTERVAL_MS);
302
342
  }
303
343
 
304
344
  // ==========================================================================
305
- // 8. INTERAZIONI E RETE
345
+ // 7. CONFIGURAZIONE E GRAFICI UTILS
306
346
  // ==========================================================================
307
347
  async function fetchServerConfig() {
308
348
  if (!window.location.protocol.includes("http")) return;
@@ -314,7 +354,7 @@ async function fetchServerConfig() {
314
354
  if (response.ok) {
315
355
  const data = await response.json();
316
356
  const actual = data.configuration || data;
317
- if (actual && typeof actual === 'object') {
357
+ if (actual) {
318
358
  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]); } };
319
359
  parseNumbers(actual);
320
360
  if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
@@ -328,8 +368,19 @@ async function fetchServerConfig() {
328
368
  }
329
369
 
330
370
  function manageHistory(t, v) {
331
- const n = Date.now(); const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
332
- if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) { store.histories[t].push(v); if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift(); store.lastUpdates[t] = n; }
371
+ const n = Date.now();
372
+ const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
373
+ if (!store.graphTempBuf[t]) store.graphTempBuf[t] = [];
374
+ store.graphTempBuf[t].push(v);
375
+
376
+ if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
377
+ const sum = store.graphTempBuf[t].reduce((a, b) => a + b, 0);
378
+ const avg = sum / store.graphTempBuf[t].length;
379
+ store.histories[t].push(avg);
380
+ if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
381
+ store.graphTempBuf[t] = [];
382
+ store.lastUpdates[t] = n;
383
+ }
333
384
  }
334
385
 
335
386
  function calculateScale(type, data, mode) {
@@ -343,22 +394,29 @@ function updateScaleLabels(t, min, max) { const el = document.getElementById(t +
343
394
  function drawGraph(d, id, min, max, isTws, isHercules) {
344
395
  const svg = document.getElementById(id); if (!svg || d.length < 2) return;
345
396
  const w = 200, h = 40, range = max - min || 1;
346
- 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" />`; });
347
- 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" />`; }
397
+ let grids = "";
398
+ [0.25, 0.5, 0.75].forEach(p => { grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(0,0,0,0.12)" stroke-width="0.5" />`; });
399
+ for (let m = 1; m < CONFIG.graphs.historyMinutes; m++) {
400
+ const x = w - (m / CONFIG.graphs.historyMinutes) * w;
401
+ grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
402
+ }
348
403
  let pD = ""; let cS = "";
349
404
  d.forEach((v, i) => {
350
405
  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} `;
351
406
  if (isTws && i > 0) {
352
407
  const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
353
- let c = (v >= CONFIG.graphs.reef2) ? "#e74c3c" : (v >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff");
408
+ let c = (v >= CONFIG.graphs.reef2) ? "#e74c3c" : (v >= CONFIG.graphs.reef1 ? "#e67e22" : "#000");
354
409
  cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
355
410
  }
356
411
  });
357
- const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#ffffff' };
358
- const colorKey = id === 'sog-graph' && displayModeSog === 'VMG' ? '#64ffda' : clrs[id];
359
- svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(255,255,255,0.08)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${colorKey}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${colorKey}" />`;
412
+ const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#000' };
413
+ const colorKey = id === 'sog-graph' && displayModeSog === 'VMG' ? '#16a085' : clrs[id];
414
+ svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(0,0,0,0.05)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${colorKey}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${colorKey}" />`;
360
415
  }
361
416
 
417
+ // ==========================================================================
418
+ // 8. INTERAZIONI E RETE
419
+ // ==========================================================================
362
420
  function toggleFocusMode(type, element) {
363
421
  const container = document.querySelector('.main-container'); const parentPanel = element.closest('.side-panel'); const isLeft = parentPanel.classList.contains('left-panel');
364
422
  isFocusActive = !isFocusActive;
@@ -374,7 +432,7 @@ function toggleFocusMode(type, element) {
374
432
  clearTimeout(pressTimer); if (isLongPressActive) return;
375
433
  const currentTime = new Date().getTime(), tapDelay = currentTime - lastTapTime;
376
434
  if (tapDelay < 300 && tapDelay > 0) { clearTimeout(tapTimeout); if (!isFocusActive) { graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); } lastTapTime = 0; }
377
- else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(100, 255, 218, 0.1)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
435
+ else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(0, 0, 0, 0.05)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
378
436
  });
379
437
  el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
380
438
  });
@@ -401,32 +459,29 @@ ui.depth.closest('.data-box').addEventListener('click', (function() { let dC = 0
401
459
 
402
460
  function startDynamicSimulation() {
403
461
  ui.status.innerText = "SIM ATTIVO";
404
- let sim = { hdg: 45, tws: 12, twd: 45, depth: 12, stw: 5, leeway: 0, currentSpeed: 1.5, currentDir: 90, startTime: Date.now() };
462
+ let sim = { hdg: 45, tws: 12, twd: 90, depth: 12, stw: 5, leeway: 0, currentSpeed: 1.5, currentDir: 90, startTime: Date.now() };
405
463
  simInterval = setInterval(() => {
406
464
  const elapsed = (Date.now() - sim.startTime) / 1000;
407
- if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
408
465
  sim.twd = (sim.twd + (Math.sin(elapsed / 20) * 0.5) + 360) % 360;
409
- const inGust = (elapsed % 40) < 5; const targetTws = (inGust ? 18 : 10) + Math.sin(elapsed / 10) * 2;
466
+ const targetTws = 10 + Math.sin(elapsed / 10) * 2;
410
467
  sim.tws += (targetTws - sim.tws) * 0.05; sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
411
468
  let twaRel = (sim.twd - sim.hdg + 360) % 360; if (twaRel > 180) twaRel -= 360;
412
- let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125)); sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
413
- const rawLeeway = Math.sin(degToRad(twaRel)) * 4; sim.leeway += (rawLeeway - sim.leeway) * 0.05;
414
- const bX = sim.stw * Math.sin(degToRad(sim.hdg + sim.leeway)), bY = sim.stw * Math.cos(degToRad(sim.hdg + sim.leeway));
415
- const cX = sim.currentSpeed * Math.sin(degToRad(sim.currentDir)), cY = sim.currentSpeed * Math.cos(degToRad(sim.currentDir));
416
- const sog = Math.sqrt(Math.pow(bX + cX, 2) + Math.pow(bY + cY, 2)), cog = (radToDeg(Math.atan2(bX + cX, bY + cY)) + 360) % 360;
469
+ let targetStw = 5; sim.stw += (targetStw - sim.stw) * 0.05;
470
+ const bX = sim.stw * Math.sin(degToRad(sim.hdg)), bY = sim.stw * Math.cos(degToRad(sim.hdg));
471
+ const sog = sim.stw, cog = sim.hdg;
417
472
  const twaRad = degToRad(twaRel), aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
418
473
  const awa = Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad));
419
474
  processIncomingData("environment.wind.speedApparent", ktsToMs(aws)); processIncomingData("environment.wind.angleApparent", awa);
420
475
  processIncomingData("environment.depth.belowTransducer", sim.depth); processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
421
476
  processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw)); processIncomingData("navigation.speedOverGround", ktsToMs(sog));
422
- processIncomingData("navigation.courseOverGroundTrue", degToRad(cog)); processIncomingData("navigation.position", { latitude: 45.0, longitude: 12.0 });
477
+ processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
423
478
  }, 1000);
424
479
  }
425
480
 
426
481
  // ==========================================================================
427
- // 11. INIT
482
+ // 10. INIT
428
483
  // ==========================================================================
429
484
  window.addEventListener('contextmenu', e => e.preventDefault(), true);
430
- (function genTicks() { const c = document.getElementById('ticks'); if (c) { for (let i = 0; i < 360; i += 10) { const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50)); l.setAttribute("stroke", m ? "#fff" : "#666"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
485
+ (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 ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
431
486
  async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
432
487
  window.addEventListener('load', init);
package/index.html CHANGED
@@ -158,16 +158,24 @@
158
158
  <text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
159
159
  </g>
160
160
 
161
- <g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
162
- <path d="M200,80 L211,95 L200,145 L189,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" />
163
- <text x="200" y="102" fill="#000" font-size="11" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
161
+ <!-- Lancetta Apparente (AWA) - Versione allungata -->
162
+ <g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
163
+ <!-- Ho portato 75 -> 70 (fuori) e 145 -> 155 (dentro) -->
164
+ <path d="M 200,70 L 213,95 L 200,145 L 187,95 Z"
165
+ fill="#ff8c00" stroke="#000" stroke-width="1" />
166
+ <text x="200" y="90" fill="#000" font-size="10" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
164
167
  </g>
165
-
166
- <g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
167
- <path d="M200,90 L206,98 L200,125 L194,98 Z" fill="#ffff00" stroke="#000" stroke-width="0.8" />
168
- <text x="200" y="104" fill="#000" font-size="8" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
169
- <circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#ffffff" opacity="0.3" />
170
- <circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#ffffff" opacity="0.3" />
168
+
169
+ <!-- Lancetta Reale (TWA) - FORMA A GOCCIA (Secondaria/Piccola) -->
170
+ <g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
171
+ <!-- Goccia piccola: inizia a y=92, punta a y=128 -->
172
+ <path d="M 200,92 A 8,8 0 0 1 208,100 C 208,108 200,128 200,128 C 200,128 192,108 192,100 A 8,8 0 0 1 200,92 Z"
173
+ fill="#ffff00" stroke="#000" stroke-width="0.8" />
174
+ <text x="200" y="106" fill="#000" font-size="9" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
175
+
176
+ <!-- Pallini Trend (Sempre agganciati alla rotazione del TWA) -->
177
+ <circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#ffffff" />
178
+ <circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#ffffff" />
171
179
  </g>
172
180
 
173
181
  <g transform="translate(75, 395)">
package/index.js CHANGED
@@ -63,8 +63,8 @@ module.exports = function (app) {
63
63
  longWindow: {
64
64
  type: 'number',
65
65
  title: 'Long Average Window (ms)',
66
- description: "Time buffer used for 'MEAN' values (e.g., 60000ms = 1 min). Higher values provide more stability but slower reaction.",
67
- default: 60000
66
+ description: "Time buffer for 'MEAN' values. Larger windows produce smoother numbers but increase the 'Unstable' (orange) alerts during maneuvers or in gusty conditions, as data coherence decreases over time.",
67
+ default: 30000
68
68
  },
69
69
  smoothWindow: {
70
70
  type: 'number',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "2.0.19",
3
+ "version": "2.0.21",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/settings.json CHANGED
@@ -8,11 +8,11 @@
8
8
  "red": 20.0
9
9
  },
10
10
  "averages": {
11
- "long_window": 60000,
11
+ "long_window": 3000,
12
12
  "smooth_window": 2000
13
13
  },
14
14
  "hercules_ranges": {
15
15
  "speed": 4.0,
16
16
  "wind": 10.0
17
17
  }
18
- }
18
+ }
package/style.css CHANGED
@@ -1,9 +1,9 @@
1
1
  /* ==========================================================================
2
- 1. BASE E STRUTTURA GENERALE (LIQUID GRID LAYOUT)
2
+ 1. BASE E RESET DI SISTEMA (GIORNO: SFONDO CHIARO)
3
3
  ========================================================================== */
4
4
  body {
5
- background-color: #000;
6
- color: #fff;
5
+ background-color: #fff;
6
+ color: #000;
7
7
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
8
8
  margin: 0;
9
9
  padding: 0;
@@ -16,6 +16,9 @@ body {
16
16
  touch-action: none;
17
17
  }
18
18
 
19
+ /* ==========================================================================
20
+ 2. LAYOUT PRINCIPALE (LIQUID GRID)
21
+ ========================================================================== */
19
22
  .main-container {
20
23
  display: grid;
21
24
  width: 100%;
@@ -23,20 +26,16 @@ body {
23
26
  padding: 5px;
24
27
  box-sizing: border-box;
25
28
  gap: 8px;
26
- /* Rapporto Standard: Lati 1fr, Centro 1.5fr (Bilanciato) */
27
29
  grid-template-columns: minmax(180px, 1fr) minmax(auto, 1.5fr) minmax(180px, 1fr);
28
30
  grid-template-rows: 100%;
29
31
  transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
30
32
  justify-content: stretch;
31
33
  }
32
34
 
33
- /* ==========================================================================
34
- 2. PANNELLI E DATA-BOX
35
- ========================================================================== */
36
35
  .side-panel {
37
36
  display: flex;
38
37
  flex-direction: column;
39
- background: rgba(255, 255, 255, 0.03);
38
+ background: rgba(0, 0, 0, 0.03);
40
39
  border-radius: 12px;
41
40
  height: 100%;
42
41
  overflow: hidden;
@@ -44,6 +43,7 @@ body {
44
43
  }
45
44
 
46
45
  .left-panel { grid-column: 1; }
46
+
47
47
  .center-panel {
48
48
  grid-column: 2;
49
49
  position: relative;
@@ -54,15 +54,19 @@ body {
54
54
  height: 100%;
55
55
  overflow: hidden;
56
56
  }
57
+
57
58
  .right-panel {
58
59
  grid-column: 3;
59
60
  align-items: flex-end;
60
61
  text-align: right;
61
62
  }
62
63
 
64
+ /* ==========================================================================
65
+ 3. DATA-BOX E TIPOGRAFIA
66
+ ========================================================================== */
63
67
  .data-box {
64
68
  position: relative;
65
- border-bottom: 1px solid #222;
69
+ border-bottom: 1px solid #eee;
66
70
  padding: 2px 4px;
67
71
  display: flex;
68
72
  flex-direction: column;
@@ -78,108 +82,74 @@ body {
78
82
  .left-panel .data-box { align-items: flex-start; text-align: left; }
79
83
  .right-panel .data-box { align-items: flex-end; text-align: right; }
80
84
 
81
- /* ==========================================================================
82
- 3. TACTICAL FOCUS MODE (DUAL SCREEN 60/40)
83
- ========================================================================== */
84
-
85
- /* Nasconde i pannelli non attivi */
86
- .focus-active .side-panel:not(.has-focus) { display: none !important; }
85
+ .label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
86
+ .label { color: #888; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
87
+ .unit { color: #aaa; font-size: 0.6rem; font-weight: bold; }
87
88
 
88
- /* Split 60/40: Grafico a Sinistra */
89
- .focus-active.focus-side-left {
90
- grid-template-columns: 3fr 2fr !important;
91
- }
92
- .focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
93
- .focus-active.focus-side-left .center-panel {
94
- grid-column: 2 !important;
95
- justify-content: center !important;
96
- align-items: center !important;
89
+ .value {
90
+ color: #000;
91
+ font-size: clamp(1.2rem, 22cqh, 3rem);
92
+ font-weight: 600;
93
+ line-height: 0.9;
94
+ letter-spacing: -1px;
95
+ padding-bottom: 5px;
97
96
  }
98
97
 
99
- /* Split 60/40: Grafico a Destra */
100
- .focus-active.focus-side-right {
101
- grid-template-columns: 2fr 3fr !important;
98
+ .value-large, .dual-value-container, .value-with-compass {
99
+ margin-top: auto;
100
+ margin-bottom: auto;
102
101
  }
103
- .focus-active.focus-side-right .center-panel {
104
- grid-column: 1 !important;
105
- justify-content: center !important;
106
- align-items: center !important;
102
+
103
+ .value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
104
+
105
+ .value-with-compass {
106
+ display: flex;
107
+ justify-content: space-between;
108
+ align-items: center;
109
+ width: 100%;
110
+ align-self: stretch;
111
+ gap: 5px;
107
112
  }
113
+
114
+ .dual-value-container { display: flex; justify-content: space-between; width: 100%; }
115
+ .dual-value-col { display: flex; flex-direction: column; width: 48%; }
116
+ .dual-label { color: #888; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
117
+ .value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
118
+
119
+ /* ==========================================================================
120
+ 4. TACTICAL FOCUS MODE
121
+ ========================================================================== */
122
+ .focus-active .side-panel:not(.has-focus) { display: none !important; }
123
+ .focus-active.focus-side-left { grid-template-columns: 3fr 2fr !important; }
124
+ .focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
125
+ .focus-active.focus-side-left .center-panel { grid-column: 2 !important; justify-content: center !important; align-items: center !important; }
126
+ .focus-active.focus-side-right { grid-template-columns: 2fr 3fr !important; }
127
+ .focus-active.focus-side-right .center-panel { grid-column: 1 !important; justify-content: center !important; align-items: center !important; }
108
128
  .focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
109
129
 
110
- /* Espansione Box Focalizzato */
111
130
  .focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
112
131
  .focus-active .has-focus .data-box.is-focused {
113
132
  height: 100vh !important;
114
133
  border: none;
115
- background: rgba(255, 255, 255, 0.05);
134
+ background: rgba(0, 0, 0, 0.02);
116
135
  padding: 2vw 3vw !important;
117
136
  display: flex;
118
137
  flex-direction: column;
119
138
  }
120
139
 
121
- /* Tipografia Massiccia Focus Mode */
122
140
  .focus-active .is-focused .value { font-size: clamp(4rem, 25cqh, 10rem) !important; margin-top: 15px; }
123
141
  .focus-active .is-focused .scale-labels { font-size: clamp(14px, 1.8vw, 22px) !important; min-width: 50px !important; }
124
142
  .focus-active .is-focused .label-row .label { font-size: 2rem !important; }
125
143
  .focus-active .is-focused .label-row .unit { font-size: 2rem !important; }
126
-
127
144
  .focus-active .is-focused .sparkline path { stroke-width: 2.5px !important; }
128
-
129
- /* Protezione Wind Gauge nel 40% di spazio */
130
- .focus-active .center-panel svg#wind-gauge {
131
- max-width: 95% !important;
132
- max-height: 85vh !important;
133
- }
134
-
135
- /* ==========================================================================
136
- 4. TIPOGRAFIA DINAMICA E ELASTICA
137
- ========================================================================== */
138
- .label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
139
-
140
- .label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
141
- .unit { color: #888; font-size: 0.6rem; font-weight: bold; }
142
-
143
- .value {
144
- color: #fff;
145
- font-size: clamp(1.2rem, 22cqh, 3rem);
146
- font-weight: 600;
147
- line-height: 0.9;
148
- letter-spacing: -1px;
149
- padding-bottom: 5px;
150
- }
151
-
152
- .value-large, .dual-value-container, .value-with-compass { margin-top: auto; margin-bottom: auto; }
153
- .value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
154
-
155
- .dual-value-container { display: flex; justify-content: space-between; width: 100%; }
156
- .dual-value-col { display: flex; flex-direction: column; width: 48%; }
157
- .dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
158
- .value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
159
-
160
- /* ==========================================================================
161
- 5. WIDGETS (BUSSOLA TWD)
162
- ========================================================================== */
163
- .value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 5px; }
164
-
165
- .mini-compass {
166
- width: min(80cqh, 42cqw);
167
- height: min(80cqh, 42cqw);
168
- aspect-ratio: 1 / 1;
169
- flex-shrink: 0;
170
- background: #000;
171
- border-radius: 50%;
172
- border: 1.5px solid #333;
173
- box-shadow: inset 0 0 10px rgba(0,0,0,0.8);
174
- transition: all 0.4s ease;
175
- }
145
+ .focus-active .center-panel svg#wind-gauge { max-width: 95% !important; max-height: 85vh !important; }
176
146
 
177
147
  /* ==========================================================================
178
- 6. GRAFICI E SCALE
179
- ========================================================================== */
148
+ 5. GRAFICI, SCALE E HERCULES MODE
149
+ ========================================================================= */
180
150
  .graph-wrapper {
181
151
  position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px;
182
- display: flex; align-items: stretch; background: rgba(255, 255, 255, 0.03);
152
+ display: flex; align-items: stretch; background: rgba(0, 0, 0, 0.04);
183
153
  border-radius: 4px; gap: 0px !important;
184
154
  }
185
155
 
@@ -191,7 +161,7 @@ body {
191
161
 
192
162
  .scale-labels {
193
163
  display: flex; flex-direction: column; justify-content: space-between;
194
- font-size: 8px; color: #777; font-weight: bold; min-width: 12px;
164
+ font-size: 8px; color: #444; font-weight: bold; min-width: 12px;
195
165
  height: 100%; line-height: 1; padding: 0; border: none !important;
196
166
  }
197
167
 
@@ -200,18 +170,9 @@ body {
200
170
  .right-panel .scale-labels { order: 1; text-align: right; padding-right: 4px; }
201
171
  .right-panel .sparkline { order: 2; }
202
172
 
203
- #stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
204
- #sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
205
- #depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
206
- #tws-graph { stroke: #ffffff; fill: rgba(255, 255, 255, 0.08); }
207
-
208
- /* ==========================================================================
209
- 7. HERCULES MODE
210
- ========================================================================== */
211
173
  .line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
212
- .box-hercules { background: rgba(255, 0, 0, 0.08) !important; }
213
- .box-hercules .scale-labels { color: #ff8888; }
214
-
174
+ .box-hercules { background: rgba(255, 0, 0, 0.05) !important; }
175
+ .box-hercules .scale-labels { color: #ff5555; }
215
176
  .box-hercules .unit::before, .box-hercules .unit::after, .box-hercules .label::before {
216
177
  font-size: 7px; color: #ff4444; font-weight: 900; letter-spacing: 1px; text-transform: uppercase;
217
178
  }
@@ -219,32 +180,67 @@ body {
219
180
  .right-panel .box-hercules .unit::after { content: " HERCULES"; }
220
181
  .right-panel .box-hercules .label:only-child::after { content: " HERCULES"; }
221
182
 
183
+ #stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
184
+ #sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
185
+ #depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
186
+ #tws-graph { stroke: #000000; fill: rgba(0, 0, 0, 0.08); }
187
+
222
188
  /* ==========================================================================
223
- 8. STATUS E ANIMAZIONI
189
+ 6. WIDGETS E STATUS (GIORNO)
224
190
  ========================================================================== */
225
191
  #status {
226
192
  position: absolute; top: 10px; right: 10px;
227
193
  font-size: 0.6rem; font-weight: 900;
228
194
  text-transform: uppercase; z-index: 1000;
229
- letter-spacing: 1px; background: rgba(0,0,0,0.4);
195
+ letter-spacing: 1px; background: rgba(0,0,0,0.05);
230
196
  padding: 2px 6px; border-radius: 4px;
231
197
  }
232
- .online { color: #2ecc71; opacity: 0.5; }
233
- .offline { color: #e74c3c; font-weight: bold; }
198
+ .online { color: #27ae60; opacity: 0.8; }
199
+ .offline { color: #c0392b; font-weight: bold; }
200
+
201
+ .mini-compass {
202
+ width: min(80cqh, 42cqw); height: min(80cqh, 42cqw); aspect-ratio: 1 / 1;
203
+ flex-shrink: 0; background: #f0f0f0;
204
+ border-radius: 50%; border: 1.5px solid #ddd;
205
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.05); transition: all 0.4s ease;
206
+ }
207
+
208
+ /* Inversione elementi Mini Bussola TWD - Giorno */
209
+ .mini-compass text:last-of-type { fill: #000 !important; opacity: 0.4; } /* La "S" nera */
210
+ #twd-boat-wrap path { fill: #000 !important; opacity: 0.3; } /* Barca interna nera */
211
+ #twd-wind-chevron { stroke: #000 !important; } /* Forza il triangolino vento a nero in giorno */
234
212
 
235
213
  #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow, #twd-boat-wrap {
236
214
  transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
237
215
  }
238
216
 
239
- .unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
240
- .alarm-warning { color: #f1c40f !important; }
217
+ #wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
218
+
219
+ /* Quadrante Vento Centrale - Giorno */
220
+ #wind-gauge circle[fill="#050505"] { fill: #fcfcfc; }
221
+ #wind-gauge circle[fill="#121212"] { fill: #f0f0f0; }
222
+ #boat-icon { fill: #000 !important; opacity: 1; }
223
+ #tick-labels { fill: #444 !important; }
224
+ #aws-val-svg { fill: #000 !important; }
225
+ #aws-display-group text { fill: #333 !important; }
226
+ #fullscreen-hotspot { fill: #eee !important; stroke: #ccc !important; }
227
+ #ticks line[stroke="#fff"] { stroke: #000 !important; }
228
+ #ticks line[stroke="#666"] { stroke: #bbb !important; }
229
+
230
+ /* Leeway Container - Giorno */
231
+ rect[fill="#222"] { fill: #eee !important; }
232
+ #leeway-val { color: #000 !important; }
233
+
234
+ /* ==========================================================================
235
+ 7. STATI DI ALLARME
236
+ ========================================================================== */
237
+ .unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #e67e22 !important; }
238
+ .alarm-warning { color: #f39c12 !important; }
241
239
  .alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
242
240
  @keyframes blink-unstable { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
243
241
 
244
- #wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
245
-
246
242
  /* ==========================================================================
247
- 9. RESPONSIVE (PORTRAIT)
243
+ 8. RESPONSIVE
248
244
  ========================================================================== */
249
245
  @media (max-aspect-ratio: 0.9 / 1) {
250
246
  .main-container { grid-template-columns: 1fr 1fr !important; grid-template-rows: 45vh calc(55vh - 8px) !important; }
@@ -258,10 +254,10 @@ body {
258
254
  }
259
255
 
260
256
  /* ==========================================================================
261
- 10. NIGHT MODE (TACTICAL RED)
257
+ 9. NIGHT MODE (TACTICAL RED)
262
258
  ========================================================================== */
263
259
  body.night-mode { background-color: #000 !important; color: #ff0000 !important; }
264
- .night-mode .side-panel { background: rgba(20, 0, 0, 0.4); border: 1px solid #330000; }
260
+ .night-mode .side-panel { background: rgba(20, 0, 0, 0.4) !important; border: 1px solid #330000; }
265
261
  .night-mode .data-box { border-bottom-color: #260000; }
266
262
  .night-mode .label, .night-mode .unit, .night-mode .dual-label { color: #800000 !important; }
267
263
  .night-mode .value, .night-mode .value-large { color: #ff3333 !important; text-shadow: 0 0 8px rgba(255, 0, 0, 0.4); }
@@ -269,43 +265,50 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
269
265
  .night-mode .graph-wrapper { background: rgba(30, 0, 0, 0.3) !important; }
270
266
  .night-mode .sparkline path:first-of-type { display: none !important; }
271
267
  .night-mode .sparkline path { fill: none !important; stroke: #ff3333 !important; stroke-width: 1.8px !important; opacity: 1 !important; filter: drop-shadow(0 0 2px rgba(255, 0, 0, 0.4)); }
272
- .night-mode .sparkline line { stroke: #4d0000 !important; stroke-width: 0.7px !important; }
268
+
269
+ .night-mode .sparkline line { stroke: rgba(255, 0, 0, 0.15) !important; stroke-width: 0.7px !important; }
273
270
  .night-mode #tws-graph line:not([stroke*="rgba"]) { stroke: #ff3333 !important; stroke-width: 1.8px !important; }
274
271
  .night-mode .scale-labels { color: #660000 !important; }
275
272
 
273
+ .night-mode .box-hercules { background: rgba(60, 0, 0, 0.2) !important; }
274
+ .night-mode .line-hercules { filter: drop-shadow(0 0 6px #ff0000) !important; }
275
+
276
+ /* Fix Bussola Centrale Night */
276
277
  .night-mode #wind-gauge circle { stroke: #330000; }
278
+ .night-mode #wind-gauge circle[fill="#050505"] { fill: #050505 !important; }
279
+ .night-mode #wind-gauge circle[fill="#121212"] { fill: #121212 !important; }
280
+ .night-mode #fullscreen-hotspot { fill: #000 !important; stroke: #330000 !important; }
281
+
277
282
  .night-mode #ticks line { stroke: #4d0000 !important; }
278
283
  .night-mode #tick-labels { fill: #800000 !important; }
279
- .night-mode #boat-icon { fill: #330000 !important; opacity: 0.6; }
284
+ .night-mode #boat-icon { fill: #330000 !important; opacity: 0.6 !important; }
280
285
  .night-mode #aws-val-svg { fill: #ff3333 !important; }
281
286
  .night-mode #aws-display-group text { fill: #ff3333 !important; }
282
287
  .night-mode #center-glow feDropShadow { flood-color: #ff0000 !important; flood-opacity: 0.6 !important; }
283
288
 
284
- /* Settori Vento Night */
289
+ /* Bussola TWD Night */
290
+ .night-mode .mini-compass { border-color: #330000; background: #000; }
291
+ .night-mode .mini-compass text { fill: #800000 !important; }
292
+ .night-mode .mini-compass text:last-of-type { fill: #800000 !important; opacity: 1; } /* La "S" torna rossa */
293
+ .night-mode #twd-boat-wrap path { fill: #ff3333 !important; opacity: 0.4 !important; } /* Barca torna rossa */
294
+ .night-mode #twd-arrow #twd-wind-chevron { stroke: #ff3333 !important; stroke-width: 3px !important; opacity: 1 !important; filter: drop-shadow(0 0 4px #ff0000); transform-origin: center; }
295
+
296
+ /* Vento e Leeway Night */
285
297
  .night-mode #wind-gauge path[stroke="#ff0000"] { stroke: #660000 !important; opacity: 0.8; }
286
298
  .night-mode #wind-gauge path[stroke="#00ff00"] { stroke: #660000 !important; stroke-dasharray: 4, 3; opacity: 0.8; }
287
299
  .night-mode #wind-gauge path[stroke="#ff8800"] { stroke: #330000 !important; stroke-width: 8; }
288
-
289
- /* Lancette Night */
290
- .night-mode #awa-pointer path { fill: #ff0000; stroke: #000; }
291
- .night-mode #twa-pointer path { fill: #800000; stroke: #000; }
292
- .night-mode #track-pointer path { fill: #ff0000; stroke: #fff; stroke-width: 0.5; }
293
-
294
- /* Leeway Night */
295
300
  .night-mode rect[fill="url(#leeway-grad)"] { fill: url(#leeway-night-grad) !important; }
296
301
  .night-mode #leeway-val { fill: #ff3333 !important; }
297
302
  .night-mode g[stroke="#555"] line { stroke: #4d0000 !important; }
298
303
  .night-mode g[fill="#555"] text { fill: #660000 !important; }
299
304
  .night-mode rect[fill="#222"] { fill: #0a0000 !important; stroke: #200000; }
300
305
 
301
- /* Bussola TWD Night */
302
- .night-mode .mini-compass { border-color: #330000; background: #000; }
303
- .night-mode .mini-compass text { fill: #800000 !important; }
304
- .night-mode #twd-arrow #twd-wind-chevron { stroke: #ff3333 !important; stroke-width: 3px !important; opacity: 1 !important; filter: drop-shadow(0 0 4px #ff0000); transform-origin: center; }
305
- .night-mode #twd-boat-wrap path { fill: #ff3333 !important; opacity: 0.4 !important; }
306
+ .night-mode #awa-pointer path { fill: #ff0000; stroke: #000; }
307
+ .night-mode #twa-pointer path { fill: #800000; stroke: #000; }
308
+ .night-mode #track-pointer path { fill: #ff0000; stroke: #fff; stroke-width: 0.5; }
306
309
 
307
310
  /* ==========================================================================
308
- 11. TREND VENTO E ALLARME STRAMBATA (GYBE)
311
+ 10. TREND VENTO E ALLARME STRAMBATA
309
312
  ========================================================================== */
310
313
  @keyframes blink-trend { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
311
314
 
@@ -314,4 +317,13 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
314
317
  transition: opacity 0.3s ease;
315
318
  }
316
319
 
317
- .is-trending { a
320
+ .is-trending {
321
+ opacity: 1 !important;
322
+ animation: blink-trend 1s infinite !important;
323
+ }
324
+
325
+ .is-gybing {
326
+ opacity: 1 !important;
327
+ animation: none !important;
328
+ filter: drop-shadow(0 0 8px #ff0000) !important;
329
+ }
package/test.html CHANGED
@@ -68,4 +68,3 @@
68
68
  </script>
69
69
  </body>
70
70
  </html>
71
- ---Fine Codice ---