@sailingrotevista/rotevista-dash 2.0.18 → 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 +29 -9
- package/package.json +1 -1
- package/style.css +116 -107
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
|
@@ -123,10 +123,22 @@
|
|
|
123
123
|
<g id="ticks"></g>
|
|
124
124
|
|
|
125
125
|
<g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="hanging" font-family="Arial" font-weight="bold">
|
|
126
|
+
<!-- Etichette Principali -->
|
|
126
127
|
<text font-size="16" transform="translate(200, 65)">0</text>
|
|
127
128
|
<text font-size="16" transform="translate(335, 200) rotate(90)">90</text>
|
|
128
129
|
<text font-size="16" transform="translate(65, 200) rotate(-90)">90</text>
|
|
129
130
|
<text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
|
|
131
|
+
|
|
132
|
+
<!-- Etichette Intermedie (30-150 Gradi) -->
|
|
133
|
+
<text font-size="11" transform="translate(267.5, 83) rotate(30)">30</text>
|
|
134
|
+
<text font-size="11" transform="translate(317, 132.5) rotate(60)">60</text>
|
|
135
|
+
<text font-size="11" transform="translate(317, 267.5) rotate(120)">120</text>
|
|
136
|
+
<text font-size="11" transform="translate(267.5, 317) rotate(150)">150</text>
|
|
137
|
+
|
|
138
|
+
<text font-size="11" transform="translate(132.5, 83) rotate(-30)">30</text>
|
|
139
|
+
<text font-size="11" transform="translate(83, 132.5) rotate(-60)">60</text>
|
|
140
|
+
<text font-size="11" transform="translate(83, 267.5) rotate(-120)">120</text>
|
|
141
|
+
<text font-size="11" transform="translate(132.5, 317) rotate(-150)">150</text>
|
|
130
142
|
</g>
|
|
131
143
|
|
|
132
144
|
<g id="track-pointer" transform="rotate(0, 200, 200)">
|
|
@@ -146,16 +158,24 @@
|
|
|
146
158
|
<text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
|
|
147
159
|
</g>
|
|
148
160
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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>
|
|
152
167
|
</g>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
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" />
|
|
159
179
|
</g>
|
|
160
180
|
|
|
161
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,33 +10,30 @@ body {
|
|
|
10
10
|
height: 100vh;
|
|
11
11
|
width: 100vw;
|
|
12
12
|
overflow: hidden;
|
|
13
|
-
/*
|
|
13
|
+
/* Inibisce i gesti di sistema per favorire Long Press e Swipe della dashboard */
|
|
14
14
|
-webkit-touch-callout: none;
|
|
15
15
|
-webkit-user-select: none;
|
|
16
16
|
user-select: none;
|
|
17
17
|
touch-action: none;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/* ==========================================================================
|
|
21
|
+
2. LAYOUT PRINCIPALE (LIQUID GRID)
|
|
22
|
+
========================================================================== */
|
|
20
23
|
.main-container {
|
|
21
24
|
display: grid;
|
|
22
25
|
width: 100%;
|
|
23
26
|
height: 100%;
|
|
24
27
|
padding: 5px;
|
|
25
28
|
box-sizing: border-box;
|
|
26
|
-
gap: 8px;
|
|
27
|
-
|
|
28
|
-
/* LAYOUT LIQUIDO:
|
|
29
|
-
I lati hanno un minimo vitale (180px) per proteggere i testi.
|
|
30
|
-
Il centro (auto) si adatta millimetricamente al diametro dell'SVG. */
|
|
29
|
+
gap: 8px;
|
|
30
|
+
/* Rapporto Standard: Lati flessibili, Centro bilanciato a 1.5fr */
|
|
31
31
|
grid-template-columns: minmax(180px, 1fr) minmax(auto, 1.5fr) minmax(180px, 1fr);
|
|
32
32
|
grid-template-rows: 100%;
|
|
33
33
|
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
34
34
|
justify-content: stretch;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/* ==========================================================================
|
|
38
|
-
2. PANNELLI E DATA-BOX (DISTRIBUZIONE FR)
|
|
39
|
-
========================================================================== */
|
|
40
37
|
.side-panel {
|
|
41
38
|
display: flex;
|
|
42
39
|
flex-direction: column;
|
|
@@ -44,14 +41,14 @@ body {
|
|
|
44
41
|
border-radius: 12px;
|
|
45
42
|
height: 100%;
|
|
46
43
|
overflow: hidden;
|
|
47
|
-
gap: 2px;
|
|
44
|
+
gap: 2px; /* Massimizza lo spazio per i 5 box verticali */
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
.left-panel { grid-column: 1; }
|
|
51
48
|
|
|
52
49
|
.center-panel {
|
|
53
50
|
grid-column: 2;
|
|
54
|
-
position: relative; /* Indispensabile per posizionare
|
|
51
|
+
position: relative; /* Indispensabile per posizionare l'etichetta STATUS in alto a dx */
|
|
55
52
|
display: flex;
|
|
56
53
|
flex-direction: column;
|
|
57
54
|
align-items: center;
|
|
@@ -66,71 +63,33 @@ body {
|
|
|
66
63
|
text-align: right;
|
|
67
64
|
}
|
|
68
65
|
|
|
66
|
+
/* ==========================================================================
|
|
67
|
+
3. DATA-BOX E TIPOGRAFIA (UNITA' ELASTICHE CQH/CLAMP)
|
|
68
|
+
========================================================================== */
|
|
69
69
|
.data-box {
|
|
70
70
|
position: relative;
|
|
71
71
|
border-bottom: 1px solid #222;
|
|
72
|
-
padding: 2px 4px;
|
|
72
|
+
padding: 2px 4px;
|
|
73
73
|
display: flex;
|
|
74
74
|
flex-direction: column;
|
|
75
75
|
width: 100%;
|
|
76
76
|
box-sizing: border-box;
|
|
77
|
-
container-type: size; /*
|
|
78
|
-
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 */
|
|
79
79
|
min-height: 0;
|
|
80
80
|
overflow: hidden;
|
|
81
|
+
transition: background-color 0.2s ease;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
/* Allineamento
|
|
84
|
+
/* Allineamento speculare dei testi tra colonna SX e DX */
|
|
84
85
|
.left-panel .data-box { align-items: flex-start; text-align: left; }
|
|
85
86
|
.right-panel .data-box { align-items: flex-end; text-align: right; }
|
|
86
87
|
|
|
87
|
-
/* ==========================================================================
|
|
88
|
-
3. TACTICAL FOCUS MODE (AUTO-EXPANDING SPLIT / DUAL SCREEN)
|
|
89
|
-
========================================================================== */
|
|
90
|
-
|
|
91
|
-
/* Nasconde le colonne non focalizzate */
|
|
92
|
-
.focus-active .side-panel:not(.has-focus) { display: none !important; }
|
|
93
|
-
|
|
94
|
-
/* In Focus Mode Left: [Pannello Sinistro Largo | Centro Stretto] */
|
|
95
|
-
.focus-active.focus-side-left {
|
|
96
|
-
grid-template-columns: 2fr auto !important;
|
|
97
|
-
}
|
|
98
|
-
.focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
|
|
99
|
-
.focus-active.focus-side-left .center-panel { grid-column: 2 !important; justify-content: flex-start; }
|
|
100
|
-
|
|
101
|
-
/* In Focus Mode Right: [Centro Stretto | Pannello Destro Largo] */
|
|
102
|
-
.focus-active.focus-side-right {
|
|
103
|
-
grid-template-columns: auto 2fr !important;
|
|
104
|
-
}
|
|
105
|
-
.focus-active.focus-side-right .center-panel { grid-column: 1 !important; justify-content: flex-start; }
|
|
106
|
-
.focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
|
|
107
|
-
|
|
108
|
-
/* Styling del box in Focus Mode */
|
|
109
|
-
.focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
|
|
110
|
-
.focus-active .has-focus .data-box.is-focused {
|
|
111
|
-
height: 100vh !important;
|
|
112
|
-
border: none;
|
|
113
|
-
background: rgba(255, 255, 255, 0.05);
|
|
114
|
-
padding: 20px;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/* Tipografia massiccia in Focus Mode */
|
|
118
|
-
.focus-active .is-focused .value { font-size: clamp(4rem, 25cqh, 4rem) !important; margin-top: 15px; }
|
|
119
|
-
.focus-active .is-focused .scale-labels { font-size: 32px !important; min-width: 40px !important; line-height: 1.2; }
|
|
120
|
-
.focus-active .is-focused .label-row .label { font-size: 2rem !important; }
|
|
121
|
-
.focus-active .is-focused .label-row .unit { font-size: 2rem !important; }
|
|
122
|
-
|
|
123
|
-
.focus-active .is-focused .sparkline path { stroke-width: 1px !important; }
|
|
124
|
-
|
|
125
|
-
/* ==========================================================================
|
|
126
|
-
4. TIPOGRAFIA DINAMICA (ELASTICA CQH)
|
|
127
|
-
========================================================================== */
|
|
128
88
|
.label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
|
|
129
|
-
|
|
130
89
|
.label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
|
|
131
90
|
.unit { color: #888; font-size: 0.6rem; font-weight: bold; }
|
|
132
91
|
|
|
133
|
-
/*
|
|
92
|
+
/* Font dinamico per i valori: massimo 22% dell'altezza del box */
|
|
134
93
|
.value {
|
|
135
94
|
color: #fff;
|
|
136
95
|
font-size: clamp(1.2rem, 22cqh, 3rem);
|
|
@@ -140,34 +99,65 @@ body {
|
|
|
140
99
|
padding-bottom: 5px;
|
|
141
100
|
}
|
|
142
101
|
|
|
143
|
-
|
|
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
|
+
|
|
144
108
|
.value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
|
|
145
109
|
|
|
146
|
-
/*
|
|
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 */
|
|
147
121
|
.dual-value-container { display: flex; justify-content: space-between; width: 100%; }
|
|
148
122
|
.dual-value-col { display: flex; flex-direction: column; width: 48%; }
|
|
149
123
|
.dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
|
|
150
124
|
.value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
|
|
151
125
|
|
|
152
126
|
/* ==========================================================================
|
|
153
|
-
|
|
127
|
+
4. TACTICAL FOCUS MODE (DUAL SCREEN 60/40)
|
|
154
128
|
========================================================================== */
|
|
155
|
-
.
|
|
129
|
+
.focus-active .side-panel:not(.has-focus) { display: none !important; }
|
|
156
130
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
167
149
|
}
|
|
168
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
|
+
|
|
169
159
|
/* ==========================================================================
|
|
170
|
-
|
|
160
|
+
5. GRAFICI, SCALE E HERCULES MODE
|
|
171
161
|
========================================================================== */
|
|
172
162
|
.graph-wrapper {
|
|
173
163
|
position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px;
|
|
@@ -175,7 +165,6 @@ body {
|
|
|
175
165
|
border-radius: 4px; gap: 0px !important;
|
|
176
166
|
}
|
|
177
167
|
|
|
178
|
-
/* Allargamento verso il centro */
|
|
179
168
|
.left-panel .graph-wrapper { margin-right: -6px; }
|
|
180
169
|
.right-panel .graph-wrapper { margin-left: -6px; }
|
|
181
170
|
|
|
@@ -188,25 +177,15 @@ body {
|
|
|
188
177
|
height: 100%; line-height: 1; padding: 0; border: none !important;
|
|
189
178
|
}
|
|
190
179
|
|
|
191
|
-
/* Simmetria scale */
|
|
192
180
|
.left-panel .scale-labels { order: 2; text-align: left; padding-left: 4px; }
|
|
193
181
|
.left-panel .sparkline { order: 1; }
|
|
194
182
|
.right-panel .scale-labels { order: 1; text-align: right; padding-right: 4px; }
|
|
195
183
|
.right-panel .sparkline { order: 2; }
|
|
196
184
|
|
|
197
|
-
/*
|
|
198
|
-
#stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
|
|
199
|
-
#sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
|
|
200
|
-
#depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
|
|
201
|
-
#tws-graph { stroke: #ffffff; fill: rgba(255, 255, 255, 0.08); }
|
|
202
|
-
|
|
203
|
-
/* ==========================================================================
|
|
204
|
-
7. HERCULES MODE (ZOOM E VISUALS)
|
|
205
|
-
========================================================================== */
|
|
185
|
+
/* Hercules Mode Visuals */
|
|
206
186
|
.line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
|
|
207
187
|
.box-hercules { background: rgba(255, 0, 0, 0.08) !important; }
|
|
208
188
|
.box-hercules .scale-labels { color: #ff8888; }
|
|
209
|
-
|
|
210
189
|
.box-hercules .unit::before, .box-hercules .unit::after, .box-hercules .label::before {
|
|
211
190
|
font-size: 7px; color: #ff4444; font-weight: 900; letter-spacing: 1px; text-transform: uppercase;
|
|
212
191
|
}
|
|
@@ -214,42 +193,51 @@ body {
|
|
|
214
193
|
.right-panel .box-hercules .unit::after { content: " HERCULES"; }
|
|
215
194
|
.right-panel .box-hercules .label:only-child::after { content: " HERCULES"; }
|
|
216
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
|
+
|
|
217
202
|
/* ==========================================================================
|
|
218
|
-
|
|
203
|
+
6. WIDGETS E STATUS
|
|
219
204
|
========================================================================== */
|
|
220
205
|
#status {
|
|
221
|
-
position: absolute;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
text-transform: uppercase;
|
|
227
|
-
z-index: 1000;
|
|
228
|
-
letter-spacing: 1px;
|
|
229
|
-
background: rgba(0,0,0,0.4);
|
|
230
|
-
padding: 2px 6px;
|
|
231
|
-
border-radius: 4px;
|
|
206
|
+
position: absolute; top: 10px; right: 10px;
|
|
207
|
+
font-size: 0.6rem; font-weight: 900;
|
|
208
|
+
text-transform: uppercase; z-index: 1000;
|
|
209
|
+
letter-spacing: 1px; background: rgba(0,0,0,0.4);
|
|
210
|
+
padding: 2px 6px; border-radius: 4px;
|
|
232
211
|
}
|
|
233
212
|
.online { color: #2ecc71; opacity: 0.5; }
|
|
234
213
|
.offline { color: #e74c3c; font-weight: bold; }
|
|
235
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
|
+
|
|
236
221
|
#awa-pointer, #twa-pointer, #track-pointer, #twd-arrow, #twd-boat-wrap {
|
|
237
222
|
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
|
238
223
|
}
|
|
239
224
|
|
|
225
|
+
#wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
|
|
226
|
+
|
|
227
|
+
/* ==========================================================================
|
|
228
|
+
7. STATI DI ALLARME E INSTABILITÀ
|
|
229
|
+
========================================================================== */
|
|
240
230
|
.unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
|
|
241
231
|
.alarm-warning { color: #f1c40f !important; }
|
|
242
232
|
.alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
|
|
243
233
|
@keyframes blink-unstable { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
244
234
|
|
|
245
|
-
#wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
|
|
246
|
-
|
|
247
235
|
/* ==========================================================================
|
|
248
|
-
|
|
236
|
+
8. RESPONSIVE (PORTRAIT MODE)
|
|
249
237
|
========================================================================== */
|
|
250
238
|
@media (max-aspect-ratio: 0.9 / 1) {
|
|
251
239
|
.main-container { grid-template-columns: 1fr 1fr !important; grid-template-rows: 45vh calc(55vh - 8px) !important; }
|
|
252
|
-
.center-panel { grid-row: 1 !important; grid-column: 1 / span 2 !important;
|
|
240
|
+
.center-panel { grid-row: 1 !important; grid-column: 1 / span 2 !important; }
|
|
253
241
|
.left-panel { grid-row: 2 !important; grid-column: 1 !important; }
|
|
254
242
|
.right-panel { grid-row: 2 !important; grid-column: 2 !important; }
|
|
255
243
|
.main-container.focus-active { display: flex !important; flex-direction: column !important; }
|
|
@@ -259,7 +247,7 @@ body {
|
|
|
259
247
|
}
|
|
260
248
|
|
|
261
249
|
/* ==========================================================================
|
|
262
|
-
|
|
250
|
+
9. NIGHT MODE (TACTICAL RED)
|
|
263
251
|
========================================================================== */
|
|
264
252
|
body.night-mode { background-color: #000 !important; color: #ff0000 !important; }
|
|
265
253
|
.night-mode .side-panel { background: rgba(20, 0, 0, 0.4); border: 1px solid #330000; }
|
|
@@ -267,6 +255,7 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
|
|
|
267
255
|
.night-mode .label, .night-mode .unit, .night-mode .dual-label { color: #800000 !important; }
|
|
268
256
|
.night-mode .value, .night-mode .value-large { color: #ff3333 !important; text-shadow: 0 0 8px rgba(255, 0, 0, 0.4); }
|
|
269
257
|
|
|
258
|
+
/* Grafici Night Mode: Solo linea, no riempimento */
|
|
270
259
|
.night-mode .graph-wrapper { background: rgba(30, 0, 0, 0.3) !important; }
|
|
271
260
|
.night-mode .sparkline path:first-of-type { display: none !important; }
|
|
272
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)); }
|
|
@@ -274,19 +263,34 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
|
|
|
274
263
|
.night-mode #tws-graph line:not([stroke*="rgba"]) { stroke: #ff3333 !important; stroke-width: 1.8px !important; }
|
|
275
264
|
.night-mode .scale-labels { color: #660000 !important; }
|
|
276
265
|
|
|
266
|
+
/* Hercules & special in Night */
|
|
277
267
|
.night-mode .box-hercules { background: rgba(60, 0, 0, 0.2) !important; }
|
|
278
268
|
.night-mode .line-hercules { filter: drop-shadow(0 0 6px #ff0000) !important; }
|
|
279
269
|
|
|
270
|
+
/* Wind Gauge Night */
|
|
280
271
|
.night-mode #wind-gauge circle { stroke: #330000; }
|
|
281
272
|
.night-mode #ticks line { stroke: #4d0000 !important; }
|
|
282
273
|
.night-mode #tick-labels { fill: #800000 !important; }
|
|
283
274
|
.night-mode #boat-icon { fill: #330000 !important; opacity: 0.6; }
|
|
284
275
|
.night-mode #aws-val-svg { fill: #ff3333 !important; }
|
|
276
|
+
.night-mode #aws-display-group text { fill: #ff3333 !important; }
|
|
277
|
+
.night-mode #center-glow feDropShadow { flood-color: #ff0000 !important; flood-opacity: 0.6 !important; }
|
|
278
|
+
|
|
279
|
+
/* Settori Vento Night (Destra tratteggiata) */
|
|
280
|
+
.night-mode #wind-gauge path[stroke="#ff0000"] { stroke: #660000 !important; opacity: 0.8; }
|
|
281
|
+
.night-mode #wind-gauge path[stroke="#00ff00"] { stroke: #660000 !important; stroke-dasharray: 4, 3; opacity: 0.8; }
|
|
282
|
+
.night-mode #wind-gauge path[stroke="#ff8800"] { stroke: #330000 !important; stroke-width: 8; }
|
|
283
|
+
|
|
284
|
+
/* Leeway & Pointer Night */
|
|
285
|
+
.night-mode rect[fill="url(#leeway-grad)"] { fill: url(#leeway-night-grad) !important; }
|
|
286
|
+
.night-mode #leeway-val { fill: #ff3333 !important; }
|
|
287
|
+
.night-mode g[stroke="#555"] line { stroke: #4d0000 !important; }
|
|
288
|
+
.night-mode g[fill="#555"] text { fill: #660000 !important; }
|
|
289
|
+
.night-mode rect[fill="#222"] { fill: #0a0000 !important; stroke: #200000; }
|
|
290
|
+
|
|
285
291
|
.night-mode #awa-pointer path { fill: #ff0000; stroke: #000; }
|
|
286
292
|
.night-mode #twa-pointer path { fill: #800000; stroke: #000; }
|
|
287
293
|
.night-mode #track-pointer path { fill: #ff0000; stroke: #fff; stroke-width: 0.5; }
|
|
288
|
-
.night-mode #aws-display-group text { fill: #ff3333 !important; }
|
|
289
|
-
.night-mode #center-glow feDropShadow { flood-color: #ff0000 !important; flood-opacity: 0.6 !important; }
|
|
290
294
|
|
|
291
295
|
/* Bussola TWD Night */
|
|
292
296
|
.night-mode .mini-compass { border-color: #330000; background: #000; }
|
|
@@ -299,14 +303,19 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
|
|
|
299
303
|
========================================================================== */
|
|
300
304
|
@keyframes blink-trend { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
301
305
|
|
|
306
|
+
/* Stato base dei pallini di trend: discreti al 30% */
|
|
302
307
|
#trend-dot-cw, #trend-dot-ccw, #trend-gauge-cw, #trend-gauge-ccw {
|
|
303
308
|
opacity: 0.3;
|
|
304
309
|
transition: opacity 0.3s ease;
|
|
305
310
|
}
|
|
306
311
|
|
|
307
|
-
|
|
312
|
+
/* Quando attivi: brillano al 100% e lampeggiano */
|
|
313
|
+
.is-trending {
|
|
314
|
+
opacity: 1 !important;
|
|
315
|
+
animation: blink-trend 1s infinite !important;
|
|
316
|
+
}
|
|
308
317
|
|
|
309
|
-
/* Allarme Strambata: Rosso fisso con bagliore neon */
|
|
318
|
+
/* Allarme Strambata: Rosso fisso con bagliore neon massimo */
|
|
310
319
|
.is-gybing {
|
|
311
320
|
opacity: 1 !important;
|
|
312
321
|
animation: none !important;
|