@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 +158 -103
- package/index.html +17 -9
- package/index.js +2 -2
- package/package.json +1 -1
- package/settings.json +2 -2
- package/style.css +138 -126
- package/test.html +0 -1
package/app.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// ==========================================================================
|
|
2
|
-
// 1. CONFIGURAZIONE E DEFAULT
|
|
2
|
+
// 1. CONFIGURAZIONE E DEFAULT
|
|
3
3
|
// ==========================================================================
|
|
4
4
|
let CONFIG = {
|
|
5
5
|
alarms: { depthDanger: 2.5, depthWarning: 5.0 },
|
|
6
|
-
|
|
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';
|
|
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
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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;
|
|
150
|
+
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
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,
|
|
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
|
-
//
|
|
197
|
+
// ALLARME STRAMBATA
|
|
171
198
|
const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
|
|
172
199
|
lastInstantTwa = instantTwaDeg;
|
|
173
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
let
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.remove('is-trending'); gaugeDots.cw.setAttribute('fill', '#bbb'); }
|
|
207
248
|
}
|
|
208
249
|
} else {
|
|
209
|
-
[
|
|
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
|
-
//
|
|
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') {
|
|
244
|
-
|
|
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) {
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
310
|
+
// SLOW TIER (3s)
|
|
271
311
|
if (tick % 3 === 0) {
|
|
272
|
-
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg,
|
|
273
|
-
|
|
274
|
-
|
|
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 = "---°"; 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
|
-
|
|
287
|
-
|
|
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')}°`;
|
|
289
328
|
if (cObj) {
|
|
290
|
-
const tackCogDeg = radToDeg((cObj.val
|
|
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')}°`;
|
|
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
|
-
//
|
|
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
|
|
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();
|
|
332
|
-
|
|
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 = "";
|
|
347
|
-
|
|
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" : "#
|
|
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': '#
|
|
358
|
-
const colorKey = id === 'sog-graph' && displayModeSog === 'VMG' ? '#
|
|
359
|
-
svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(
|
|
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(
|
|
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:
|
|
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
|
|
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 =
|
|
413
|
-
const
|
|
414
|
-
const
|
|
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));
|
|
477
|
+
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
|
|
423
478
|
}, 1000);
|
|
424
479
|
}
|
|
425
480
|
|
|
426
481
|
// ==========================================================================
|
|
427
|
-
//
|
|
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 ? "#
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<
|
|
170
|
-
|
|
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
|
|
67
|
-
default:
|
|
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
package/settings.json
CHANGED
package/style.css
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
1. BASE E
|
|
2
|
+
1. BASE E RESET DI SISTEMA (GIORNO: SFONDO CHIARO)
|
|
3
3
|
========================================================================== */
|
|
4
4
|
body {
|
|
5
|
-
background-color: #
|
|
6
|
-
color: #
|
|
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(
|
|
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 #
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
.value-large, .dual-value-container, .value-with-compass {
|
|
99
|
+
margin-top: auto;
|
|
100
|
+
margin-bottom: auto;
|
|
102
101
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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: #
|
|
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.
|
|
213
|
-
.box-hercules .scale-labels { color: #
|
|
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
|
-
|
|
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.
|
|
195
|
+
letter-spacing: 1px; background: rgba(0,0,0,0.05);
|
|
230
196
|
padding: 2px 6px; border-radius: 4px;
|
|
231
197
|
}
|
|
232
|
-
.online { color: #
|
|
233
|
-
.offline { color: #
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
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
|
-
|
|
302
|
-
.night-mode
|
|
303
|
-
.night-mode
|
|
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
|
-
|
|
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 {
|
|
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