@sailingrotevista/rotevista-dash 2.0.16 → 2.0.18
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 +183 -137
- package/index.html +35 -81
- package/package.json +1 -1
- package/style.css +76 -249
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
|
|
|
@@ -17,18 +22,18 @@ const SIM_SAMPLE_INTERVAL = 1000;
|
|
|
17
22
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
18
23
|
// ==========================================================================
|
|
19
24
|
let simulationMode = false;
|
|
25
|
+
let displayModeSog = 'SOG'; // 'SOG' o 'VMG'
|
|
20
26
|
let socket, renderInterval, simInterval;
|
|
21
27
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
22
28
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
29
|
+
let curBoatCompassRot = 0, curWindCompassRot = 0;
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
let
|
|
27
|
-
let
|
|
28
|
-
let lastInstantTwa = null;
|
|
29
|
-
let isNavigating = false;
|
|
31
|
+
let smoothedLeeway = 0, rotationTrend = 0;
|
|
32
|
+
let lastShortAvgVal = null, lastInstantTwa = null;
|
|
33
|
+
let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
|
|
34
|
+
let twDirty = false, isNavigating = false, reconnectDelay = 1000;
|
|
30
35
|
|
|
31
|
-
let pressTimer, isFocusActive = false
|
|
36
|
+
let pressTimer, isFocusActive = false;
|
|
32
37
|
|
|
33
38
|
const graphModes = {
|
|
34
39
|
stw: localStorage.getItem('mode_stw') || 'standard',
|
|
@@ -41,8 +46,8 @@ const store = {
|
|
|
41
46
|
raw: {}, timestamps: {},
|
|
42
47
|
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
43
48
|
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
44
|
-
histories: { stw: [], sog: [], depth: [], tws: [] },
|
|
45
|
-
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 }
|
|
49
|
+
histories: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
|
|
50
|
+
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0 }
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
const ui = {
|
|
@@ -61,7 +66,7 @@ const ui = {
|
|
|
61
66
|
};
|
|
62
67
|
|
|
63
68
|
// ==========================================================================
|
|
64
|
-
// 3. MATEMATICA E
|
|
69
|
+
// 3. UTILITIES DI SISTEMA (MATEMATICA E BUFFER)
|
|
65
70
|
// ==========================================================================
|
|
66
71
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
67
72
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
@@ -69,93 +74,128 @@ function msToKts(ms) { return ms * 1.94384; }
|
|
|
69
74
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
70
75
|
function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
|
|
71
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Gestione sicura dei buffer per prevenire memory leak (O(1) complexity).
|
|
79
|
+
*/
|
|
80
|
+
function safePush(buffer, val, time, maxLen = 200) {
|
|
81
|
+
buffer.push({ val: val, time: time });
|
|
82
|
+
if (buffer.length > maxLen) { buffer.shift(); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Calcola medie circolari restituendo Radianti.
|
|
87
|
+
*/
|
|
72
88
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
73
|
-
const now = Date.now();
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
74
91
|
if (validData.length === 0) return null;
|
|
75
|
-
let sSin = 0, sCos = 0;
|
|
92
|
+
let sSin = 0, sCos = 0;
|
|
93
|
+
validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
76
94
|
let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
|
|
77
95
|
let isStable = (validData.length > 2) && (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
78
|
-
let
|
|
79
|
-
return { val: signed ?
|
|
96
|
+
let avgRad = Math.atan2(sSin, sCos);
|
|
97
|
+
return { val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI), stable: isStable };
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
// ==========================================================================
|
|
83
|
-
// 4. LOGICA FISICA
|
|
101
|
+
// 4. LOGICA FISICA E CALCOLI
|
|
84
102
|
// ==========================================================================
|
|
85
103
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function playGybeAlarm() {
|
|
94
|
-
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
95
|
-
const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
|
|
96
|
-
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); }
|
|
97
|
-
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); }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function checkDepthAlarm(m) {
|
|
101
|
-
ui.depth.classList.remove('alarm-warning', 'alarm-danger');
|
|
102
|
-
if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); }
|
|
103
|
-
else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning');
|
|
104
|
-
}
|
|
105
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Calcola il Vento Reale partendo dagli Apparenti.
|
|
106
|
+
* Logica Acqua (Vele) vs Terra (Meteo).
|
|
107
|
+
*/
|
|
106
108
|
function computeTrueWind() {
|
|
107
109
|
const aws = store.raw["environment.wind.speedApparent"];
|
|
108
|
-
|
|
109
|
-
const stw = store.raw["navigation.speedThroughWater"] || 0;
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
const cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
110
|
+
let awa = store.raw["environment.wind.angleApparent"];
|
|
111
|
+
const stw = store.raw["navigation.speedThroughWater"] || 0, sog = store.raw["navigation.speedOverGround"] || 0;
|
|
112
|
+
const hdg = store.raw["navigation.headingTrue"] || 0, cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
113
|
+
|
|
113
114
|
if (aws === undefined || awa === undefined) return;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
115
|
+
if (awa > Math.PI) awa -= 2 * Math.PI;
|
|
116
|
+
|
|
117
|
+
// Vento Reale su ACQUA (TWA/TWS)
|
|
118
|
+
const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
|
|
119
|
+
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y), twa_water = Math.atan2(tw_water_y, tw_water_x);
|
|
120
|
+
|
|
121
|
+
// Vento Reale su TERRA (TWD)
|
|
122
|
+
const drift_angle = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
|
|
123
|
+
const sog_vec_x = sog * Math.cos(drift_angle), sog_vec_y = sog * Math.sin(drift_angle);
|
|
124
|
+
const tw_ground_x = aws * Math.cos(awa) - sog_vec_x, tw_ground_y = aws * Math.sin(awa) - sog_vec_y;
|
|
125
|
+
let twd_ground = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
|
|
126
|
+
|
|
124
127
|
const now = Date.now();
|
|
125
|
-
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
126
|
-
|
|
127
|
-
store.
|
|
128
|
-
store.smoothBuf.
|
|
129
|
-
store.longBuf.twa.push({ val: twa_water, time: now });
|
|
130
|
-
store.smoothBuf.twd.push({ val: twd_ground, time: now });
|
|
131
|
-
store.longBuf.twd.push({ val: twd_ground, time: now });
|
|
128
|
+
store.raw["environment.wind.speedTrue"] = tws_water; store.raw["environment.wind.angleTrueWater"] = twa_water; store.raw["environment.wind.directionTrue"] = twd_ground;
|
|
129
|
+
|
|
130
|
+
safePush(store.smoothBuf.twa, twa_water, now); safePush(store.longBuf.twa, twa_water, now);
|
|
131
|
+
safePush(store.smoothBuf.twd, twd_ground, now); safePush(store.longBuf.twd, twd_ground, now);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
function processIncomingData(path, val) {
|
|
135
|
+
const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
|
|
136
|
+
if (path === "navigation.position") store.raw["navigation.position"] = val;
|
|
137
|
+
if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
|
|
138
|
+
|
|
139
|
+
// Dirty flag + Debounce 100ms
|
|
140
|
+
const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
|
|
141
|
+
if (twPaths.includes(path)) twDirty = true;
|
|
142
|
+
if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
|
|
143
|
+
|
|
144
|
+
if (path === "navigation.headingTrue") { safePush(store.smoothBuf.hdg, val, now); safePush(store.longBuf.hdg, val, now); }
|
|
145
|
+
if (path === "navigation.courseOverGroundTrue") { safePush(store.smoothBuf.cog, val, now); safePush(store.longBuf.cog, val, now); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ==========================================================================
|
|
149
|
+
// 5. AUDIO E ALLARMI
|
|
150
|
+
// ==========================================================================
|
|
151
|
+
function playBingBing() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
|
|
152
|
+
function playGybeAlarm() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n; function note(f, s, d) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.type = 'square'; o.frequency.value = f; g.gain.setValueAtTime(0.05, s); g.gain.exponentialRampToValueAtTime(0.001, s + d); o.start(s); o.stop(s + d); } for (let i = 0; i < 4; i++) { note(1800, audioCtx.currentTime + (i * 0.15), 0.1); note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1); } }
|
|
153
|
+
function checkDepthAlarm(m) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
|
|
154
|
+
|
|
155
|
+
// ==========================================================================
|
|
156
|
+
// 6. TREND VENTO E TATTICA
|
|
157
|
+
// ==========================================================================
|
|
134
158
|
function updateWindTrend() {
|
|
159
|
+
const now = Date.now();
|
|
135
160
|
const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true);
|
|
136
161
|
const shortAvg = getCircularAverageFromBuffer(store.longBuf.twd, 5000, false);
|
|
137
162
|
const instantTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
163
|
+
|
|
138
164
|
if (!shortAvg || !twaAvgObj || instantTwaRad === undefined) return;
|
|
139
|
-
const instantTwaDeg = radToDeg(instantTwaRad);
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
const instantTwaDeg = radToDeg(instantTwaRad), shortAvgDeg = radToDeg(shortAvg.val), twaAvgDeg = radToDeg(twaAvgObj.val);
|
|
166
|
+
|
|
167
|
+
if (lastShortAvgVal === null) { lastShortAvgVal = shortAvgDeg; lastInstantTwa = instantTwaDeg; return; }
|
|
168
|
+
const dt = (now - lastTrendTime) / 1000; lastTrendTime = now;
|
|
169
|
+
|
|
170
|
+
// Gybe Safety
|
|
171
|
+
const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
|
|
142
172
|
lastInstantTwa = instantTwaDeg;
|
|
173
|
+
if (gybeDetected && isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
|
|
174
|
+
|
|
143
175
|
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
144
176
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
145
|
-
|
|
146
|
-
|
|
177
|
+
|
|
178
|
+
if (now - lastGybeAlarmTime < 4000 && isNavigating) {
|
|
147
179
|
[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'); }});
|
|
148
180
|
return;
|
|
149
181
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
|
|
183
|
+
// Trend calculation con Clamping e Alpha adattivo
|
|
184
|
+
let diff = (shortAvgDeg - lastShortAvgVal + 540) % 360 - 180; lastShortAvgVal = shortAvgDeg;
|
|
185
|
+
if (dt > 0) {
|
|
186
|
+
let rate = Math.max(-10, Math.min(10, diff / dt));
|
|
187
|
+
const alpha = Math.min(1, dt / 5);
|
|
188
|
+
rotationTrend = rotationTrend * (1 - alpha) + rate * alpha;
|
|
189
|
+
}
|
|
190
|
+
|
|
153
191
|
if (Math.abs(rotationTrend) > 1.0) {
|
|
154
|
-
const pos = store.raw["navigation.position"];
|
|
155
|
-
|
|
156
|
-
let
|
|
157
|
-
|
|
192
|
+
const pos = store.raw["navigation.position"], isSouth = pos && pos.latitude < 0;
|
|
193
|
+
let meteoColor = (!isSouth) ? (rotationTrend < 0 ? "#00ff00" : "#ff0000") : (rotationTrend > 0 ? "#00ff00" : "#ff0000");
|
|
194
|
+
let isLift = (twaAvgDeg > 0) ? (rotationTrend > 0) : (rotationTrend < 0);
|
|
195
|
+
const absTwa = Math.abs(twaAvgDeg);
|
|
196
|
+
if (absTwa >= 90 && absTwa < 160) isLift = !isLift; else if (absTwa >= 160) isLift = false;
|
|
158
197
|
const tacticColor = isLift ? "#00ff00" : "#ff0000";
|
|
198
|
+
|
|
159
199
|
if (rotationTrend > 0) {
|
|
160
200
|
if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
|
|
161
201
|
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
|
|
@@ -171,60 +211,64 @@ function updateWindTrend() {
|
|
|
171
211
|
}
|
|
172
212
|
|
|
173
213
|
// ==========================================================================
|
|
174
|
-
//
|
|
175
|
-
// ==========================================================================
|
|
176
|
-
function processIncomingData(path, val) {
|
|
177
|
-
const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
|
|
178
|
-
if (path === "navigation.position") store.raw["navigation.position"] = val;
|
|
179
|
-
if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
|
|
180
|
-
if (path === "environment.wind.speedApparent" || path === "environment.wind.angleApparent" || path === "navigation.speedThroughWater") computeTrueWind();
|
|
181
|
-
if (path === "navigation.headingTrue") { store.smoothBuf.hdg.push({ val: val, time: now }); store.longBuf.hdg.push({ val: val, time: now }); }
|
|
182
|
-
if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ==========================================================================
|
|
186
|
-
// 6. MOTORE RENDERING UI
|
|
214
|
+
// 7. RENDERING ENGINE (TIERED RENDERING)
|
|
187
215
|
// ==========================================================================
|
|
188
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)}°`; }
|
|
189
217
|
|
|
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
|
+
}
|
|
226
|
+
|
|
190
227
|
function startDisplayLoop() {
|
|
228
|
+
let tick = 0;
|
|
191
229
|
renderInterval = setInterval(() => {
|
|
192
|
-
const now = Date.now();
|
|
230
|
+
const now = Date.now(); tick++;
|
|
231
|
+
|
|
232
|
+
// --- LIVE TIER (1s) ---
|
|
193
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 };
|
|
194
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]; } }
|
|
195
235
|
|
|
196
|
-
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
|
|
197
|
-
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
236
|
+
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0), sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
198
237
|
isNavigating = stwKts > CONFIG.averages.minSpeed || sogKts > CONFIG.averages.minSpeed;
|
|
199
238
|
|
|
200
239
|
if (store.raw["navigation.speedThroughWater"] !== undefined) { ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts); }
|
|
201
240
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
ui.sog.style.color =
|
|
241
|
+
const twaRad = store.raw["environment.wind.angleTrueWater"], vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
|
|
242
|
+
manageHistory('vmg', vmgKts); manageHistory('sog', sogKts);
|
|
243
|
+
if (displayModeSog === 'VMG') { ui.sog.innerText = vmgKts.toFixed(1); ui.sog.style.color = "#64ffda"; document.getElementById('sog-vmg-label').textContent = 'VMG'; }
|
|
244
|
+
else { ui.sog.innerText = sogKts.toFixed(1); ui.sog.style.color = (sogKts-stwKts > 0.3) ? "#2ecc71" : (sogKts-stwKts < -0.3 ? "#e74c3c" : "#fff"); document.getElementById('sog-vmg-label').textContent = 'SOG'; }
|
|
206
245
|
}
|
|
207
246
|
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); }
|
|
208
|
-
if (store.raw["environment.wind.speedTrue"] !== undefined) {
|
|
209
|
-
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = twsKts.toFixed(1);
|
|
210
|
-
ui.tws.style.color = (twsKts >= CONFIG.graphs.reef2) ? "#e74c3c" : (twsKts >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff");
|
|
211
|
-
manageHistory('tws', twsKts);
|
|
212
|
-
}
|
|
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); }
|
|
213
248
|
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
214
249
|
|
|
215
250
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
|
|
216
|
-
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, smAwa.val); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
251
|
+
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
217
252
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
218
|
-
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
253
|
+
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
219
254
|
|
|
220
255
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
221
|
-
let
|
|
222
|
-
if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (
|
|
256
|
+
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
257
|
+
if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
223
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; ui.leewayVal.style.color = isContaminated ? "#f39c12" : "#fff";
|
|
224
260
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
225
|
-
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
updateWindTrend(); // Fast update
|
|
264
|
+
|
|
265
|
+
// --- HEAVY TIER (2s) ---
|
|
266
|
+
if (tick % 2 === 0) {
|
|
267
|
+
refreshGraph('stw'); refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog'); refreshGraph('depth'); refreshGraph('tws');
|
|
268
|
+
}
|
|
226
269
|
|
|
227
|
-
|
|
270
|
+
// --- SLOW TIER (3s) ---
|
|
271
|
+
if (tick % 3 === 0) {
|
|
228
272
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
|
|
229
273
|
cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
|
|
230
274
|
awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true),
|
|
@@ -234,26 +278,31 @@ function startDisplayLoop() {
|
|
|
234
278
|
const upUI = (el, obj, isCompass = false) => {
|
|
235
279
|
if (!obj || obj.val === null) { el.innerHTML = "---°"; el.classList.remove('unstable-data'); }
|
|
236
280
|
else {
|
|
237
|
-
let
|
|
281
|
+
let valDeg = Math.round(radToDeg(obj.val)); el.innerHTML = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
238
282
|
if (obj.stable || !isNavigating) el.classList.remove('unstable-data'); else el.classList.add('unstable-data');
|
|
239
283
|
}
|
|
240
284
|
};
|
|
241
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);
|
|
242
286
|
if (hObj && twObj && hObj.val !== null) {
|
|
243
|
-
const
|
|
244
|
-
ui.tackHdg.innerHTML = `${Math.round((
|
|
245
|
-
if (cObj)
|
|
287
|
+
const tackHdgDeg = radToDeg((hObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
|
|
288
|
+
ui.tackHdg.innerHTML = `${Math.round((tackHdgDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
289
|
+
if (cObj) {
|
|
290
|
+
const tackCogDeg = radToDeg((cObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
|
|
291
|
+
ui.tackCog.innerHTML = `${Math.round((tackCogDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const smHdg = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false), smTwd = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
295
|
+
if (smHdg && smTwd) {
|
|
296
|
+
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwd.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
297
|
+
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdg.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
246
298
|
}
|
|
247
|
-
if (twdObj && hObj) { updateWindTrend(); curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`); }
|
|
248
|
-
lastAvgUIUpdate = now;
|
|
249
299
|
}
|
|
250
|
-
|
|
251
|
-
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(); }
|
|
300
|
+
if (tick % 60 === 0) tick = 0;
|
|
252
301
|
}, RENDER_INTERVAL_MS);
|
|
253
302
|
}
|
|
254
303
|
|
|
255
304
|
// ==========================================================================
|
|
256
|
-
//
|
|
305
|
+
// 8. INTERAZIONI E RETE
|
|
257
306
|
// ==========================================================================
|
|
258
307
|
async function fetchServerConfig() {
|
|
259
308
|
if (!window.location.protocol.includes("http")) return;
|
|
@@ -281,8 +330,6 @@ async function fetchServerConfig() {
|
|
|
281
330
|
function manageHistory(t, v) {
|
|
282
331
|
const n = Date.now(); const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
283
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; }
|
|
284
|
-
const mode = graphModes[t], cfg = calculateScale(t, store.histories[t], mode);
|
|
285
|
-
updateScaleLabels(t, cfg.min, cfg.max); drawGraph(store.histories[t], t + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
286
333
|
}
|
|
287
334
|
|
|
288
335
|
function calculateScale(type, data, mode) {
|
|
@@ -308,14 +355,10 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
308
355
|
}
|
|
309
356
|
});
|
|
310
357
|
const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#ffffff' };
|
|
311
|
-
|
|
358
|
+
const colorKey = id === 'sog-graph' && displayModeSog === 'VMG' ? '#64ffda' : clrs[id];
|
|
359
|
+
svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(255,255,255,0.08)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${colorKey}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${colorKey}" />`;
|
|
312
360
|
}
|
|
313
361
|
|
|
314
|
-
// ==========================================================================
|
|
315
|
-
// 8. INTERAZIONI
|
|
316
|
-
// ==========================================================================
|
|
317
|
-
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
318
|
-
|
|
319
362
|
function toggleFocusMode(type, element) {
|
|
320
363
|
const container = document.querySelector('.main-container'); const parentPanel = element.closest('.side-panel'); const isLeft = parentPanel.classList.contains('left-panel');
|
|
321
364
|
isFocusActive = !isFocusActive;
|
|
@@ -324,14 +367,16 @@ function toggleFocusMode(type, element) {
|
|
|
324
367
|
}
|
|
325
368
|
|
|
326
369
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
327
|
-
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
370
|
+
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
371
|
+
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
372
|
+
el.addEventListener('pointerdown', (e) => { isLongPressActive = false; pressTimer = setTimeout(() => { if (!isFocusActive) { isLongPressActive = true; toggleFocusMode(type, el); lastTapTime = 0; } }, 1000); });
|
|
373
|
+
el.addEventListener('pointerup', (e) => {
|
|
374
|
+
clearTimeout(pressTimer); if (isLongPressActive) return;
|
|
375
|
+
const currentTime = new Date().getTime(), tapDelay = currentTime - lastTapTime;
|
|
331
376
|
if (tapDelay < 300 && tapDelay > 0) { clearTimeout(tapTimeout); if (!isFocusActive) { graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); } lastTapTime = 0; }
|
|
332
|
-
else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); },
|
|
377
|
+
else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(100, 255, 218, 0.1)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
|
|
333
378
|
});
|
|
334
|
-
el.addEventListener('
|
|
379
|
+
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
335
380
|
});
|
|
336
381
|
|
|
337
382
|
if (ui.hotspot) {
|
|
@@ -340,25 +385,23 @@ if (ui.hotspot) {
|
|
|
340
385
|
ui.hotspot.addEventListener('pointerleave', () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } });
|
|
341
386
|
}
|
|
342
387
|
|
|
343
|
-
// ==========================================================================
|
|
344
|
-
// 9. CONNESSIONE E SIMULATORE
|
|
345
|
-
// ==========================================================================
|
|
346
388
|
function connect() {
|
|
347
389
|
if (simulationMode) return;
|
|
348
390
|
let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
|
|
391
|
+
const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws';
|
|
349
392
|
try {
|
|
350
|
-
socket = new WebSocket(
|
|
351
|
-
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; };
|
|
393
|
+
socket = new WebSocket(`${protocol}://${addr}/signalk/v1/stream?subscribe=self`);
|
|
394
|
+
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; reconnectDelay = 1000; };
|
|
352
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))); };
|
|
353
|
-
socket.onclose = () => !simulationMode
|
|
354
|
-
} catch (e) { setTimeout(connect,
|
|
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); }
|
|
355
398
|
}
|
|
356
399
|
|
|
357
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; } }; })());
|
|
358
401
|
|
|
359
402
|
function startDynamicSimulation() {
|
|
360
403
|
ui.status.innerText = "SIM ATTIVO";
|
|
361
|
-
let sim = { hdg: 45, tws: 12, twd: 45, depth: 12, stw: 5, leeway: 0, startTime: Date.now() };
|
|
404
|
+
let sim = { hdg: 45, tws: 12, twd: 45, depth: 12, stw: 5, leeway: 0, currentSpeed: 1.5, currentDir: 90, startTime: Date.now() };
|
|
362
405
|
simInterval = setInterval(() => {
|
|
363
406
|
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
364
407
|
if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
|
|
@@ -368,19 +411,22 @@ function startDynamicSimulation() {
|
|
|
368
411
|
let twaRel = (sim.twd - sim.hdg + 360) % 360; if (twaRel > 180) twaRel -= 360;
|
|
369
412
|
let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125)); sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
|
|
370
413
|
const rawLeeway = Math.sin(degToRad(twaRel)) * 4; sim.leeway += (rawLeeway - sim.leeway) * 0.05;
|
|
371
|
-
const
|
|
372
|
-
const
|
|
414
|
+
const bX = sim.stw * Math.sin(degToRad(sim.hdg + sim.leeway)), bY = sim.stw * Math.cos(degToRad(sim.hdg + sim.leeway));
|
|
415
|
+
const cX = sim.currentSpeed * Math.sin(degToRad(sim.currentDir)), cY = sim.currentSpeed * Math.cos(degToRad(sim.currentDir));
|
|
416
|
+
const sog = Math.sqrt(Math.pow(bX + cX, 2) + Math.pow(bY + cY, 2)), cog = (radToDeg(Math.atan2(bX + cX, bY + cY)) + 360) % 360;
|
|
417
|
+
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));
|
|
373
418
|
const awa = Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad));
|
|
374
419
|
processIncomingData("environment.wind.speedApparent", ktsToMs(aws)); processIncomingData("environment.wind.angleApparent", awa);
|
|
375
420
|
processIncomingData("environment.depth.belowTransducer", sim.depth); processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
376
|
-
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw)); processIncomingData("navigation.speedOverGround", ktsToMs(
|
|
421
|
+
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw)); processIncomingData("navigation.speedOverGround", ktsToMs(sog));
|
|
377
422
|
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog)); processIncomingData("navigation.position", { latitude: 45.0, longitude: 12.0 });
|
|
378
423
|
}, 1000);
|
|
379
424
|
}
|
|
380
425
|
|
|
381
426
|
// ==========================================================================
|
|
382
|
-
//
|
|
427
|
+
// 11. INIT
|
|
383
428
|
// ==========================================================================
|
|
429
|
+
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
384
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); } } })();
|
|
385
431
|
async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
|
|
386
432
|
window.addEventListener('load', init);
|
package/index.html
CHANGED
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
|
|
14
|
-
<!-- Etichetta stato connessione SignalK -->
|
|
15
|
-
<div id="status" class="offline">OFFLINE</div>
|
|
16
|
-
|
|
17
14
|
<div class="main-container">
|
|
18
15
|
|
|
19
16
|
<!-- ======================================================= -->
|
|
@@ -34,10 +31,10 @@
|
|
|
34
31
|
</div>
|
|
35
32
|
</div>
|
|
36
33
|
|
|
37
|
-
<!-- SOG: Velocità sul fondo
|
|
34
|
+
<!-- SOG / VMG: Velocità sul fondo o verso il vento -->
|
|
38
35
|
<div class="data-box">
|
|
39
36
|
<div class="label-row">
|
|
40
|
-
<span class="label">SOG</span>
|
|
37
|
+
<span class="label" id="sog-vmg-label">SOG</span>
|
|
41
38
|
<span class="unit">kts</span>
|
|
42
39
|
</div>
|
|
43
40
|
<span class="value" id="sog">0.0</span>
|
|
@@ -83,15 +80,15 @@
|
|
|
83
80
|
</div>
|
|
84
81
|
|
|
85
82
|
<!-- ======================================================= -->
|
|
86
|
-
<!-- CENTRO: Strumento Vento SVG (
|
|
83
|
+
<!-- CENTRO: Strumento Vento SVG (Pannello di Controllo) -->
|
|
87
84
|
<!-- ======================================================= -->
|
|
88
85
|
<div class="center-panel">
|
|
89
|
-
<!--
|
|
86
|
+
<!-- Stato Connessione integrato nell'angolo del pannello centrale -->
|
|
87
|
+
<div id="status" class="offline">OFFLINE</div>
|
|
88
|
+
|
|
90
89
|
<svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
|
|
91
90
|
<defs>
|
|
92
|
-
<!-- Maschera per tagliare la barca esattamente sul bordo del cerchio r=50 -->
|
|
93
91
|
<clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
94
|
-
<!-- Gradienti e Maschere per i settori del vento -->
|
|
95
92
|
<linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
96
93
|
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
|
97
94
|
<stop offset="100%" style="stop-color:#888888;stop-opacity:1" />
|
|
@@ -103,74 +100,52 @@
|
|
|
103
100
|
<stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" />
|
|
104
101
|
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
105
102
|
</linearGradient>
|
|
106
|
-
<!-- Gradiente Leeway per la Night Mode (Rosso cupo al centro, Rosso vivo ai lati) -->
|
|
107
103
|
<linearGradient id="leeway-night-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
108
|
-
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
109
|
-
<stop offset="50%" style="stop-color:#330000;stop-opacity:1" />
|
|
110
|
-
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
104
|
+
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
105
|
+
<stop offset="50%" style="stop-color:#330000;stop-opacity:1" />
|
|
106
|
+
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
111
107
|
</linearGradient>
|
|
112
108
|
<clipPath id="leeway-clip">
|
|
113
109
|
<rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" />
|
|
114
110
|
</clipPath>
|
|
115
|
-
|
|
116
|
-
<!-- Filtro Glow per l'area di interazione (Hotspot) centrale -->
|
|
117
111
|
<filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
118
112
|
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
119
113
|
</filter>
|
|
120
114
|
</defs>
|
|
121
115
|
|
|
122
|
-
<!-- Sfondo circolare del quadrante -->
|
|
123
116
|
<circle cx="200" cy="200" r="160" fill="#050505" />
|
|
124
117
|
<circle cx="200" cy="200" r="125" fill="#121212" />
|
|
125
118
|
|
|
126
|
-
<!-- Settori Vento (Rosso/Sinitra, Verde/Dritta, Arancio/Poppa) -->
|
|
127
119
|
<path d="M 82.0 101.0 A 154 154 0 0 1 142.3 57.2" fill="none" stroke="#ff0000" stroke-width="12" opacity="1"/>
|
|
128
120
|
<path d="M 257.7 57.2 A 154 154 0 0 1 318.0 101.0" fill="none" stroke="#00ff00" stroke-width="12" opacity="1"/>
|
|
129
121
|
<path d="M 265.1 339.6 A 154 154 0 0 1 134.9 339.6" fill="none" stroke="#ff8800" stroke-width="12" opacity="1"/>
|
|
130
122
|
|
|
131
|
-
<!-- Tacche Gradate (Generate dinamicamente da Javascript) -->
|
|
132
123
|
<g id="ticks"></g>
|
|
133
124
|
|
|
134
|
-
<!-- Etichette fisse dei Gradi -->
|
|
135
125
|
<g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="hanging" font-family="Arial" font-weight="bold">
|
|
136
126
|
<text font-size="16" transform="translate(200, 65)">0</text>
|
|
137
127
|
<text font-size="16" transform="translate(335, 200) rotate(90)">90</text>
|
|
138
128
|
<text font-size="16" transform="translate(65, 200) rotate(-90)">90</text>
|
|
139
129
|
<text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
|
|
140
|
-
|
|
141
|
-
<!-- Dettagli 30-150 Gradi -->
|
|
142
|
-
<text font-size="11" transform="translate(267.5, 83) rotate(30)">30</text>
|
|
143
|
-
<text font-size="11" transform="translate(317, 132.5) rotate(60)">60</text>
|
|
144
|
-
<text font-size="11" transform="translate(317, 267.5) rotate(120)">120</text>
|
|
145
|
-
<text font-size="11" transform="translate(267.5, 317) rotate(150)">150</text>
|
|
146
|
-
<text font-size="11" transform="translate(132.5, 83) rotate(-30)">30</text>
|
|
147
|
-
<text font-size="11" transform="translate(83, 132.5) rotate(-60)">60</text>
|
|
148
|
-
<text font-size="11" transform="translate(83, 267.5) rotate(-120)">120</text>
|
|
149
|
-
<text font-size="11" transform="translate(132.5, 317) rotate(-150)">150</text>
|
|
150
130
|
</g>
|
|
151
131
|
|
|
152
|
-
<!-- Puntatore Track / COG (Blu) -->
|
|
153
132
|
<g id="track-pointer" transform="rotate(0, 200, 200)">
|
|
154
133
|
<path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" />
|
|
155
134
|
</g>
|
|
156
135
|
|
|
157
|
-
<!-- Grande area sensibile al tocco (Hotspot) con Glow per Fullscreen -->
|
|
158
136
|
<circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
|
|
159
137
|
|
|
160
|
-
<!-- Icona Barca Centrale (Spinta Y+5 per centratura visiva perfetta) -->
|
|
161
138
|
<path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
|
|
162
139
|
fill="url(#axiom-grad)"
|
|
163
140
|
transform="translate(0, 5)"
|
|
164
141
|
clip-path="url(#boat-clip)"
|
|
165
142
|
style="pointer-events: none;" />
|
|
166
143
|
|
|
167
|
-
<!-- Display Centrale Numerico: Vento Apparente -->
|
|
168
144
|
<g id="aws-display-group" transform="translate(200, 265)">
|
|
169
145
|
<text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
|
|
170
146
|
<text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
|
|
171
147
|
</g>
|
|
172
148
|
|
|
173
|
-
<!-- Lancette Dinamiche: AWA (Apparente) e TWA (Reale) -->
|
|
174
149
|
<g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
|
|
175
150
|
<path d="M200,80 L211,95 L200,145 L189,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" />
|
|
176
151
|
<text x="200" y="102" fill="#000" font-size="11" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
|
|
@@ -183,24 +158,25 @@
|
|
|
183
158
|
<circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#ffffff" opacity="0.3" />
|
|
184
159
|
</g>
|
|
185
160
|
|
|
186
|
-
<!-- Barra LEEWAY / SCARROCCIO Inferiore -->
|
|
187
161
|
<g transform="translate(75, 395)">
|
|
188
162
|
<text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0°</text>
|
|
189
163
|
<rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
|
|
190
164
|
<rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
|
|
191
165
|
|
|
192
|
-
<!--
|
|
166
|
+
<!-- RIPRISTINO SCALA: Tacche ogni 5 gradi (31.25px per intervallo) -->
|
|
193
167
|
<g stroke="#555" stroke-width="1">
|
|
194
|
-
<line x1="0" y1="-2" x2="0" y2="14" />
|
|
195
|
-
<line x1="31.25" y1="2" x2="31.25" y2="10" />
|
|
196
|
-
<line x1="62.5" y1="2" x2="62.5" y2="10" />
|
|
197
|
-
<line x1="93.75" y1="3" x2="93.75" y2="9" />
|
|
198
|
-
<line x1="125" y1="-2" x2="125" y2="14" />
|
|
199
|
-
<line x1="156.25" y1="3" x2="156.25" y2="9" />
|
|
200
|
-
<line x1="187.5" y1="2" x2="187.5" y2="10" />
|
|
201
|
-
<line x1="218.75" y1="2" x2="218.75" y2="10" />
|
|
202
|
-
<line x1="250" y1="-2" x2="250" y2="14" />
|
|
168
|
+
<line x1="0" y1="-2" x2="0" y2="14" /> <!-- -20° -->
|
|
169
|
+
<line x1="31.25" y1="2" x2="31.25" y2="10" /> <!-- -15° -->
|
|
170
|
+
<line x1="62.5" y1="2" x2="62.5" y2="10" /> <!-- -10° -->
|
|
171
|
+
<line x1="93.75" y1="3" x2="93.75" y2="9" /> <!-- -5° -->
|
|
172
|
+
<line x1="125" y1="-2" x2="125" y2="14" /> <!-- 0° centrale -->
|
|
173
|
+
<line x1="156.25" y1="3" x2="156.25" y2="9" /> <!-- +5° -->
|
|
174
|
+
<line x1="187.5" y1="2" x2="187.5" y2="10" /> <!-- +10° -->
|
|
175
|
+
<line x1="218.75" y1="2" x2="218.75" y2="10" /> <!-- +15° -->
|
|
176
|
+
<line x1="250" y1="-2" x2="250" y2="14" /> <!-- +20° -->
|
|
203
177
|
</g>
|
|
178
|
+
|
|
179
|
+
<!-- Etichette numeriche riallineate -->
|
|
204
180
|
<g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
|
|
205
181
|
<text x="0" y="24">-20°</text>
|
|
206
182
|
<text x="62.5" y="24">-10</text>
|
|
@@ -217,7 +193,6 @@
|
|
|
217
193
|
<!-- ======================================================= -->
|
|
218
194
|
<div class="side-panel right-panel">
|
|
219
195
|
|
|
220
|
-
<!-- DEPTH: Profondità sotto il trasduttore -->
|
|
221
196
|
<div class="data-box">
|
|
222
197
|
<div class="label-row">
|
|
223
198
|
<span class="unit">m</span>
|
|
@@ -230,7 +205,6 @@
|
|
|
230
205
|
</div>
|
|
231
206
|
</div>
|
|
232
207
|
|
|
233
|
-
<!-- TWS: Velocità del vento reale -->
|
|
234
208
|
<div class="data-box">
|
|
235
209
|
<div class="label-row">
|
|
236
210
|
<span class="unit">kts</span>
|
|
@@ -243,7 +217,6 @@
|
|
|
243
217
|
</div>
|
|
244
218
|
</div>
|
|
245
219
|
|
|
246
|
-
<!-- TWA: Angolo del vento reale -->
|
|
247
220
|
<div class="data-box">
|
|
248
221
|
<div class="label-row">
|
|
249
222
|
<span class="label">TWA (MEAN)</span>
|
|
@@ -251,7 +224,6 @@
|
|
|
251
224
|
<span class="value value-large" id="twa-avg">---°</span>
|
|
252
225
|
</div>
|
|
253
226
|
|
|
254
|
-
<!-- AWA: Angolo del vento apparente -->
|
|
255
227
|
<div class="data-box">
|
|
256
228
|
<div class="label-row">
|
|
257
229
|
<span class="label">AWA (MEAN)</span>
|
|
@@ -259,48 +231,30 @@
|
|
|
259
231
|
<span class="value value-large" id="awa-avg">---°</span>
|
|
260
232
|
</div>
|
|
261
233
|
|
|
262
|
-
<!-- TWD: Direzione geografica reale del vento (con Bussola) -->
|
|
263
234
|
<div class="data-box">
|
|
264
235
|
<div class="label-row">
|
|
265
236
|
<span class="label">TWD (MEAN)</span>
|
|
266
237
|
</div>
|
|
267
238
|
<div class="value-with-compass">
|
|
268
239
|
<svg class="mini-compass" viewBox="0 0 40 40">
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
<!-- 1. SAGOMA BARCA: Ruota con l'Heading (id: twd-boat-wrap) -->
|
|
283
|
-
<g id="twd-boat-wrap" transform="rotate(0, 20, 20)">
|
|
284
|
-
<path d="M 20,17 L 17,26 L 20,24.5 L 23,26 Z" fill="white" opacity="0.2" />
|
|
285
|
-
</g>
|
|
286
|
-
|
|
287
|
-
<g id="twd-arrow" transform="rotate(0, 20, 20)">
|
|
288
|
-
<path id="twd-wind-chevron" d="M 17,5 L 20,7.5 L 23,5"
|
|
289
|
-
fill="none"
|
|
290
|
-
stroke="#ffff00"
|
|
291
|
-
stroke-width="2.2"
|
|
292
|
-
stroke-linecap="round"
|
|
293
|
-
stroke-linejoin="round" />
|
|
294
|
-
<circle id="trend-dot-cw" cx="24" cy="7.5" r="1.5" fill="#ffffff" opacity="0.3" />
|
|
295
|
-
<circle id="trend-dot-ccw" cx="16" cy="7.5" r="1.5" fill="#ffffff" opacity="0.3" />
|
|
296
|
-
</g>
|
|
297
|
-
</svg>
|
|
240
|
+
<circle cx="20" cy="20" r="18.5" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1.2"/>
|
|
241
|
+
<text x="20" y="8" fill="#e74c3c" font-size="5.5" text-anchor="middle" font-weight="900">N</text>
|
|
242
|
+
<text x="20" y="36" fill="rgba(255,255,255,0.3)" font-size="5.5" text-anchor="middle" font-weight="900">S</text>
|
|
243
|
+
<g id="twd-boat-wrap" transform="rotate(0, 20, 20)">
|
|
244
|
+
<path d="M 20,17 L 17,26 L 20,24.5 L 23,26 Z" fill="white" opacity="0.2" />
|
|
245
|
+
</g>
|
|
246
|
+
<g id="twd-arrow" transform="rotate(0, 20, 20)">
|
|
247
|
+
<path id="twd-wind-chevron" d="M 17,5 L 20,7.5 L 23,5" fill="none" stroke="#ffff00" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
|
|
248
|
+
<circle id="trend-dot-cw" cx="24" cy="7.5" r="1.5" fill="#ffffff" opacity="0.3" />
|
|
249
|
+
<circle id="trend-dot-ccw" cx="16" cy="7.5" r="1.5" fill="#ffffff" opacity="0.3" />
|
|
250
|
+
</g>
|
|
251
|
+
</svg>
|
|
298
252
|
<span class="value value-large" id="twd-avg">---°</span>
|
|
299
253
|
</div>
|
|
300
254
|
</div>
|
|
301
255
|
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
304
258
|
|
|
305
259
|
<script src="app.js"></script>
|
|
306
260
|
</body>
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -25,27 +25,17 @@ body {
|
|
|
25
25
|
box-sizing: border-box;
|
|
26
26
|
gap: 8px; /* Spazio costante tra tutti i blocchi della dashboard */
|
|
27
27
|
|
|
28
|
-
/*
|
|
29
|
-
LAYOUT LIQUIDO:
|
|
28
|
+
/* LAYOUT LIQUIDO:
|
|
30
29
|
I lati hanno un minimo vitale (180px) per proteggere i testi.
|
|
31
|
-
Il centro (auto) si adatta millimetricamente al diametro dell'SVG.
|
|
32
|
-
*/
|
|
30
|
+
Il centro (auto) si adatta millimetricamente al diametro dell'SVG. */
|
|
33
31
|
grid-template-columns: minmax(180px, 1fr) minmax(auto, 1.5fr) minmax(180px, 1fr);
|
|
34
32
|
grid-template-rows: 100%;
|
|
35
33
|
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
36
34
|
justify-content: stretch;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
/* Espansione colonne per schermi molto larghi (es. 16:10 / 16:9)
|
|
40
|
-
@media (min-aspect-ratio: 1.5) {
|
|
41
|
-
.main-container {
|
|
42
|
-
grid-template-columns: minmax(200px, 1fr) minmax(auto, 3fr) minmax(200px, 1fr);
|
|
43
|
-
gap: 15px;
|
|
44
|
-
}
|
|
45
|
-
}*/
|
|
46
|
-
|
|
47
37
|
/* ==========================================================================
|
|
48
|
-
2. PANNELLI E DATA-BOX
|
|
38
|
+
2. PANNELLI E DATA-BOX (DISTRIBUZIONE FR)
|
|
49
39
|
========================================================================== */
|
|
50
40
|
.side-panel {
|
|
51
41
|
display: flex;
|
|
@@ -54,12 +44,14 @@ body {
|
|
|
54
44
|
border-radius: 12px;
|
|
55
45
|
height: 100%;
|
|
56
46
|
overflow: hidden;
|
|
47
|
+
gap: 2px;
|
|
57
48
|
}
|
|
58
49
|
|
|
59
50
|
.left-panel { grid-column: 1; }
|
|
60
51
|
|
|
61
52
|
.center-panel {
|
|
62
53
|
grid-column: 2;
|
|
54
|
+
position: relative; /* Indispensabile per posizionare #status al suo interno */
|
|
63
55
|
display: flex;
|
|
64
56
|
flex-direction: column;
|
|
65
57
|
align-items: center;
|
|
@@ -77,13 +69,14 @@ body {
|
|
|
77
69
|
.data-box {
|
|
78
70
|
position: relative;
|
|
79
71
|
border-bottom: 1px solid #222;
|
|
80
|
-
padding: 4px
|
|
72
|
+
padding: 2px 4px; /* Padding ottimizzato per il recupero spazio verticale */
|
|
81
73
|
display: flex;
|
|
82
74
|
flex-direction: column;
|
|
83
75
|
width: 100%;
|
|
84
76
|
box-sizing: border-box;
|
|
85
|
-
container-type: size; /*
|
|
86
|
-
flex: 1;
|
|
77
|
+
container-type: size; /* Per unità cqh */
|
|
78
|
+
flex: 1 1 0px; /* Divide lo spazio equamente in 5 parti */
|
|
79
|
+
min-height: 0;
|
|
87
80
|
overflow: hidden;
|
|
88
81
|
}
|
|
89
82
|
|
|
@@ -91,35 +84,24 @@ body {
|
|
|
91
84
|
.left-panel .data-box { align-items: flex-start; text-align: left; }
|
|
92
85
|
.right-panel .data-box { align-items: flex-end; text-align: right; }
|
|
93
86
|
|
|
94
|
-
.side-panel {
|
|
95
|
-
display: flex;
|
|
96
|
-
flex-direction: column;
|
|
97
|
-
height: 100%;
|
|
98
|
-
gap: 2px;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.data-box {
|
|
102
|
-
flex: 1 1 0px; /* Ogni box si divide lo spazio equamente, ma può ridursi se serve */
|
|
103
|
-
min-height: 0; /* Fondamentale per permettere al grafico di non eccedere */
|
|
104
|
-
padding: 2px 4px; /* Ancora più compatto */
|
|
105
|
-
}
|
|
106
|
-
|
|
107
87
|
/* ==========================================================================
|
|
108
|
-
3. TACTICAL FOCUS MODE (AUTO-EXPANDING SPLIT)
|
|
88
|
+
3. TACTICAL FOCUS MODE (AUTO-EXPANDING SPLIT / DUAL SCREEN)
|
|
109
89
|
========================================================================== */
|
|
110
90
|
|
|
111
91
|
/* Nasconde le colonne non focalizzate */
|
|
112
92
|
.focus-active .side-panel:not(.has-focus) { display: none !important; }
|
|
113
93
|
|
|
114
|
-
/*
|
|
115
|
-
.focus-active
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.focus-active.focus-side-left { grid-template-columns: 1fr auto !important; }
|
|
94
|
+
/* In Focus Mode Left: [Pannello Sinistro Largo | Centro Stretto] */
|
|
95
|
+
.focus-active.focus-side-left {
|
|
96
|
+
grid-template-columns: 2fr auto !important;
|
|
97
|
+
}
|
|
119
98
|
.focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
|
|
120
99
|
.focus-active.focus-side-left .center-panel { grid-column: 2 !important; justify-content: flex-start; }
|
|
121
100
|
|
|
122
|
-
|
|
101
|
+
/* In Focus Mode Right: [Centro Stretto | Pannello Destro Largo] */
|
|
102
|
+
.focus-active.focus-side-right {
|
|
103
|
+
grid-template-columns: auto 2fr !important;
|
|
104
|
+
}
|
|
123
105
|
.focus-active.focus-side-right .center-panel { grid-column: 1 !important; justify-content: flex-start; }
|
|
124
106
|
.focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
|
|
125
107
|
|
|
@@ -138,15 +120,6 @@ body {
|
|
|
138
120
|
.focus-active .is-focused .label-row .label { font-size: 2rem !important; }
|
|
139
121
|
.focus-active .is-focused .label-row .unit { font-size: 2rem !important; }
|
|
140
122
|
|
|
141
|
-
/* Ingrandimento etichette Hercules in Focus */
|
|
142
|
-
.focus-active .is-focused.box-hercules .unit::before,
|
|
143
|
-
.focus-active .is-focused.box-hercules .unit::after,
|
|
144
|
-
.focus-active .is-focused.box-hercules .label::before,
|
|
145
|
-
.focus-active .is-focused.box-hercules .label::after {
|
|
146
|
-
font-size: 1.5rem !important;
|
|
147
|
-
letter-spacing: 2px !important;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
123
|
.focus-active .is-focused .sparkline path { stroke-width: 1px !important; }
|
|
151
124
|
|
|
152
125
|
/* ==========================================================================
|
|
@@ -154,17 +127,10 @@ body {
|
|
|
154
127
|
========================================================================== */
|
|
155
128
|
.label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
|
|
156
129
|
|
|
157
|
-
/* Simmetria Titoli/Unità: SX [Label...Unit], DX [Unit...Label] */
|
|
158
|
-
.left-panel .label-row { justify-content: space-between; flex-direction: row; }
|
|
159
|
-
.right-panel .label-row { justify-content: space-between; flex-direction: row; }
|
|
160
|
-
|
|
161
|
-
/* Forza i titoli MEAN (senza unità) a destra nella colonna DX */
|
|
162
|
-
.right-panel .label-row .label:only-child { margin-left: auto; }
|
|
163
|
-
|
|
164
130
|
.label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
|
|
165
131
|
.unit { color: #888; font-size: 0.6rem; font-weight: bold; }
|
|
166
132
|
|
|
167
|
-
/* Numero standard (22% altezza box
|
|
133
|
+
/* Numero standard (22% altezza box) */
|
|
168
134
|
.value {
|
|
169
135
|
color: #fff;
|
|
170
136
|
font-size: clamp(1.2rem, 22cqh, 3rem);
|
|
@@ -174,12 +140,7 @@ body {
|
|
|
174
140
|
padding-bottom: 5px;
|
|
175
141
|
}
|
|
176
142
|
|
|
177
|
-
|
|
178
|
-
.value-large, .dual-value-container, .value-with-compass {
|
|
179
|
-
margin-top: auto;
|
|
180
|
-
margin-bottom: auto;
|
|
181
|
-
}
|
|
182
|
-
|
|
143
|
+
.value-large, .dual-value-container, .value-with-compass { margin-top: auto; margin-bottom: auto; }
|
|
183
144
|
.value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
|
|
184
145
|
|
|
185
146
|
/* TACK Layout (Mure opposte) */
|
|
@@ -189,12 +150,11 @@ body {
|
|
|
189
150
|
.value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
|
|
190
151
|
|
|
191
152
|
/* ==========================================================================
|
|
192
|
-
5. BUSSOLA TWD DINAMICA
|
|
153
|
+
5. BUSSOLA TWD DINAMICA (WIDGET)
|
|
193
154
|
========================================================================== */
|
|
194
155
|
.value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 5px; }
|
|
195
156
|
|
|
196
157
|
.mini-compass {
|
|
197
|
-
/* Forma circolare protetta: 80% dell'altezza o 42% della larghezza */
|
|
198
158
|
width: min(80cqh, 42cqw);
|
|
199
159
|
height: min(80cqh, 42cqw);
|
|
200
160
|
aspect-ratio: 1 / 1;
|
|
@@ -206,13 +166,8 @@ body {
|
|
|
206
166
|
transition: all 0.4s ease;
|
|
207
167
|
}
|
|
208
168
|
|
|
209
|
-
.value-with-compass .value-large {
|
|
210
|
-
margin: 0; flex: 1; text-align: right;
|
|
211
|
-
font-size: clamp(1rem, 38cqh, 4rem); white-space: nowrap;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
169
|
/* ==========================================================================
|
|
215
|
-
6. GRAFICI E SCALE (
|
|
170
|
+
6. GRAFICI E SCALE (OTTIMIZZAZIONE ORIZZONTALE)
|
|
216
171
|
========================================================================== */
|
|
217
172
|
.graph-wrapper {
|
|
218
173
|
position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px;
|
|
@@ -220,7 +175,7 @@ body {
|
|
|
220
175
|
border-radius: 4px; gap: 0px !important;
|
|
221
176
|
}
|
|
222
177
|
|
|
223
|
-
/*
|
|
178
|
+
/* Allargamento verso il centro */
|
|
224
179
|
.left-panel .graph-wrapper { margin-right: -6px; }
|
|
225
180
|
.right-panel .graph-wrapper { margin-left: -6px; }
|
|
226
181
|
|
|
@@ -229,17 +184,17 @@ body {
|
|
|
229
184
|
|
|
230
185
|
.scale-labels {
|
|
231
186
|
display: flex; flex-direction: column; justify-content: space-between;
|
|
232
|
-
font-size:
|
|
233
|
-
height: 100%; line-height: 1; padding: 0
|
|
187
|
+
font-size: 8px; color: #777; font-weight: bold; min-width: 12px;
|
|
188
|
+
height: 100%; line-height: 1; padding: 0; border: none !important;
|
|
234
189
|
}
|
|
235
190
|
|
|
236
|
-
/* Simmetria
|
|
237
|
-
.left-panel .scale-labels { order: 2; text-align: left; padding-left: 4px;
|
|
191
|
+
/* Simmetria scale */
|
|
192
|
+
.left-panel .scale-labels { order: 2; text-align: left; padding-left: 4px; }
|
|
238
193
|
.left-panel .sparkline { order: 1; }
|
|
239
|
-
.right-panel .scale-labels { order: 1; text-align: right; padding-right: 4px;
|
|
194
|
+
.right-panel .scale-labels { order: 1; text-align: right; padding-right: 4px; }
|
|
240
195
|
.right-panel .sparkline { order: 2; }
|
|
241
196
|
|
|
242
|
-
/* Colori
|
|
197
|
+
/* Colori grafici */
|
|
243
198
|
#stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
|
|
244
199
|
#sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
|
|
245
200
|
#depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
|
|
@@ -252,9 +207,7 @@ body {
|
|
|
252
207
|
.box-hercules { background: rgba(255, 0, 0, 0.08) !important; }
|
|
253
208
|
.box-hercules .scale-labels { color: #ff8888; }
|
|
254
209
|
|
|
255
|
-
|
|
256
|
-
.box-hercules .unit::before, .box-hercules .unit::after,
|
|
257
|
-
.box-hercules .label::before {
|
|
210
|
+
.box-hercules .unit::before, .box-hercules .unit::after, .box-hercules .label::before {
|
|
258
211
|
font-size: 7px; color: #ff4444; font-weight: 900; letter-spacing: 1px; text-transform: uppercase;
|
|
259
212
|
}
|
|
260
213
|
.left-panel .box-hercules .unit::before { content: "HERCULES "; }
|
|
@@ -262,226 +215,100 @@ body {
|
|
|
262
215
|
.right-panel .box-hercules .label:only-child::after { content: " HERCULES"; }
|
|
263
216
|
|
|
264
217
|
/* ==========================================================================
|
|
265
|
-
8. STATI E ANIMAZIONI
|
|
218
|
+
8. STATI, STATUS E ANIMAZIONI
|
|
266
219
|
========================================================================== */
|
|
267
|
-
#status {
|
|
220
|
+
#status {
|
|
221
|
+
position: absolute;
|
|
222
|
+
top: 10px;
|
|
223
|
+
right: 10px;
|
|
224
|
+
font-size: 0.6rem;
|
|
225
|
+
font-weight: 900;
|
|
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;
|
|
232
|
+
}
|
|
268
233
|
.online { color: #2ecc71; opacity: 0.5; }
|
|
269
234
|
.offline { color: #e74c3c; font-weight: bold; }
|
|
270
235
|
|
|
271
|
-
/* Animazioni fluide per gli elementi rotanti (0.8s) */
|
|
272
236
|
#awa-pointer, #twa-pointer, #track-pointer, #twd-arrow, #twd-boat-wrap {
|
|
273
237
|
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
|
274
238
|
}
|
|
275
239
|
|
|
276
|
-
|
|
240
|
+
.unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
|
|
277
241
|
.alarm-warning { color: #f1c40f !important; }
|
|
278
242
|
.alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
|
|
279
|
-
|
|
280
|
-
@keyframes blink-unstable { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
|
|
281
|
-
.unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
|
|
243
|
+
@keyframes blink-unstable { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
282
244
|
|
|
283
245
|
#wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
|
|
284
246
|
|
|
285
247
|
/* ==========================================================================
|
|
286
|
-
9. RESPONSIVE (PORTRAIT MODE
|
|
248
|
+
9. RESPONSIVE (PORTRAIT MODE)
|
|
287
249
|
========================================================================== */
|
|
288
250
|
@media (max-aspect-ratio: 0.9 / 1) {
|
|
289
|
-
.main-container {
|
|
290
|
-
grid-template-columns: 1fr 1fr !important;
|
|
291
|
-
grid-template-rows: 45vh calc(55vh - 8px) !important;
|
|
292
|
-
}
|
|
293
|
-
|
|
251
|
+
.main-container { grid-template-columns: 1fr 1fr !important; grid-template-rows: 45vh calc(55vh - 8px) !important; }
|
|
294
252
|
.center-panel { grid-row: 1 !important; grid-column: 1 / span 2 !important; padding: 5px 0; }
|
|
295
253
|
.left-panel { grid-row: 2 !important; grid-column: 1 !important; }
|
|
296
254
|
.right-panel { grid-row: 2 !important; grid-column: 2 !important; }
|
|
297
|
-
|
|
298
|
-
/* Focus Mode Verticale: Impilamento Flex sopra/sotto */
|
|
299
255
|
.main-container.focus-active { display: flex !important; flex-direction: column !important; }
|
|
300
|
-
.focus-active .center-panel { flex: 0 0 45vh !important;
|
|
301
|
-
.focus-active .side-panel.has-focus { flex: 0 0 calc(55vh - 8px) !important;
|
|
302
|
-
|
|
303
|
-
/* Altezze box Portrait dinamiche (per evitare overflow) */
|
|
256
|
+
.focus-active .center-panel { flex: 0 0 45vh !important; }
|
|
257
|
+
.focus-active .side-panel.has-focus { flex: 0 0 calc(55vh - 8px) !important; display: flex !important; }
|
|
304
258
|
.data-box { height: auto !important; flex: 1 !important; }
|
|
305
|
-
.data-box:nth-child(1), .data-box:nth-child(2) { flex: 1.5 !important; }
|
|
306
|
-
|
|
307
|
-
.value-large { font-size: clamp(1.2rem, 35cqh, 2.5rem); margin: auto 0; }
|
|
308
|
-
.value.dual-val { font-size: clamp(0.9rem, 30cqh, 1.4rem); }
|
|
309
|
-
|
|
310
|
-
/* Protezione bussola per box bassi (Portrait) */
|
|
311
|
-
.mini-compass {
|
|
312
|
-
width: min(70cqh, 40cqw) !important;
|
|
313
|
-
height: min(70cqh, 40cqw) !important;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
.right-panel .label-row .label:only-child { margin-left: auto !important; }
|
|
317
|
-
.box-hercules .unit::before, .box-hercules .unit::after { font-size: 6px !important; }
|
|
318
259
|
}
|
|
319
260
|
|
|
320
261
|
/* ==========================================================================
|
|
321
262
|
10. NIGHT MODE (RED ON BLACK - TACTICAL)
|
|
322
263
|
========================================================================== */
|
|
323
|
-
body.night-mode {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
.night-mode .
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
.night-mode
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
.night-mode .label,
|
|
339
|
-
.night-mode .unit,
|
|
340
|
-
.night-mode .dual-label {
|
|
341
|
-
color: #800000 !important; /* Rosso cupo per non affaticare la vista */
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/* --- Valori Numerici --- */
|
|
345
|
-
.night-mode .value,
|
|
346
|
-
.night-mode .value-large {
|
|
347
|
-
color: #ff3333 !important;
|
|
348
|
-
text-shadow: 0 0 8px rgba(255, 0, 0, 0.4); /* Bagliore per profondità */
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/* --- Grafici Sparkline (Solo Linea, No Riempimento) --- */
|
|
352
|
-
.night-mode .graph-wrapper {
|
|
353
|
-
background: rgba(30, 0, 0, 0.3) !important;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/* Nasconde l'area di riempimento sotto la linea */
|
|
357
|
-
.night-mode .sparkline path:first-of-type {
|
|
358
|
-
display: none !important;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/* Rende la linea del tracciato molto più visibile */
|
|
362
|
-
.night-mode .sparkline path {
|
|
363
|
-
fill: none !important;
|
|
364
|
-
stroke: #ff3333 !important;
|
|
365
|
-
stroke-width: 1.8px !important;
|
|
366
|
-
opacity: 1 !important;
|
|
367
|
-
filter: drop-shadow(0 0 2px rgba(255, 0, 0, 0.4));
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/* Griglie interne (Orizzontali e Verticali) uniformi */
|
|
371
|
-
.night-mode .sparkline line {
|
|
372
|
-
stroke: #4d0000 !important;
|
|
373
|
-
stroke-width: 0.7px !important;
|
|
374
|
-
opacity: 1 !important;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/* Forza i segmenti colorati del TWS alla stessa luminosità della linea */
|
|
378
|
-
.night-mode #tws-graph line:not([stroke*="rgba"]) {
|
|
379
|
-
stroke: #ff3333 !important;
|
|
380
|
-
stroke-width: 1.8px !important;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/* Numeri delle scale a lato dei grafici */
|
|
384
|
-
.night-mode .scale-labels {
|
|
385
|
-
color: #660000 !important;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/* --- Hercules Mode in Night Mode --- */
|
|
389
|
-
.night-mode .box-hercules {
|
|
390
|
-
background: rgba(60, 0, 0, 0.2) !important;
|
|
391
|
-
}
|
|
392
|
-
.night-mode .line-hercules {
|
|
393
|
-
filter: drop-shadow(0 0 6px #ff0000) !important;
|
|
394
|
-
}
|
|
264
|
+
body.night-mode { background-color: #000 !important; color: #ff0000 !important; }
|
|
265
|
+
.night-mode .side-panel { background: rgba(20, 0, 0, 0.4); border: 1px solid #330000; }
|
|
266
|
+
.night-mode .data-box { border-bottom-color: #260000; }
|
|
267
|
+
.night-mode .label, .night-mode .unit, .night-mode .dual-label { color: #800000 !important; }
|
|
268
|
+
.night-mode .value, .night-mode .value-large { color: #ff3333 !important; text-shadow: 0 0 8px rgba(255, 0, 0, 0.4); }
|
|
269
|
+
|
|
270
|
+
.night-mode .graph-wrapper { background: rgba(30, 0, 0, 0.3) !important; }
|
|
271
|
+
.night-mode .sparkline path:first-of-type { display: none !important; }
|
|
272
|
+
.night-mode .sparkline path { fill: none !important; stroke: #ff3333 !important; stroke-width: 1.8px !important; opacity: 1 !important; filter: drop-shadow(0 0 2px rgba(255, 0, 0, 0.4)); }
|
|
273
|
+
.night-mode .sparkline line { stroke: #4d0000 !important; stroke-width: 0.7px !important; }
|
|
274
|
+
.night-mode #tws-graph line:not([stroke*="rgba"]) { stroke: #ff3333 !important; stroke-width: 1.8px !important; }
|
|
275
|
+
.night-mode .scale-labels { color: #660000 !important; }
|
|
276
|
+
|
|
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; }
|
|
395
279
|
|
|
396
|
-
/* Scritta Hercules in alto (Colore personalizzato #f60000) */
|
|
397
|
-
.night-mode .box-hercules .unit::before,
|
|
398
|
-
.night-mode .box-hercules .unit::after,
|
|
399
|
-
.night-mode .box-hercules .label::before,
|
|
400
|
-
.night-mode .box-hercules .label::after {
|
|
401
|
-
color: #f60000 !important;
|
|
402
|
-
font-weight: 700;
|
|
403
|
-
opacity: 0.9;
|
|
404
|
-
letter-spacing: 1px;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/* --- Strumento Centrale (Wind Gauge) --- */
|
|
408
280
|
.night-mode #wind-gauge circle { stroke: #330000; }
|
|
409
281
|
.night-mode #ticks line { stroke: #4d0000 !important; }
|
|
410
282
|
.night-mode #tick-labels { fill: #800000 !important; }
|
|
411
283
|
.night-mode #boat-icon { fill: #330000 !important; opacity: 0.6; }
|
|
412
284
|
.night-mode #aws-val-svg { fill: #ff3333 !important; }
|
|
413
|
-
|
|
414
|
-
/* Settori Vento (Distinguibili per stile, non per colore) */
|
|
415
|
-
.night-mode #wind-gauge path[stroke="#ff0000"] { stroke: #660000 !important; opacity: 0.8; } /* Sx Solid */
|
|
416
|
-
.night-mode #wind-gauge path[stroke="#00ff00"] {
|
|
417
|
-
stroke: #660000 !important;
|
|
418
|
-
stroke-dasharray: 4, 3; /* Dx Dashed */
|
|
419
|
-
opacity: 0.8;
|
|
420
|
-
}
|
|
421
|
-
.night-mode #wind-gauge path[stroke="#ff8800"] { stroke: #330000 !important; stroke-width: 8; } /* Poppa Dark */
|
|
422
|
-
|
|
423
|
-
/* Lancette Wind Gauge */
|
|
424
285
|
.night-mode #awa-pointer path { fill: #ff0000; stroke: #000; }
|
|
425
286
|
.night-mode #twa-pointer path { fill: #800000; stroke: #000; }
|
|
426
287
|
.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; }
|
|
427
290
|
|
|
428
|
-
/*
|
|
429
|
-
.night-mode rect[fill="url(#leeway-grad)"] { fill: url(#leeway-night-grad) !important; }
|
|
430
|
-
.night-mode #leeway-val { fill: #ff3333 !important; }
|
|
431
|
-
.night-mode g[stroke="#555"] line { stroke: #4d0000 !important; }
|
|
432
|
-
.night-mode g[fill="#555"] text { fill: #660000 !important; }
|
|
433
|
-
.night-mode rect[fill="#222"] { fill: #0a0000 !important; stroke: #200000; }
|
|
434
|
-
|
|
435
|
-
/* --- Bussola TWD --- */
|
|
291
|
+
/* Bussola TWD Night */
|
|
436
292
|
.night-mode .mini-compass { border-color: #330000; background: #000; }
|
|
437
293
|
.night-mode .mini-compass text { fill: #800000 !important; }
|
|
438
|
-
.night-mode #twd-arrow
|
|
439
|
-
.night-mode #twd-boat-wrap path { fill: #
|
|
294
|
+
.night-mode #twd-arrow #twd-wind-chevron { stroke: #ff3333 !important; stroke-width: 3px !important; opacity: 1 !important; filter: drop-shadow(0 0 4px #ff0000); transform-origin: center; }
|
|
295
|
+
.night-mode #twd-boat-wrap path { fill: #ff3333 !important; opacity: 0.4 !important; }
|
|
440
296
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
.night-mode #aws-display-group text {
|
|
447
|
-
fill: #ff3333 !important;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/* --- Modifica: Miglioramento visibilità TWD in Night Mode --- */
|
|
451
|
-
.night-mode #twd-arrow #twd-wind-chevron {
|
|
452
|
-
stroke: #ff3333 !important; /* Rosso più brillante */
|
|
453
|
-
stroke-width: 3px !important; /* Più spesso */
|
|
454
|
-
opacity: 1 !important; /* Piena opacità */
|
|
455
|
-
filter: drop-shadow(0 0 4px #ff0000); /* Effetto neon per staccarsi dallo sfondo */
|
|
456
|
-
transform-origin: center; /* Forza il centro corretto */
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/* Assicuriamoci che anche la punta della barca sia visibile */
|
|
460
|
-
.night-mode #twd-boat-wrap path {
|
|
461
|
-
fill: #ff3333 !important;
|
|
462
|
-
opacity: 0.4 !important;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/* Definiamo il lampeggio */
|
|
466
|
-
@keyframes blink-trend {
|
|
467
|
-
0%, 100% { opacity: 1; }
|
|
468
|
-
50% { opacity: 0; }
|
|
469
|
-
}
|
|
297
|
+
/* ==========================================================================
|
|
298
|
+
11. TREND VENTO E ALLARME STRAMBATA (GYBE)
|
|
299
|
+
========================================================================== */
|
|
300
|
+
@keyframes blink-trend { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
470
301
|
|
|
471
|
-
/* Stile base dei pallini */
|
|
472
302
|
#trend-dot-cw, #trend-dot-ccw, #trend-gauge-cw, #trend-gauge-ccw {
|
|
473
303
|
opacity: 0.3;
|
|
474
304
|
transition: opacity 0.3s ease;
|
|
475
305
|
}
|
|
476
306
|
|
|
477
|
-
|
|
478
|
-
.is-trending {
|
|
479
|
-
animation: blink-trend 1s infinite !important;
|
|
480
|
-
}
|
|
307
|
+
.is-trending { animation: blink-trend 1s infinite !important; }
|
|
481
308
|
|
|
482
309
|
/* Allarme Strambata: Rosso fisso con bagliore neon */
|
|
483
310
|
.is-gybing {
|
|
484
311
|
opacity: 1 !important;
|
|
485
|
-
animation: none !important;
|
|
312
|
+
animation: none !important;
|
|
486
313
|
filter: drop-shadow(0 0 8px #ff0000) !important;
|
|
487
314
|
}
|