@sailingrotevista/rotevista-dash 2.0.17 → 2.0.19
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 +58 -141
- package/index.html +12 -0
- package/package.json +1 -1
- package/style.css +68 -65
package/app.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// ==========================================================================
|
|
2
|
-
// 1. CONFIGURAZIONE E DEFAULT
|
|
2
|
+
// 1. CONFIGURAZIONE E DEFAULT (Sincronizzato con il Plugin SignalK)
|
|
3
3
|
// ==========================================================================
|
|
4
4
|
let CONFIG = {
|
|
5
5
|
alarms: { depthDanger: 2.5, depthWarning: 5.0 },
|
|
6
6
|
averages: { smoothWindow: 2000, longWindow: 60000, stabilityTolerance: 2000, stabilityThreshold: 0.90, minSpeed: 0.5 },
|
|
7
7
|
graphs: { reef1: 15.0, reef2: 20.0, historyMinutes: 5, samples: 60 },
|
|
8
|
-
scales: {
|
|
8
|
+
scales: {
|
|
9
|
+
stw: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
10
|
+
sog: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
11
|
+
tws: { stdMax: 25, hercSpan: 10, step: 5 },
|
|
12
|
+
depth: { stdMax: 20, hercSpan: 10, step: 10 }
|
|
13
|
+
},
|
|
9
14
|
server: { fallbackIp: "192.168.111.240:3000" }
|
|
10
15
|
};
|
|
11
16
|
|
|
@@ -26,9 +31,7 @@ let curBoatCompassRot = 0, curWindCompassRot = 0;
|
|
|
26
31
|
let smoothedLeeway = 0, rotationTrend = 0;
|
|
27
32
|
let lastShortAvgVal = null, lastInstantTwa = null;
|
|
28
33
|
let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
|
|
29
|
-
let twDirty = false;
|
|
30
|
-
let reconnectDelay = 1000;
|
|
31
|
-
let isNavigating = false;
|
|
34
|
+
let twDirty = false, isNavigating = false, reconnectDelay = 1000;
|
|
32
35
|
|
|
33
36
|
let pressTimer, isFocusActive = false;
|
|
34
37
|
|
|
@@ -72,13 +75,16 @@ function ktsToMs(kts) { return kts / 1.94384; }
|
|
|
72
75
|
function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
|
|
73
76
|
|
|
74
77
|
/**
|
|
75
|
-
* Gestione sicura dei buffer per prevenire memory leak.
|
|
78
|
+
* Gestione sicura dei buffer per prevenire memory leak (O(1) complexity).
|
|
76
79
|
*/
|
|
77
80
|
function safePush(buffer, val, time, maxLen = 200) {
|
|
78
81
|
buffer.push({ val: val, time: time });
|
|
79
82
|
if (buffer.length > maxLen) { buffer.shift(); }
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Calcola medie circolari restituendo Radianti.
|
|
87
|
+
*/
|
|
82
88
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
83
89
|
const now = Date.now();
|
|
84
90
|
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
@@ -91,8 +97,13 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
|
91
97
|
return { val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI), stable: isStable };
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
// ==========================================================================
|
|
101
|
+
// 4. LOGICA FISICA E CALCOLI
|
|
102
|
+
// ==========================================================================
|
|
103
|
+
|
|
94
104
|
/**
|
|
95
|
-
* Calcola il Vento Reale
|
|
105
|
+
* Calcola il Vento Reale partendo dagli Apparenti.
|
|
106
|
+
* Logica Acqua (Vele) vs Terra (Meteo).
|
|
96
107
|
*/
|
|
97
108
|
function computeTrueWind() {
|
|
98
109
|
const aws = store.raw["environment.wind.speedApparent"];
|
|
@@ -103,11 +114,11 @@ function computeTrueWind() {
|
|
|
103
114
|
if (aws === undefined || awa === undefined) return;
|
|
104
115
|
if (awa > Math.PI) awa -= 2 * Math.PI;
|
|
105
116
|
|
|
106
|
-
//
|
|
117
|
+
// Vento Reale su ACQUA (TWA/TWS)
|
|
107
118
|
const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
|
|
108
119
|
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y), twa_water = Math.atan2(tw_water_y, tw_water_x);
|
|
109
120
|
|
|
110
|
-
//
|
|
121
|
+
// Vento Reale su TERRA (TWD)
|
|
111
122
|
const drift_angle = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
|
|
112
123
|
const sog_vec_x = sog * Math.cos(drift_angle), sog_vec_y = sog * Math.sin(drift_angle);
|
|
113
124
|
const tw_ground_x = aws * Math.cos(awa) - sog_vec_x, tw_ground_y = aws * Math.sin(awa) - sog_vec_y;
|
|
@@ -125,6 +136,7 @@ function processIncomingData(path, val) {
|
|
|
125
136
|
if (path === "navigation.position") store.raw["navigation.position"] = val;
|
|
126
137
|
if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
|
|
127
138
|
|
|
139
|
+
// Dirty flag + Debounce 100ms
|
|
128
140
|
const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
|
|
129
141
|
if (twPaths.includes(path)) twDirty = true;
|
|
130
142
|
if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
|
|
@@ -134,25 +146,15 @@ function processIncomingData(path, val) {
|
|
|
134
146
|
}
|
|
135
147
|
|
|
136
148
|
// ==========================================================================
|
|
137
|
-
//
|
|
149
|
+
// 5. AUDIO E ALLARMI
|
|
138
150
|
// ==========================================================================
|
|
139
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); }
|
|
140
|
-
|
|
141
|
-
function playGybeAlarm() {
|
|
142
|
-
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
143
|
-
const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
|
|
144
|
-
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); }
|
|
145
|
-
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); }
|
|
146
|
-
}
|
|
147
|
-
|
|
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); } }
|
|
148
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'); }
|
|
149
154
|
|
|
150
155
|
// ==========================================================================
|
|
151
|
-
//
|
|
156
|
+
// 6. TREND VENTO E TATTICA
|
|
152
157
|
// ==========================================================================
|
|
153
|
-
/**
|
|
154
|
-
* Monitoraggio Trend Vento Professionale (Tattica + Meteo + Gybe Safety)
|
|
155
|
-
*/
|
|
156
158
|
function updateWindTrend() {
|
|
157
159
|
const now = Date.now();
|
|
158
160
|
const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true);
|
|
@@ -160,14 +162,12 @@ function updateWindTrend() {
|
|
|
160
162
|
const instantTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
161
163
|
|
|
162
164
|
if (!shortAvg || !twaAvgObj || instantTwaRad === undefined) return;
|
|
163
|
-
const instantTwaDeg = radToDeg(instantTwaRad);
|
|
164
|
-
const shortAvgDeg = radToDeg(shortAvg.val);
|
|
165
|
-
const twaAvgDeg = radToDeg(twaAvgObj.val);
|
|
165
|
+
const instantTwaDeg = radToDeg(instantTwaRad), shortAvgDeg = radToDeg(shortAvg.val), twaAvgDeg = radToDeg(twaAvgObj.val);
|
|
166
166
|
|
|
167
167
|
if (lastShortAvgVal === null) { lastShortAvgVal = shortAvgDeg; lastInstantTwa = instantTwaDeg; return; }
|
|
168
168
|
const dt = (now - lastTrendTime) / 1000; lastTrendTime = now;
|
|
169
169
|
|
|
170
|
-
//
|
|
170
|
+
// Gybe Safety
|
|
171
171
|
const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
|
|
172
172
|
lastInstantTwa = instantTwaDeg;
|
|
173
173
|
if (gybeDetected && isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
|
|
@@ -180,12 +180,12 @@ function updateWindTrend() {
|
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
//
|
|
183
|
+
// Trend calculation con Clamping e Alpha adattivo
|
|
184
184
|
let diff = (shortAvgDeg - lastShortAvgVal + 540) % 360 - 180; lastShortAvgVal = shortAvgDeg;
|
|
185
185
|
if (dt > 0) {
|
|
186
|
-
let
|
|
186
|
+
let rate = Math.max(-10, Math.min(10, diff / dt));
|
|
187
187
|
const alpha = Math.min(1, dt / 5);
|
|
188
|
-
rotationTrend = rotationTrend * (1 - alpha) +
|
|
188
|
+
rotationTrend = rotationTrend * (1 - alpha) + rate * alpha;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
if (Math.abs(rotationTrend) > 1.0) {
|
|
@@ -211,31 +211,34 @@ function updateWindTrend() {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
// ==========================================================================
|
|
214
|
-
//
|
|
214
|
+
// 7. RENDERING ENGINE (TIERED RENDERING)
|
|
215
215
|
// ==========================================================================
|
|
216
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
217
|
|
|
218
|
-
function
|
|
219
|
-
|
|
218
|
+
function refreshGraph(t) {
|
|
219
|
+
const type = (t === 'vmg') ? 'sog' : t;
|
|
220
|
+
const data = store.histories[t]; if (!data || data.length < 2) return;
|
|
221
|
+
const mode = graphModes[type], cfg = calculateScale(type, data, mode);
|
|
222
|
+
const graphEl = document.getElementById(type + '-graph');
|
|
223
|
+
if (graphEl) { const box = graphEl.closest('.data-box'); if (box) box.classList.toggle('box-hercules', mode === 'hercules'); }
|
|
224
|
+
updateScaleLabels(type, cfg.min, cfg.max); drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
225
|
+
}
|
|
220
226
|
|
|
227
|
+
function startDisplayLoop() {
|
|
228
|
+
let tick = 0;
|
|
221
229
|
renderInterval = setInterval(() => {
|
|
222
|
-
const now = Date.now();
|
|
223
|
-
tick++;
|
|
230
|
+
const now = Date.now(); tick++;
|
|
224
231
|
|
|
225
|
-
//
|
|
226
|
-
// LIVE TIER (Ogni 1s) - Reattività, Numeri, Allarmi, Lancette
|
|
227
|
-
// ==========================================================
|
|
232
|
+
// --- LIVE TIER (1s) ---
|
|
228
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 };
|
|
229
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]; } }
|
|
230
235
|
|
|
231
236
|
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0), sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
232
237
|
isNavigating = stwKts > CONFIG.averages.minSpeed || sogKts > CONFIG.averages.minSpeed;
|
|
233
238
|
|
|
234
|
-
// Numeri veloci
|
|
235
239
|
if (store.raw["navigation.speedThroughWater"] !== undefined) { ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts); }
|
|
236
240
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
237
|
-
const twaRad = store.raw["environment.wind.angleTrueWater"];
|
|
238
|
-
const vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
|
|
241
|
+
const twaRad = store.raw["environment.wind.angleTrueWater"], vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
|
|
239
242
|
manageHistory('vmg', vmgKts); manageHistory('sog', sogKts);
|
|
240
243
|
if (displayModeSog === 'VMG') { ui.sog.innerText = vmgKts.toFixed(1); ui.sog.style.color = "#64ffda"; document.getElementById('sog-vmg-label').textContent = 'VMG'; }
|
|
241
244
|
else { ui.sog.innerText = sogKts.toFixed(1); ui.sog.style.color = (sogKts-stwKts > 0.3) ? "#2ecc71" : (sogKts-stwKts < -0.3 ? "#e74c3c" : "#fff"); document.getElementById('sog-vmg-label').textContent = 'SOG'; }
|
|
@@ -244,38 +247,27 @@ function startDisplayLoop() {
|
|
|
244
247
|
if (store.raw["environment.wind.speedTrue"] !== undefined) { const twsKts = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = twsKts.toFixed(1); ui.tws.style.color = (twsKts >= CONFIG.graphs.reef2) ? "#e74c3c" : (twsKts >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff"); manageHistory('tws', twsKts); }
|
|
245
248
|
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
246
249
|
|
|
247
|
-
// Lancette (Sempre fluide ogni 1s)
|
|
248
250
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
|
|
249
251
|
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
250
252
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
251
253
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
252
254
|
|
|
253
|
-
// Leeway (Ogni 1s)
|
|
254
255
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
255
|
-
let
|
|
256
|
-
let driftDeg = radToDeg(driftRad);
|
|
256
|
+
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
257
257
|
if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
258
258
|
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
259
|
-
const isContaminated = Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7;
|
|
260
|
-
ui.leewayVal.style.color = isContaminated ? "#f39c12" : "#fff";
|
|
259
|
+
const isContaminated = Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7; ui.leewayVal.style.color = isContaminated ? "#f39c12" : "#fff";
|
|
261
260
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
262
261
|
}
|
|
263
262
|
|
|
264
|
-
updateWindTrend(); //
|
|
263
|
+
updateWindTrend(); // Fast update
|
|
265
264
|
|
|
266
|
-
//
|
|
267
|
-
// HEAVY TIER (Ogni 2s) - Disegno Sparklines SVG
|
|
268
|
-
// ==========================================================
|
|
265
|
+
// --- HEAVY TIER (2s) ---
|
|
269
266
|
if (tick % 2 === 0) {
|
|
270
|
-
refreshGraph('stw');
|
|
271
|
-
refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog');
|
|
272
|
-
refreshGraph('depth');
|
|
273
|
-
refreshGraph('tws');
|
|
267
|
+
refreshGraph('stw'); refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog'); refreshGraph('depth'); refreshGraph('tws');
|
|
274
268
|
}
|
|
275
269
|
|
|
276
|
-
//
|
|
277
|
-
// SLOW TIER (Ogni 3s) - Medie MEAN e Calcoli TACK
|
|
278
|
-
// ==========================================================
|
|
270
|
+
// --- SLOW TIER (3s) ---
|
|
279
271
|
if (tick % 3 === 0) {
|
|
280
272
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
|
|
281
273
|
cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
|
|
@@ -291,7 +283,6 @@ function startDisplayLoop() {
|
|
|
291
283
|
}
|
|
292
284
|
};
|
|
293
285
|
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);
|
|
294
|
-
|
|
295
286
|
if (hObj && twObj && hObj.val !== null) {
|
|
296
287
|
const tackHdgDeg = radToDeg((hObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
|
|
297
288
|
ui.tackHdg.innerHTML = `${Math.round((tackHdgDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
@@ -300,25 +291,18 @@ function startDisplayLoop() {
|
|
|
300
291
|
ui.tackCog.innerHTML = `${Math.round((tackCogDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
301
292
|
}
|
|
302
293
|
}
|
|
303
|
-
|
|
304
294
|
const smHdg = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false), smTwd = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
305
295
|
if (smHdg && smTwd) {
|
|
306
296
|
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwd.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
307
297
|
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdg.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
308
298
|
}
|
|
309
299
|
}
|
|
310
|
-
|
|
311
|
-
// Pulizia buffer
|
|
312
|
-
if (tick % 60 === 0) { // Ogni minuto pulisce tutto per sicurezza
|
|
313
|
-
for (let b in store.smoothBuf) { while (store.smoothBuf[b].length > 0 && (now - store.smoothBuf[b][0].time) > CONFIG.averages.smoothWindow) store.smoothBuf[b].shift(); }
|
|
314
|
-
for (let b in store.longBuf) { while (store.longBuf[b].length > 0 && (now - store.longBuf[b][0].time) > CONFIG.averages.longWindow) store.longBuf[b].shift(); }
|
|
315
|
-
tick = 0; // Reset contatore
|
|
316
|
-
}
|
|
300
|
+
if (tick % 60 === 0) tick = 0;
|
|
317
301
|
}, RENDER_INTERVAL_MS);
|
|
318
302
|
}
|
|
319
303
|
|
|
320
304
|
// ==========================================================================
|
|
321
|
-
//
|
|
305
|
+
// 8. INTERAZIONI E RETE
|
|
322
306
|
// ==========================================================================
|
|
323
307
|
async function fetchServerConfig() {
|
|
324
308
|
if (!window.location.protocol.includes("http")) return;
|
|
@@ -343,43 +327,9 @@ async function fetchServerConfig() {
|
|
|
343
327
|
}
|
|
344
328
|
}
|
|
345
329
|
|
|
346
|
-
/**
|
|
347
|
-
* Gestisce l'archiviazione dei dati storici.
|
|
348
|
-
* Il disegno grafico viene ora gestito separatamente nel loop pesante.
|
|
349
|
-
*/
|
|
350
330
|
function manageHistory(t, v) {
|
|
351
|
-
const n = Date.now();
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
|
|
355
|
-
store.histories[t].push(v);
|
|
356
|
-
if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
|
|
357
|
-
store.lastUpdates[t] = n;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Funzione dedicata al ridisegno dei grafici (chiamata dal loop HEAVY)
|
|
363
|
-
*/
|
|
364
|
-
function refreshGraph(t) {
|
|
365
|
-
const type = (t === 'vmg') ? 'sog' : t;
|
|
366
|
-
const data = store.histories[t];
|
|
367
|
-
if (!data || data.length < 2) return;
|
|
368
|
-
|
|
369
|
-
const mode = graphModes[type];
|
|
370
|
-
const cfg = calculateScale(type, data, mode);
|
|
371
|
-
|
|
372
|
-
// Aggiornamento stile box Hercules
|
|
373
|
-
const graphEl = document.getElementById(type + '-graph');
|
|
374
|
-
if (graphEl) {
|
|
375
|
-
const box = graphEl.closest('.data-box');
|
|
376
|
-
if (box) {
|
|
377
|
-
if (mode === 'hercules') box.classList.add('box-hercules');
|
|
378
|
-
else box.classList.remove('box-hercules');
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
updateScaleLabels(type, cfg.min, cfg.max);
|
|
382
|
-
drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
331
|
+
const n = Date.now(); const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
332
|
+
if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) { store.histories[t].push(v); if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift(); store.lastUpdates[t] = n; }
|
|
383
333
|
}
|
|
384
334
|
|
|
385
335
|
function calculateScale(type, data, mode) {
|
|
@@ -409,9 +359,6 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
409
359
|
svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(255,255,255,0.08)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${colorKey}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${colorKey}" />`;
|
|
410
360
|
}
|
|
411
361
|
|
|
412
|
-
// ==========================================================================
|
|
413
|
-
// 8. INTERAZIONI E SIMULATORE
|
|
414
|
-
// ==========================================================================
|
|
415
362
|
function toggleFocusMode(type, element) {
|
|
416
363
|
const container = document.querySelector('.main-container'); const parentPanel = element.closest('.side-panel'); const isLeft = parentPanel.classList.contains('left-panel');
|
|
417
364
|
isFocusActive = !isFocusActive;
|
|
@@ -440,44 +387,14 @@ if (ui.hotspot) {
|
|
|
440
387
|
|
|
441
388
|
function connect() {
|
|
442
389
|
if (simulationMode) return;
|
|
443
|
-
|
|
444
390
|
let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
|
|
445
391
|
const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws';
|
|
446
|
-
|
|
447
392
|
try {
|
|
448
393
|
socket = new WebSocket(`${protocol}://${addr}/signalk/v1/stream?subscribe=self`);
|
|
449
|
-
|
|
450
|
-
socket.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
reconnectDelay = 1000; // Reset del ritardo dopo una connessione riuscita
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
socket.onmessage = (e) => {
|
|
457
|
-
if (simulationMode) return;
|
|
458
|
-
const d = JSON.parse(e.data);
|
|
459
|
-
if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value)));
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
socket.onclose = () => {
|
|
463
|
-
if (!simulationMode) {
|
|
464
|
-
ui.status.className = "offline";
|
|
465
|
-
ui.status.innerText = "RECONNECTING...";
|
|
466
|
-
|
|
467
|
-
// Exponential Backoff: aumenta il tempo ad ogni tentativo fallito
|
|
468
|
-
setTimeout(connect, reconnectDelay);
|
|
469
|
-
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); // Max 10 secondi
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
socket.onerror = () => {
|
|
474
|
-
socket.close(); // Forza il trigger di onclose
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
} catch (e) {
|
|
478
|
-
setTimeout(connect, reconnectDelay);
|
|
479
|
-
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
|
|
480
|
-
}
|
|
394
|
+
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; reconnectDelay = 1000; };
|
|
395
|
+
socket.onmessage = (e) => { if (simulationMode) return; const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
|
|
396
|
+
socket.onclose = () => { if (!simulationMode) { ui.status.className = "offline"; ui.status.innerText = "RECONNECTING..."; setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); } };
|
|
397
|
+
} catch (e) { setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); }
|
|
481
398
|
}
|
|
482
399
|
|
|
483
400
|
ui.depth.closest('.data-box').addEventListener('click', (function() { let dC = 0, lC = 0; return function() { const n = Date.now(); if (n - lC < 500) dC++; else dC = 1; lC = n; if (dC === 3) { simulationMode = !simulationMode; if (simulationMode) { if (socket) socket.close(); startDynamicSimulation(); } else location.reload(); dC = 0; } }; })());
|
|
@@ -507,7 +424,7 @@ function startDynamicSimulation() {
|
|
|
507
424
|
}
|
|
508
425
|
|
|
509
426
|
// ==========================================================================
|
|
510
|
-
//
|
|
427
|
+
// 11. INIT
|
|
511
428
|
// ==========================================================================
|
|
512
429
|
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
513
430
|
(function genTicks() { const c = document.getElementById('ticks'); if (c) { for (let i = 0; i < 360; i += 10) { const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50)); l.setAttribute("stroke", m ? "#fff" : "#666"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
|
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)">
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -10,7 +10,6 @@ body {
|
|
|
10
10
|
height: 100vh;
|
|
11
11
|
width: 100vw;
|
|
12
12
|
overflow: hidden;
|
|
13
|
-
/* BLOCCO TOTALE GESTI DI SISTEMA: Fondamentale per il Long Press su mobile */
|
|
14
13
|
-webkit-touch-callout: none;
|
|
15
14
|
-webkit-user-select: none;
|
|
16
15
|
user-select: none;
|
|
@@ -23,11 +22,8 @@ body {
|
|
|
23
22
|
height: 100%;
|
|
24
23
|
padding: 5px;
|
|
25
24
|
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. */
|
|
25
|
+
gap: 8px;
|
|
26
|
+
/* Rapporto Standard: Lati 1fr, Centro 1.5fr (Bilanciato) */
|
|
31
27
|
grid-template-columns: minmax(180px, 1fr) minmax(auto, 1.5fr) minmax(180px, 1fr);
|
|
32
28
|
grid-template-rows: 100%;
|
|
33
29
|
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
@@ -35,7 +31,7 @@ body {
|
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
/* ==========================================================================
|
|
38
|
-
2. PANNELLI E DATA-BOX
|
|
34
|
+
2. PANNELLI E DATA-BOX
|
|
39
35
|
========================================================================== */
|
|
40
36
|
.side-panel {
|
|
41
37
|
display: flex;
|
|
@@ -48,10 +44,9 @@ body {
|
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
.left-panel { grid-column: 1; }
|
|
51
|
-
|
|
52
47
|
.center-panel {
|
|
53
48
|
grid-column: 2;
|
|
54
|
-
position: relative;
|
|
49
|
+
position: relative;
|
|
55
50
|
display: flex;
|
|
56
51
|
flex-direction: column;
|
|
57
52
|
align-items: center;
|
|
@@ -59,7 +54,6 @@ body {
|
|
|
59
54
|
height: 100%;
|
|
60
55
|
overflow: hidden;
|
|
61
56
|
}
|
|
62
|
-
|
|
63
57
|
.right-panel {
|
|
64
58
|
grid-column: 3;
|
|
65
59
|
align-items: flex-end;
|
|
@@ -69,68 +63,83 @@ body {
|
|
|
69
63
|
.data-box {
|
|
70
64
|
position: relative;
|
|
71
65
|
border-bottom: 1px solid #222;
|
|
72
|
-
padding: 2px 4px;
|
|
66
|
+
padding: 2px 4px;
|
|
73
67
|
display: flex;
|
|
74
68
|
flex-direction: column;
|
|
75
69
|
width: 100%;
|
|
76
70
|
box-sizing: border-box;
|
|
77
|
-
container-type: size;
|
|
78
|
-
flex: 1 1 0px;
|
|
71
|
+
container-type: size;
|
|
72
|
+
flex: 1 1 0px;
|
|
79
73
|
min-height: 0;
|
|
80
74
|
overflow: hidden;
|
|
75
|
+
transition: background-color 0.2s ease;
|
|
81
76
|
}
|
|
82
77
|
|
|
83
|
-
/* Allineamento Speculare: SX a sinistra, DX a destra del box */
|
|
84
78
|
.left-panel .data-box { align-items: flex-start; text-align: left; }
|
|
85
79
|
.right-panel .data-box { align-items: flex-end; text-align: right; }
|
|
86
80
|
|
|
87
81
|
/* ==========================================================================
|
|
88
|
-
3. TACTICAL FOCUS MODE (
|
|
82
|
+
3. TACTICAL FOCUS MODE (DUAL SCREEN 60/40)
|
|
89
83
|
========================================================================== */
|
|
90
84
|
|
|
91
|
-
/* Nasconde
|
|
85
|
+
/* Nasconde i pannelli non attivi */
|
|
92
86
|
.focus-active .side-panel:not(.has-focus) { display: none !important; }
|
|
93
87
|
|
|
94
|
-
/*
|
|
88
|
+
/* Split 60/40: Grafico a Sinistra */
|
|
95
89
|
.focus-active.focus-side-left {
|
|
96
|
-
grid-template-columns: 2fr
|
|
90
|
+
grid-template-columns: 3fr 2fr !important;
|
|
97
91
|
}
|
|
98
92
|
.focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
|
|
99
|
-
.focus-active.focus-side-left .center-panel {
|
|
93
|
+
.focus-active.focus-side-left .center-panel {
|
|
94
|
+
grid-column: 2 !important;
|
|
95
|
+
justify-content: center !important;
|
|
96
|
+
align-items: center !important;
|
|
97
|
+
}
|
|
100
98
|
|
|
101
|
-
/*
|
|
99
|
+
/* Split 60/40: Grafico a Destra */
|
|
102
100
|
.focus-active.focus-side-right {
|
|
103
|
-
grid-template-columns:
|
|
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;
|
|
104
107
|
}
|
|
105
|
-
.focus-active.focus-side-right .center-panel { grid-column: 1 !important; justify-content: flex-start; }
|
|
106
108
|
.focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
|
|
107
109
|
|
|
108
|
-
/*
|
|
110
|
+
/* Espansione Box Focalizzato */
|
|
109
111
|
.focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
|
|
110
112
|
.focus-active .has-focus .data-box.is-focused {
|
|
111
113
|
height: 100vh !important;
|
|
112
114
|
border: none;
|
|
113
115
|
background: rgba(255, 255, 255, 0.05);
|
|
114
|
-
padding:
|
|
116
|
+
padding: 2vw 3vw !important;
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
/* Tipografia
|
|
118
|
-
.focus-active .is-focused .value { font-size: clamp(4rem, 25cqh,
|
|
119
|
-
.focus-active .is-focused .scale-labels { font-size:
|
|
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; }
|
|
120
124
|
.focus-active .is-focused .label-row .label { font-size: 2rem !important; }
|
|
121
125
|
.focus-active .is-focused .label-row .unit { font-size: 2rem !important; }
|
|
122
126
|
|
|
123
|
-
.focus-active .is-focused .sparkline path { stroke-width:
|
|
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
|
+
}
|
|
124
134
|
|
|
125
135
|
/* ==========================================================================
|
|
126
|
-
4. TIPOGRAFIA DINAMICA
|
|
136
|
+
4. TIPOGRAFIA DINAMICA E ELASTICA
|
|
127
137
|
========================================================================== */
|
|
128
138
|
.label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
|
|
129
139
|
|
|
130
140
|
.label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
|
|
131
141
|
.unit { color: #888; font-size: 0.6rem; font-weight: bold; }
|
|
132
142
|
|
|
133
|
-
/* Numero standard (22% altezza box) */
|
|
134
143
|
.value {
|
|
135
144
|
color: #fff;
|
|
136
145
|
font-size: clamp(1.2rem, 22cqh, 3rem);
|
|
@@ -143,14 +152,13 @@ body {
|
|
|
143
152
|
.value-large, .dual-value-container, .value-with-compass { margin-top: auto; margin-bottom: auto; }
|
|
144
153
|
.value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
|
|
145
154
|
|
|
146
|
-
/* TACK Layout (Mure opposte) */
|
|
147
155
|
.dual-value-container { display: flex; justify-content: space-between; width: 100%; }
|
|
148
156
|
.dual-value-col { display: flex; flex-direction: column; width: 48%; }
|
|
149
157
|
.dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
|
|
150
158
|
.value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
|
|
151
159
|
|
|
152
160
|
/* ==========================================================================
|
|
153
|
-
5. BUSSOLA TWD
|
|
161
|
+
5. WIDGETS (BUSSOLA TWD)
|
|
154
162
|
========================================================================== */
|
|
155
163
|
.value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 5px; }
|
|
156
164
|
|
|
@@ -167,7 +175,7 @@ body {
|
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
/* ==========================================================================
|
|
170
|
-
6. GRAFICI E SCALE
|
|
178
|
+
6. GRAFICI E SCALE
|
|
171
179
|
========================================================================== */
|
|
172
180
|
.graph-wrapper {
|
|
173
181
|
position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px;
|
|
@@ -175,7 +183,6 @@ body {
|
|
|
175
183
|
border-radius: 4px; gap: 0px !important;
|
|
176
184
|
}
|
|
177
185
|
|
|
178
|
-
/* Allargamento verso il centro */
|
|
179
186
|
.left-panel .graph-wrapper { margin-right: -6px; }
|
|
180
187
|
.right-panel .graph-wrapper { margin-left: -6px; }
|
|
181
188
|
|
|
@@ -188,20 +195,18 @@ body {
|
|
|
188
195
|
height: 100%; line-height: 1; padding: 0; border: none !important;
|
|
189
196
|
}
|
|
190
197
|
|
|
191
|
-
/* Simmetria scale */
|
|
192
198
|
.left-panel .scale-labels { order: 2; text-align: left; padding-left: 4px; }
|
|
193
199
|
.left-panel .sparkline { order: 1; }
|
|
194
200
|
.right-panel .scale-labels { order: 1; text-align: right; padding-right: 4px; }
|
|
195
201
|
.right-panel .sparkline { order: 2; }
|
|
196
202
|
|
|
197
|
-
/* Colori grafici */
|
|
198
203
|
#stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
|
|
199
204
|
#sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
|
|
200
205
|
#depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
|
|
201
206
|
#tws-graph { stroke: #ffffff; fill: rgba(255, 255, 255, 0.08); }
|
|
202
207
|
|
|
203
208
|
/* ==========================================================================
|
|
204
|
-
7. HERCULES MODE
|
|
209
|
+
7. HERCULES MODE
|
|
205
210
|
========================================================================== */
|
|
206
211
|
.line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
|
|
207
212
|
.box-hercules { background: rgba(255, 0, 0, 0.08) !important; }
|
|
@@ -215,20 +220,14 @@ body {
|
|
|
215
220
|
.right-panel .box-hercules .label:only-child::after { content: " HERCULES"; }
|
|
216
221
|
|
|
217
222
|
/* ==========================================================================
|
|
218
|
-
8.
|
|
223
|
+
8. STATUS E ANIMAZIONI
|
|
219
224
|
========================================================================== */
|
|
220
225
|
#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;
|
|
226
|
+
position: absolute; top: 10px; right: 10px;
|
|
227
|
+
font-size: 0.6rem; font-weight: 900;
|
|
228
|
+
text-transform: uppercase; z-index: 1000;
|
|
229
|
+
letter-spacing: 1px; background: rgba(0,0,0,0.4);
|
|
230
|
+
padding: 2px 6px; border-radius: 4px;
|
|
232
231
|
}
|
|
233
232
|
.online { color: #2ecc71; opacity: 0.5; }
|
|
234
233
|
.offline { color: #e74c3c; font-weight: bold; }
|
|
@@ -245,11 +244,11 @@ body {
|
|
|
245
244
|
#wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
|
|
246
245
|
|
|
247
246
|
/* ==========================================================================
|
|
248
|
-
9. RESPONSIVE (PORTRAIT
|
|
247
|
+
9. RESPONSIVE (PORTRAIT)
|
|
249
248
|
========================================================================== */
|
|
250
249
|
@media (max-aspect-ratio: 0.9 / 1) {
|
|
251
250
|
.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;
|
|
251
|
+
.center-panel { grid-row: 1 !important; grid-column: 1 / span 2 !important; }
|
|
253
252
|
.left-panel { grid-row: 2 !important; grid-column: 1 !important; }
|
|
254
253
|
.right-panel { grid-row: 2 !important; grid-column: 2 !important; }
|
|
255
254
|
.main-container.focus-active { display: flex !important; flex-direction: column !important; }
|
|
@@ -259,7 +258,7 @@ body {
|
|
|
259
258
|
}
|
|
260
259
|
|
|
261
260
|
/* ==========================================================================
|
|
262
|
-
10. NIGHT MODE (RED
|
|
261
|
+
10. NIGHT MODE (TACTICAL RED)
|
|
263
262
|
========================================================================== */
|
|
264
263
|
body.night-mode { background-color: #000 !important; color: #ff0000 !important; }
|
|
265
264
|
.night-mode .side-panel { background: rgba(20, 0, 0, 0.4); border: 1px solid #330000; }
|
|
@@ -274,19 +273,30 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
|
|
|
274
273
|
.night-mode #tws-graph line:not([stroke*="rgba"]) { stroke: #ff3333 !important; stroke-width: 1.8px !important; }
|
|
275
274
|
.night-mode .scale-labels { color: #660000 !important; }
|
|
276
275
|
|
|
277
|
-
.night-mode .box-hercules { background: rgba(60, 0, 0, 0.2) !important; }
|
|
278
|
-
.night-mode .line-hercules { filter: drop-shadow(0 0 6px #ff0000) !important; }
|
|
279
|
-
|
|
280
276
|
.night-mode #wind-gauge circle { stroke: #330000; }
|
|
281
277
|
.night-mode #ticks line { stroke: #4d0000 !important; }
|
|
282
278
|
.night-mode #tick-labels { fill: #800000 !important; }
|
|
283
279
|
.night-mode #boat-icon { fill: #330000 !important; opacity: 0.6; }
|
|
284
280
|
.night-mode #aws-val-svg { fill: #ff3333 !important; }
|
|
281
|
+
.night-mode #aws-display-group text { fill: #ff3333 !important; }
|
|
282
|
+
.night-mode #center-glow feDropShadow { flood-color: #ff0000 !important; flood-opacity: 0.6 !important; }
|
|
283
|
+
|
|
284
|
+
/* Settori Vento Night */
|
|
285
|
+
.night-mode #wind-gauge path[stroke="#ff0000"] { stroke: #660000 !important; opacity: 0.8; }
|
|
286
|
+
.night-mode #wind-gauge path[stroke="#00ff00"] { stroke: #660000 !important; stroke-dasharray: 4, 3; opacity: 0.8; }
|
|
287
|
+
.night-mode #wind-gauge path[stroke="#ff8800"] { stroke: #330000 !important; stroke-width: 8; }
|
|
288
|
+
|
|
289
|
+
/* Lancette Night */
|
|
285
290
|
.night-mode #awa-pointer path { fill: #ff0000; stroke: #000; }
|
|
286
291
|
.night-mode #twa-pointer path { fill: #800000; stroke: #000; }
|
|
287
292
|
.night-mode #track-pointer path { fill: #ff0000; stroke: #fff; stroke-width: 0.5; }
|
|
288
|
-
|
|
289
|
-
|
|
293
|
+
|
|
294
|
+
/* Leeway Night */
|
|
295
|
+
.night-mode rect[fill="url(#leeway-grad)"] { fill: url(#leeway-night-grad) !important; }
|
|
296
|
+
.night-mode #leeway-val { fill: #ff3333 !important; }
|
|
297
|
+
.night-mode g[stroke="#555"] line { stroke: #4d0000 !important; }
|
|
298
|
+
.night-mode g[fill="#555"] text { fill: #660000 !important; }
|
|
299
|
+
.night-mode rect[fill="#222"] { fill: #0a0000 !important; stroke: #200000; }
|
|
290
300
|
|
|
291
301
|
/* Bussola TWD Night */
|
|
292
302
|
.night-mode .mini-compass { border-color: #330000; background: #000; }
|
|
@@ -304,11 +314,4 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
|
|
|
304
314
|
transition: opacity 0.3s ease;
|
|
305
315
|
}
|
|
306
316
|
|
|
307
|
-
.is-trending {
|
|
308
|
-
|
|
309
|
-
/* Allarme Strambata: Rosso fisso con bagliore neon */
|
|
310
|
-
.is-gybing {
|
|
311
|
-
opacity: 1 !important;
|
|
312
|
-
animation: none !important;
|
|
313
|
-
filter: drop-shadow(0 0 8px #ff0000) !important;
|
|
314
|
-
}
|
|
317
|
+
.is-trending { a
|