@sailingrotevista/rotevista-dash 2.0.19 → 2.0.20
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 +78 -66
- package/index.html +17 -9
- package/package.json +1 -1
- package/style.css +109 -103
package/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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 },
|
|
@@ -22,7 +22,7 @@ const SIM_SAMPLE_INTERVAL = 1000;
|
|
|
22
22
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
23
23
|
// ==========================================================================
|
|
24
24
|
let simulationMode = false;
|
|
25
|
-
let displayModeSog = 'SOG';
|
|
25
|
+
let displayModeSog = 'SOG';
|
|
26
26
|
let socket, renderInterval, simInterval;
|
|
27
27
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
28
28
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
@@ -66,7 +66,7 @@ const ui = {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
// ==========================================================================
|
|
69
|
-
// 3. UTILITIES
|
|
69
|
+
// 3. UTILITIES (MATEMATICA, BUFFER, AUDIO)
|
|
70
70
|
// ==========================================================================
|
|
71
71
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
72
72
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
@@ -74,17 +74,11 @@ function msToKts(ms) { return ms * 1.94384; }
|
|
|
74
74
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
75
75
|
function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
* Gestione sicura dei buffer per prevenire memory leak (O(1) complexity).
|
|
79
|
-
*/
|
|
80
77
|
function safePush(buffer, val, time, maxLen = 200) {
|
|
81
78
|
buffer.push({ val: val, time: time });
|
|
82
79
|
if (buffer.length > maxLen) { buffer.shift(); }
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
/**
|
|
86
|
-
* Calcola medie circolari restituendo Radianti.
|
|
87
|
-
*/
|
|
88
82
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
89
83
|
const now = Date.now();
|
|
90
84
|
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
@@ -97,14 +91,35 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
|
97
91
|
return { val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI), stable: isStable };
|
|
98
92
|
}
|
|
99
93
|
|
|
94
|
+
function playBingBing() {
|
|
95
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
96
|
+
const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n;
|
|
97
|
+
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); }
|
|
98
|
+
b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function playGybeAlarm() {
|
|
102
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
103
|
+
const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
|
|
104
|
+
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); }
|
|
105
|
+
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); }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function checkDepthAlarm(m) {
|
|
109
|
+
ui.depth.classList.remove('alarm-warning', 'alarm-danger');
|
|
110
|
+
if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); }
|
|
111
|
+
else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function updateLeewayDisplay(deg) {
|
|
115
|
+
const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125);
|
|
116
|
+
ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w);
|
|
117
|
+
ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`;
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
// ==========================================================================
|
|
101
|
-
// 4.
|
|
121
|
+
// 4. MOTORE DI CALCOLO VENTO E DATA ROUTING
|
|
102
122
|
// ==========================================================================
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Calcola il Vento Reale partendo dagli Apparenti.
|
|
106
|
-
* Logica Acqua (Vele) vs Terra (Meteo).
|
|
107
|
-
*/
|
|
108
123
|
function computeTrueWind() {
|
|
109
124
|
const aws = store.raw["environment.wind.speedApparent"];
|
|
110
125
|
let awa = store.raw["environment.wind.angleApparent"];
|
|
@@ -114,21 +129,27 @@ function computeTrueWind() {
|
|
|
114
129
|
if (aws === undefined || awa === undefined) return;
|
|
115
130
|
if (awa > Math.PI) awa -= 2 * Math.PI;
|
|
116
131
|
|
|
117
|
-
// Vento Reale su ACQUA (TWA/TWS)
|
|
118
132
|
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)
|
|
133
|
+
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
120
134
|
|
|
121
|
-
// Vento Reale su TERRA (TWD)
|
|
122
135
|
const drift_angle = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
|
|
123
136
|
const sog_vec_x = sog * Math.cos(drift_angle), sog_vec_y = sog * Math.sin(drift_angle);
|
|
124
137
|
const tw_ground_x = aws * Math.cos(awa) - sog_vec_x, tw_ground_y = aws * Math.sin(awa) - sog_vec_y;
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
const tws_ground = Math.sqrt(tw_ground_x * tw_ground_x + tw_ground_y * tw_ground_y);
|
|
139
|
+
|
|
127
140
|
const now = Date.now();
|
|
128
|
-
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
141
|
+
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
129
142
|
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
if (tws_water > 0.05) {
|
|
144
|
+
const twa_water = Math.atan2(tw_water_y, tw_water_x);
|
|
145
|
+
store.raw["environment.wind.angleTrueWater"] = twa_water;
|
|
146
|
+
safePush(store.smoothBuf.twa, twa_water, now); safePush(store.longBuf.twa, twa_water, now);
|
|
147
|
+
}
|
|
148
|
+
if (tws_ground > 0.05) {
|
|
149
|
+
let twd_ground = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
|
|
150
|
+
store.raw["environment.wind.directionTrue"] = twd_ground;
|
|
151
|
+
safePush(store.smoothBuf.twd, twd_ground, now); safePush(store.longBuf.twd, twd_ground, now);
|
|
152
|
+
}
|
|
132
153
|
}
|
|
133
154
|
|
|
134
155
|
function processIncomingData(path, val) {
|
|
@@ -136,7 +157,6 @@ function processIncomingData(path, val) {
|
|
|
136
157
|
if (path === "navigation.position") store.raw["navigation.position"] = val;
|
|
137
158
|
if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
|
|
138
159
|
|
|
139
|
-
// Dirty flag + Debounce 100ms
|
|
140
160
|
const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
|
|
141
161
|
if (twPaths.includes(path)) twDirty = true;
|
|
142
162
|
if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
|
|
@@ -146,14 +166,7 @@ function processIncomingData(path, val) {
|
|
|
146
166
|
}
|
|
147
167
|
|
|
148
168
|
// ==========================================================================
|
|
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
|
|
169
|
+
// 5. TREND VENTO E SICUREZZA
|
|
157
170
|
// ==========================================================================
|
|
158
171
|
function updateWindTrend() {
|
|
159
172
|
const now = Date.now();
|
|
@@ -167,20 +180,18 @@ function updateWindTrend() {
|
|
|
167
180
|
if (lastShortAvgVal === null) { lastShortAvgVal = shortAvgDeg; lastInstantTwa = instantTwaDeg; return; }
|
|
168
181
|
const dt = (now - lastTrendTime) / 1000; lastTrendTime = now;
|
|
169
182
|
|
|
170
|
-
// Gybe Safety
|
|
171
183
|
const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
|
|
172
184
|
lastInstantTwa = instantTwaDeg;
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
|
|
175
186
|
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
176
187
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
177
188
|
|
|
189
|
+
if (gybeDetected && isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
|
|
178
190
|
if (now - lastGybeAlarmTime < 4000 && isNavigating) {
|
|
179
191
|
[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
192
|
return;
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
// Trend calculation con Clamping e Alpha adattivo
|
|
184
195
|
let diff = (shortAvgDeg - lastShortAvgVal + 540) % 360 - 180; lastShortAvgVal = shortAvgDeg;
|
|
185
196
|
if (dt > 0) {
|
|
186
197
|
let rate = Math.max(-10, Math.min(10, diff / dt));
|
|
@@ -211,10 +222,8 @@ function updateWindTrend() {
|
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
// ==========================================================================
|
|
214
|
-
//
|
|
225
|
+
// 6. RENDERING ENGINE (TIERED)
|
|
215
226
|
// ==========================================================================
|
|
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
227
|
function refreshGraph(t) {
|
|
219
228
|
const type = (t === 'vmg') ? 'sog' : t;
|
|
220
229
|
const data = store.histories[t]; if (!data || data.length < 2) return;
|
|
@@ -229,13 +238,13 @@ function startDisplayLoop() {
|
|
|
229
238
|
renderInterval = setInterval(() => {
|
|
230
239
|
const now = Date.now(); tick++;
|
|
231
240
|
|
|
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
241
|
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0), sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
237
242
|
isNavigating = stwKts > CONFIG.averages.minSpeed || sogKts > CONFIG.averages.minSpeed;
|
|
238
243
|
|
|
244
|
+
// LIVE TIER (1s)
|
|
245
|
+
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 };
|
|
246
|
+
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]; } }
|
|
247
|
+
|
|
239
248
|
if (store.raw["navigation.speedThroughWater"] !== undefined) { ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts); }
|
|
240
249
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
241
250
|
const twaRad = store.raw["environment.wind.angleTrueWater"], vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
|
|
@@ -244,36 +253,32 @@ function startDisplayLoop() {
|
|
|
244
253
|
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'; }
|
|
245
254
|
}
|
|
246
255
|
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) {
|
|
256
|
+
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" : "#fff"); manageHistory('tws', msToKts(store.raw["environment.wind.speedTrue"])); }
|
|
248
257
|
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
249
258
|
|
|
250
|
-
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa,
|
|
259
|
+
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
|
|
251
260
|
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
252
|
-
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa,
|
|
261
|
+
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
|
|
253
262
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
254
263
|
|
|
255
264
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
256
265
|
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
257
266
|
if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
258
267
|
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
259
|
-
|
|
268
|
+
ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#f39c12" : "#fff";
|
|
260
269
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
261
270
|
}
|
|
262
|
-
|
|
263
|
-
updateWindTrend(); // Fast update
|
|
264
271
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
272
|
+
updateWindTrend();
|
|
273
|
+
|
|
274
|
+
// HEAVY TIER (2s)
|
|
275
|
+
if (tick % 2 === 0) { refreshGraph('stw'); refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog'); refreshGraph('depth'); refreshGraph('tws'); }
|
|
269
276
|
|
|
270
|
-
//
|
|
277
|
+
// SLOW TIER (3s)
|
|
271
278
|
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);
|
|
279
|
+
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, 60000, false), cObj = getCircularAverageFromBuffer(store.longBuf.cog, 60000, false),
|
|
280
|
+
awObj = getCircularAverageFromBuffer(store.longBuf.awa, 60000, true), twObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true),
|
|
281
|
+
twdObj = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
277
282
|
|
|
278
283
|
const upUI = (el, obj, isCompass = false) => {
|
|
279
284
|
if (!obj || obj.val === null) { el.innerHTML = "---°"; el.classList.remove('unstable-data'); }
|
|
@@ -283,11 +288,12 @@ function startDisplayLoop() {
|
|
|
283
288
|
}
|
|
284
289
|
};
|
|
285
290
|
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
|
-
|
|
291
|
+
|
|
292
|
+
if (hObj && twObj) {
|
|
293
|
+
const tackHdgDeg = radToDeg((hObj.val - twObj.val * 2 + Math.PI * 2) % (Math.PI * 2));
|
|
288
294
|
ui.tackHdg.innerHTML = `${Math.round((tackHdgDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
289
295
|
if (cObj) {
|
|
290
|
-
const tackCogDeg = radToDeg((cObj.val -
|
|
296
|
+
const tackCogDeg = radToDeg((cObj.val - twObj.val * 2 + Math.PI * 2) % (Math.PI * 2));
|
|
291
297
|
ui.tackCog.innerHTML = `${Math.round((tackCogDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
292
298
|
}
|
|
293
299
|
}
|
|
@@ -296,13 +302,14 @@ function startDisplayLoop() {
|
|
|
296
302
|
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwd.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
297
303
|
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdg.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
298
304
|
}
|
|
305
|
+
lastAvgUIUpdate = now;
|
|
299
306
|
}
|
|
300
307
|
if (tick % 60 === 0) tick = 0;
|
|
301
308
|
}, RENDER_INTERVAL_MS);
|
|
302
309
|
}
|
|
303
310
|
|
|
304
311
|
// ==========================================================================
|
|
305
|
-
//
|
|
312
|
+
// 7. CONFIGURAZIONE E GRAFICI UTILS
|
|
306
313
|
// ==========================================================================
|
|
307
314
|
async function fetchServerConfig() {
|
|
308
315
|
if (!window.location.protocol.includes("http")) return;
|
|
@@ -314,7 +321,7 @@ async function fetchServerConfig() {
|
|
|
314
321
|
if (response.ok) {
|
|
315
322
|
const data = await response.json();
|
|
316
323
|
const actual = data.configuration || data;
|
|
317
|
-
if (actual
|
|
324
|
+
if (actual) {
|
|
318
325
|
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
326
|
parseNumbers(actual);
|
|
320
327
|
if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
|
|
@@ -359,6 +366,9 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
359
366
|
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}" />`;
|
|
360
367
|
}
|
|
361
368
|
|
|
369
|
+
// ==========================================================================
|
|
370
|
+
// 8. INTERAZIONI E RETE
|
|
371
|
+
// ==========================================================================
|
|
362
372
|
function toggleFocusMode(type, element) {
|
|
363
373
|
const container = document.querySelector('.main-container'); const parentPanel = element.closest('.side-panel'); const isLeft = parentPanel.classList.contains('left-panel');
|
|
364
374
|
isFocusActive = !isFocusActive;
|
|
@@ -401,7 +411,7 @@ ui.depth.closest('.data-box').addEventListener('click', (function() { let dC = 0
|
|
|
401
411
|
|
|
402
412
|
function startDynamicSimulation() {
|
|
403
413
|
ui.status.innerText = "SIM ATTIVO";
|
|
404
|
-
let sim = { hdg: 45, tws: 12, twd:
|
|
414
|
+
let sim = { hdg: 45, tws: 12, twd: 90, depth: 12, stw: 5, leeway: 0, currentSpeed: 1.5, currentDir: 90, startTime: Date.now() };
|
|
405
415
|
simInterval = setInterval(() => {
|
|
406
416
|
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
407
417
|
if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
|
|
@@ -411,9 +421,11 @@ function startDynamicSimulation() {
|
|
|
411
421
|
let twaRel = (sim.twd - sim.hdg + 360) % 360; if (twaRel > 180) twaRel -= 360;
|
|
412
422
|
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
423
|
const rawLeeway = Math.sin(degToRad(twaRel)) * 4; sim.leeway += (rawLeeway - sim.leeway) * 0.05;
|
|
424
|
+
|
|
414
425
|
const bX = sim.stw * Math.sin(degToRad(sim.hdg + sim.leeway)), bY = sim.stw * Math.cos(degToRad(sim.hdg + sim.leeway));
|
|
415
426
|
const cX = sim.currentSpeed * Math.sin(degToRad(sim.currentDir)), cY = sim.currentSpeed * Math.cos(degToRad(sim.currentDir));
|
|
416
427
|
const sog = Math.sqrt(Math.pow(bX + cX, 2) + Math.pow(bY + cY, 2)), cog = (radToDeg(Math.atan2(bX + cX, bY + cY)) + 360) % 360;
|
|
428
|
+
|
|
417
429
|
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
430
|
const awa = Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad));
|
|
419
431
|
processIncomingData("environment.wind.speedApparent", ktsToMs(aws)); processIncomingData("environment.wind.angleApparent", awa);
|
|
@@ -424,7 +436,7 @@ function startDynamicSimulation() {
|
|
|
424
436
|
}
|
|
425
437
|
|
|
426
438
|
// ==========================================================================
|
|
427
|
-
//
|
|
439
|
+
// 10. INIT
|
|
428
440
|
// ==========================================================================
|
|
429
441
|
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
430
442
|
(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); } } })();
|
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/package.json
CHANGED
package/style.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
1. BASE E
|
|
2
|
+
1. BASE E RESET DI SISTEMA
|
|
3
3
|
========================================================================== */
|
|
4
4
|
body {
|
|
5
5
|
background-color: #000;
|
|
@@ -10,12 +10,16 @@ body {
|
|
|
10
10
|
height: 100vh;
|
|
11
11
|
width: 100vw;
|
|
12
12
|
overflow: hidden;
|
|
13
|
+
/* Inibisce i gesti di sistema per favorire Long Press e Swipe della dashboard */
|
|
13
14
|
-webkit-touch-callout: none;
|
|
14
15
|
-webkit-user-select: none;
|
|
15
16
|
user-select: none;
|
|
16
17
|
touch-action: none;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/* ==========================================================================
|
|
21
|
+
2. LAYOUT PRINCIPALE (LIQUID GRID)
|
|
22
|
+
========================================================================== */
|
|
19
23
|
.main-container {
|
|
20
24
|
display: grid;
|
|
21
25
|
width: 100%;
|
|
@@ -23,16 +27,13 @@ body {
|
|
|
23
27
|
padding: 5px;
|
|
24
28
|
box-sizing: border-box;
|
|
25
29
|
gap: 8px;
|
|
26
|
-
/* Rapporto Standard: Lati
|
|
30
|
+
/* Rapporto Standard: Lati flessibili, Centro bilanciato a 1.5fr */
|
|
27
31
|
grid-template-columns: minmax(180px, 1fr) minmax(auto, 1.5fr) minmax(180px, 1fr);
|
|
28
32
|
grid-template-rows: 100%;
|
|
29
33
|
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
30
34
|
justify-content: stretch;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
/* ==========================================================================
|
|
34
|
-
2. PANNELLI E DATA-BOX
|
|
35
|
-
========================================================================== */
|
|
36
37
|
.side-panel {
|
|
37
38
|
display: flex;
|
|
38
39
|
flex-direction: column;
|
|
@@ -40,13 +41,14 @@ body {
|
|
|
40
41
|
border-radius: 12px;
|
|
41
42
|
height: 100%;
|
|
42
43
|
overflow: hidden;
|
|
43
|
-
gap: 2px;
|
|
44
|
+
gap: 2px; /* Massimizza lo spazio per i 5 box verticali */
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
.left-panel { grid-column: 1; }
|
|
48
|
+
|
|
47
49
|
.center-panel {
|
|
48
50
|
grid-column: 2;
|
|
49
|
-
position: relative;
|
|
51
|
+
position: relative; /* Indispensabile per posizionare l'etichetta STATUS in alto a dx */
|
|
50
52
|
display: flex;
|
|
51
53
|
flex-direction: column;
|
|
52
54
|
align-items: center;
|
|
@@ -54,12 +56,16 @@ body {
|
|
|
54
56
|
height: 100%;
|
|
55
57
|
overflow: hidden;
|
|
56
58
|
}
|
|
59
|
+
|
|
57
60
|
.right-panel {
|
|
58
61
|
grid-column: 3;
|
|
59
62
|
align-items: flex-end;
|
|
60
63
|
text-align: right;
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
/* ==========================================================================
|
|
67
|
+
3. DATA-BOX E TIPOGRAFIA (UNITA' ELASTICHE CQH/CLAMP)
|
|
68
|
+
========================================================================== */
|
|
63
69
|
.data-box {
|
|
64
70
|
position: relative;
|
|
65
71
|
border-bottom: 1px solid #222;
|
|
@@ -68,78 +74,22 @@ body {
|
|
|
68
74
|
flex-direction: column;
|
|
69
75
|
width: 100%;
|
|
70
76
|
box-sizing: border-box;
|
|
71
|
-
container-type: size;
|
|
72
|
-
flex: 1 1 0px;
|
|
77
|
+
container-type: size; /* Permette l'uso di cqh per i font */
|
|
78
|
+
flex: 1 1 0px; /* Distribuzione equa dello spazio verticale disponibile */
|
|
73
79
|
min-height: 0;
|
|
74
80
|
overflow: hidden;
|
|
75
81
|
transition: background-color 0.2s ease;
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
/* Allineamento speculare dei testi tra colonna SX e DX */
|
|
78
85
|
.left-panel .data-box { align-items: flex-start; text-align: left; }
|
|
79
86
|
.right-panel .data-box { align-items: flex-end; text-align: right; }
|
|
80
87
|
|
|
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; }
|
|
87
|
-
|
|
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;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/* Split 60/40: Grafico a Destra */
|
|
100
|
-
.focus-active.focus-side-right {
|
|
101
|
-
grid-template-columns: 2fr 3fr !important;
|
|
102
|
-
}
|
|
103
|
-
.focus-active.focus-side-right .center-panel {
|
|
104
|
-
grid-column: 1 !important;
|
|
105
|
-
justify-content: center !important;
|
|
106
|
-
align-items: center !important;
|
|
107
|
-
}
|
|
108
|
-
.focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
|
|
109
|
-
|
|
110
|
-
/* Espansione Box Focalizzato */
|
|
111
|
-
.focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
|
|
112
|
-
.focus-active .has-focus .data-box.is-focused {
|
|
113
|
-
height: 100vh !important;
|
|
114
|
-
border: none;
|
|
115
|
-
background: rgba(255, 255, 255, 0.05);
|
|
116
|
-
padding: 2vw 3vw !important;
|
|
117
|
-
display: flex;
|
|
118
|
-
flex-direction: column;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/* Tipografia Massiccia Focus Mode */
|
|
122
|
-
.focus-active .is-focused .value { font-size: clamp(4rem, 25cqh, 10rem) !important; margin-top: 15px; }
|
|
123
|
-
.focus-active .is-focused .scale-labels { font-size: clamp(14px, 1.8vw, 22px) !important; min-width: 50px !important; }
|
|
124
|
-
.focus-active .is-focused .label-row .label { font-size: 2rem !important; }
|
|
125
|
-
.focus-active .is-focused .label-row .unit { font-size: 2rem !important; }
|
|
126
|
-
|
|
127
|
-
.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
88
|
.label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
|
|
139
|
-
|
|
140
89
|
.label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
|
|
141
90
|
.unit { color: #888; font-size: 0.6rem; font-weight: bold; }
|
|
142
91
|
|
|
92
|
+
/* Font dinamico per i valori: massimo 22% dell'altezza del box */
|
|
143
93
|
.value {
|
|
144
94
|
color: #fff;
|
|
145
95
|
font-size: clamp(1.2rem, 22cqh, 3rem);
|
|
@@ -149,33 +99,65 @@ body {
|
|
|
149
99
|
padding-bottom: 5px;
|
|
150
100
|
}
|
|
151
101
|
|
|
152
|
-
|
|
102
|
+
/* Centratura verticale automatica per i box senza grafico */
|
|
103
|
+
.value-large, .dual-value-container, .value-with-compass {
|
|
104
|
+
margin-top: auto;
|
|
105
|
+
margin-bottom: auto;
|
|
106
|
+
}
|
|
107
|
+
|
|
153
108
|
.value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
|
|
154
109
|
|
|
110
|
+
/* --- 5. BUSSOLA TWD (FIX ALLINEAMENTO) --- */
|
|
111
|
+
.value-with-compass {
|
|
112
|
+
display: flex;
|
|
113
|
+
justify-content: space-between; /* Bussola a SX, Valore a DX */
|
|
114
|
+
align-items: center;
|
|
115
|
+
width: 100%;
|
|
116
|
+
align-self: stretch; /* Impedisce al pannello DX di schiacciare il widget a destra */
|
|
117
|
+
gap: 5px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* TACK (Mure opposte) Layout */
|
|
155
121
|
.dual-value-container { display: flex; justify-content: space-between; width: 100%; }
|
|
156
122
|
.dual-value-col { display: flex; flex-direction: column; width: 48%; }
|
|
157
123
|
.dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
|
|
158
124
|
.value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
|
|
159
125
|
|
|
160
126
|
/* ==========================================================================
|
|
161
|
-
|
|
127
|
+
4. TACTICAL FOCUS MODE (DUAL SCREEN 60/40)
|
|
162
128
|
========================================================================== */
|
|
163
|
-
.
|
|
129
|
+
.focus-active .side-panel:not(.has-focus) { display: none !important; }
|
|
164
130
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
131
|
+
/* Split: 60% per il grafico scelto, 40% per il vento centrale */
|
|
132
|
+
.focus-active.focus-side-left { grid-template-columns: 3fr 2fr !important; }
|
|
133
|
+
.focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
|
|
134
|
+
.focus-active.focus-side-left .center-panel { grid-column: 2 !important; justify-content: center !important; align-items: center !important; }
|
|
135
|
+
|
|
136
|
+
.focus-active.focus-side-right { grid-template-columns: 2fr 3fr !important; }
|
|
137
|
+
.focus-active.focus-side-right .center-panel { grid-column: 1 !important; justify-content: center !important; align-items: center !important; }
|
|
138
|
+
.focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
|
|
139
|
+
|
|
140
|
+
/* Espansione box e tipografia massiccia in Focus Mode */
|
|
141
|
+
.focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
|
|
142
|
+
.focus-active .has-focus .data-box.is-focused {
|
|
143
|
+
height: 100vh !important;
|
|
144
|
+
border: none;
|
|
145
|
+
background: rgba(255, 255, 255, 0.05);
|
|
146
|
+
padding: 2vw 3vw !important;
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-direction: column;
|
|
175
149
|
}
|
|
176
150
|
|
|
151
|
+
.focus-active .is-focused .value { font-size: clamp(4rem, 25cqh, 10rem) !important; margin-top: 15px; }
|
|
152
|
+
.focus-active .is-focused .scale-labels { font-size: clamp(14px, 1.8vw, 22px) !important; min-width: 50px !important; }
|
|
153
|
+
.focus-active .is-focused .label-row .label { font-size: 2rem !important; }
|
|
154
|
+
.focus-active .is-focused .label-row .unit { font-size: 2rem !important; }
|
|
155
|
+
.focus-active .is-focused .sparkline path { stroke-width: 2.5px !important; }
|
|
156
|
+
|
|
157
|
+
.focus-active .center-panel svg#wind-gauge { max-width: 95% !important; max-height: 85vh !important; }
|
|
158
|
+
|
|
177
159
|
/* ==========================================================================
|
|
178
|
-
|
|
160
|
+
5. GRAFICI, SCALE E HERCULES MODE
|
|
179
161
|
========================================================================== */
|
|
180
162
|
.graph-wrapper {
|
|
181
163
|
position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px;
|
|
@@ -200,18 +182,10 @@ body {
|
|
|
200
182
|
.right-panel .scale-labels { order: 1; text-align: right; padding-right: 4px; }
|
|
201
183
|
.right-panel .sparkline { order: 2; }
|
|
202
184
|
|
|
203
|
-
|
|
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
|
-
========================================================================== */
|
|
185
|
+
/* Hercules Mode Visuals */
|
|
211
186
|
.line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
|
|
212
187
|
.box-hercules { background: rgba(255, 0, 0, 0.08) !important; }
|
|
213
188
|
.box-hercules .scale-labels { color: #ff8888; }
|
|
214
|
-
|
|
215
189
|
.box-hercules .unit::before, .box-hercules .unit::after, .box-hercules .label::before {
|
|
216
190
|
font-size: 7px; color: #ff4444; font-weight: 900; letter-spacing: 1px; text-transform: uppercase;
|
|
217
191
|
}
|
|
@@ -219,8 +193,14 @@ body {
|
|
|
219
193
|
.right-panel .box-hercules .unit::after { content: " HERCULES"; }
|
|
220
194
|
.right-panel .box-hercules .label:only-child::after { content: " HERCULES"; }
|
|
221
195
|
|
|
196
|
+
/* Colori standard grafici */
|
|
197
|
+
#stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
|
|
198
|
+
#sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
|
|
199
|
+
#depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
|
|
200
|
+
#tws-graph { stroke: #ffffff; fill: rgba(255, 255, 255, 0.08); }
|
|
201
|
+
|
|
222
202
|
/* ==========================================================================
|
|
223
|
-
|
|
203
|
+
6. WIDGETS E STATUS
|
|
224
204
|
========================================================================== */
|
|
225
205
|
#status {
|
|
226
206
|
position: absolute; top: 10px; right: 10px;
|
|
@@ -232,19 +212,28 @@ body {
|
|
|
232
212
|
.online { color: #2ecc71; opacity: 0.5; }
|
|
233
213
|
.offline { color: #e74c3c; font-weight: bold; }
|
|
234
214
|
|
|
215
|
+
.mini-compass {
|
|
216
|
+
width: min(80cqh, 42cqw); height: min(80cqh, 42cqw); aspect-ratio: 1 / 1;
|
|
217
|
+
flex-shrink: 0; background: #000; border-radius: 50%; border: 1.5px solid #333;
|
|
218
|
+
box-shadow: inset 0 0 10px rgba(0,0,0,0.8); transition: all 0.4s ease;
|
|
219
|
+
}
|
|
220
|
+
|
|
235
221
|
#awa-pointer, #twa-pointer, #track-pointer, #twd-arrow, #twd-boat-wrap {
|
|
236
222
|
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
|
237
223
|
}
|
|
238
224
|
|
|
225
|
+
#wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
|
|
226
|
+
|
|
227
|
+
/* ==========================================================================
|
|
228
|
+
7. STATI DI ALLARME E INSTABILITÀ
|
|
229
|
+
========================================================================== */
|
|
239
230
|
.unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
|
|
240
231
|
.alarm-warning { color: #f1c40f !important; }
|
|
241
232
|
.alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
|
|
242
233
|
@keyframes blink-unstable { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
243
234
|
|
|
244
|
-
#wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
|
|
245
|
-
|
|
246
235
|
/* ==========================================================================
|
|
247
|
-
|
|
236
|
+
8. RESPONSIVE (PORTRAIT MODE)
|
|
248
237
|
========================================================================== */
|
|
249
238
|
@media (max-aspect-ratio: 0.9 / 1) {
|
|
250
239
|
.main-container { grid-template-columns: 1fr 1fr !important; grid-template-rows: 45vh calc(55vh - 8px) !important; }
|
|
@@ -258,7 +247,7 @@ body {
|
|
|
258
247
|
}
|
|
259
248
|
|
|
260
249
|
/* ==========================================================================
|
|
261
|
-
|
|
250
|
+
9. NIGHT MODE (TACTICAL RED)
|
|
262
251
|
========================================================================== */
|
|
263
252
|
body.night-mode { background-color: #000 !important; color: #ff0000 !important; }
|
|
264
253
|
.night-mode .side-panel { background: rgba(20, 0, 0, 0.4); border: 1px solid #330000; }
|
|
@@ -266,6 +255,7 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
|
|
|
266
255
|
.night-mode .label, .night-mode .unit, .night-mode .dual-label { color: #800000 !important; }
|
|
267
256
|
.night-mode .value, .night-mode .value-large { color: #ff3333 !important; text-shadow: 0 0 8px rgba(255, 0, 0, 0.4); }
|
|
268
257
|
|
|
258
|
+
/* Grafici Night Mode: Solo linea, no riempimento */
|
|
269
259
|
.night-mode .graph-wrapper { background: rgba(30, 0, 0, 0.3) !important; }
|
|
270
260
|
.night-mode .sparkline path:first-of-type { display: none !important; }
|
|
271
261
|
.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)); }
|
|
@@ -273,6 +263,11 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
|
|
|
273
263
|
.night-mode #tws-graph line:not([stroke*="rgba"]) { stroke: #ff3333 !important; stroke-width: 1.8px !important; }
|
|
274
264
|
.night-mode .scale-labels { color: #660000 !important; }
|
|
275
265
|
|
|
266
|
+
/* Hercules & special in Night */
|
|
267
|
+
.night-mode .box-hercules { background: rgba(60, 0, 0, 0.2) !important; }
|
|
268
|
+
.night-mode .line-hercules { filter: drop-shadow(0 0 6px #ff0000) !important; }
|
|
269
|
+
|
|
270
|
+
/* Wind Gauge Night */
|
|
276
271
|
.night-mode #wind-gauge circle { stroke: #330000; }
|
|
277
272
|
.night-mode #ticks line { stroke: #4d0000 !important; }
|
|
278
273
|
.night-mode #tick-labels { fill: #800000 !important; }
|
|
@@ -281,23 +276,22 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
|
|
|
281
276
|
.night-mode #aws-display-group text { fill: #ff3333 !important; }
|
|
282
277
|
.night-mode #center-glow feDropShadow { flood-color: #ff0000 !important; flood-opacity: 0.6 !important; }
|
|
283
278
|
|
|
284
|
-
/* Settori Vento Night */
|
|
279
|
+
/* Settori Vento Night (Destra tratteggiata) */
|
|
285
280
|
.night-mode #wind-gauge path[stroke="#ff0000"] { stroke: #660000 !important; opacity: 0.8; }
|
|
286
281
|
.night-mode #wind-gauge path[stroke="#00ff00"] { stroke: #660000 !important; stroke-dasharray: 4, 3; opacity: 0.8; }
|
|
287
282
|
.night-mode #wind-gauge path[stroke="#ff8800"] { stroke: #330000 !important; stroke-width: 8; }
|
|
288
283
|
|
|
289
|
-
/*
|
|
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 */
|
|
284
|
+
/* Leeway & Pointer Night */
|
|
295
285
|
.night-mode rect[fill="url(#leeway-grad)"] { fill: url(#leeway-night-grad) !important; }
|
|
296
286
|
.night-mode #leeway-val { fill: #ff3333 !important; }
|
|
297
287
|
.night-mode g[stroke="#555"] line { stroke: #4d0000 !important; }
|
|
298
288
|
.night-mode g[fill="#555"] text { fill: #660000 !important; }
|
|
299
289
|
.night-mode rect[fill="#222"] { fill: #0a0000 !important; stroke: #200000; }
|
|
300
290
|
|
|
291
|
+
.night-mode #awa-pointer path { fill: #ff0000; stroke: #000; }
|
|
292
|
+
.night-mode #twa-pointer path { fill: #800000; stroke: #000; }
|
|
293
|
+
.night-mode #track-pointer path { fill: #ff0000; stroke: #fff; stroke-width: 0.5; }
|
|
294
|
+
|
|
301
295
|
/* Bussola TWD Night */
|
|
302
296
|
.night-mode .mini-compass { border-color: #330000; background: #000; }
|
|
303
297
|
.night-mode .mini-compass text { fill: #800000 !important; }
|
|
@@ -309,9 +303,21 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
|
|
|
309
303
|
========================================================================== */
|
|
310
304
|
@keyframes blink-trend { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
311
305
|
|
|
306
|
+
/* Stato base dei pallini di trend: discreti al 30% */
|
|
312
307
|
#trend-dot-cw, #trend-dot-ccw, #trend-gauge-cw, #trend-gauge-ccw {
|
|
313
308
|
opacity: 0.3;
|
|
314
309
|
transition: opacity 0.3s ease;
|
|
315
310
|
}
|
|
316
311
|
|
|
317
|
-
|
|
312
|
+
/* Quando attivi: brillano al 100% e lampeggiano */
|
|
313
|
+
.is-trending {
|
|
314
|
+
opacity: 1 !important;
|
|
315
|
+
animation: blink-trend 1s infinite !important;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* Allarme Strambata: Rosso fisso con bagliore neon massimo */
|
|
319
|
+
.is-gybing {
|
|
320
|
+
opacity: 1 !important;
|
|
321
|
+
animation: none !important;
|
|
322
|
+
filter: drop-shadow(0 0 8px #ff0000) !important;
|
|
323
|
+
}
|