@sailingrotevista/rotevista-dash 1.0.24 → 1.0.25
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 +74 -158
- package/index.html +26 -74
- package/package.json +1 -1
- package/style.css +78 -127
package/app.js
CHANGED
|
@@ -1,51 +1,29 @@
|
|
|
1
1
|
// ==========================================================================
|
|
2
|
-
// 1. CONFIGURAZIONE E
|
|
2
|
+
// 1. CONFIGURAZIONE E STATO
|
|
3
3
|
// ==========================================================================
|
|
4
4
|
let CONFIG = {
|
|
5
|
-
alarms: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
},
|
|
9
|
-
averages: {
|
|
10
|
-
smoothWindow: 2000, // Media veloce per le lancette (ms)
|
|
11
|
-
longWindow: 60000, // Media lunga per i dati MEAN (ms)
|
|
12
|
-
stabilityTolerance: 2000, // Tolleranza riempimento buffer (ms)
|
|
13
|
-
stabilityThreshold: 0.90, // Indice R minimo per dati stabili
|
|
14
|
-
minSpeed: 0.5 // Velocità sotto la quale non lampeggia nulla (kts)
|
|
15
|
-
},
|
|
16
|
-
graphs: {
|
|
17
|
-
reef1: 15.0, // Soglia arancio TWS
|
|
18
|
-
reef2: 20.0, // Soglia rossa TWS
|
|
19
|
-
historyMinutes: 5, // Durata temporale dei grafici (minuti)
|
|
20
|
-
samples: 60 // Numero di punti disegnati nel grafico
|
|
21
|
-
},
|
|
5
|
+
alarms: { depthDanger: 2.5, depthWarning: 5.0 },
|
|
6
|
+
averages: { smoothWindow: 2000, longWindow: 60000, stabilityTolerance: 2000, stabilityThreshold: 0.90, minSpeed: 0.5 },
|
|
7
|
+
graphs: { reef1: 15.0, reef2: 20.0, historyMinutes: 5, samples: 60 },
|
|
22
8
|
scales: {
|
|
23
|
-
stw:
|
|
24
|
-
sog:
|
|
25
|
-
tws:
|
|
9
|
+
stw: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
10
|
+
sog: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
11
|
+
tws: { stdMax: 25, hercSpan: 10, step: 5 },
|
|
26
12
|
depth: { stdMax: 20, hercSpan: 10, step: 10 }
|
|
27
13
|
},
|
|
28
|
-
server: {
|
|
29
|
-
fallbackIp: "192.168.111.240:3000"
|
|
30
|
-
}
|
|
14
|
+
server: { fallbackIp: "192.168.111.240:3000" }
|
|
31
15
|
};
|
|
32
16
|
|
|
33
17
|
const RENDER_INTERVAL_MS = 1000;
|
|
34
18
|
const TIMEOUT_MS = 5000;
|
|
35
19
|
const SIM_SAMPLE_INTERVAL = 1000;
|
|
36
20
|
|
|
37
|
-
// ==========================================================================
|
|
38
|
-
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
39
|
-
// ==========================================================================
|
|
40
21
|
let simulationMode = false;
|
|
41
22
|
let socket, renderInterval, simInterval;
|
|
42
23
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
43
24
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
44
|
-
|
|
45
|
-
// Gestione Focus e Interazioni
|
|
46
25
|
let pressTimer, isFocusActive = false, blockNextClick = false;
|
|
47
26
|
|
|
48
|
-
// Modalità Scale (Standard/Hercules) salvate nel browser
|
|
49
27
|
const graphModes = {
|
|
50
28
|
stw: localStorage.getItem('mode_stw') || 'standard',
|
|
51
29
|
sog: localStorage.getItem('mode_sog') || 'standard',
|
|
@@ -53,14 +31,7 @@ const graphModes = {
|
|
|
53
31
|
depth: localStorage.getItem('mode_depth') || 'standard'
|
|
54
32
|
};
|
|
55
33
|
|
|
56
|
-
const store = {
|
|
57
|
-
raw: {},
|
|
58
|
-
timestamps: {},
|
|
59
|
-
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
60
|
-
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
61
|
-
histories: { stw: [], sog: [], depth: [], tws: [] },
|
|
62
|
-
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 }
|
|
63
|
-
};
|
|
34
|
+
const store = { raw: {}, timestamps: {}, smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] }, longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] }, histories: { stw: [], sog: [], depth: [], tws: [] }, lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 } };
|
|
64
35
|
|
|
65
36
|
const ui = {
|
|
66
37
|
stw: document.getElementById('stw'), sog: document.getElementById('sog'),
|
|
@@ -76,13 +47,13 @@ const ui = {
|
|
|
76
47
|
};
|
|
77
48
|
|
|
78
49
|
// ==========================================================================
|
|
79
|
-
// 3.
|
|
50
|
+
// 3. LOGICA DI COMUNICAZIONE E CALCOLO
|
|
80
51
|
// ==========================================================================
|
|
52
|
+
|
|
81
53
|
async function fetchServerConfig() {
|
|
82
54
|
if (!window.location.protocol.includes("http")) return;
|
|
83
55
|
const pluginID = 'rotevista-dash';
|
|
84
56
|
const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
|
|
85
|
-
|
|
86
57
|
for (let url of possibleUrls) {
|
|
87
58
|
try {
|
|
88
59
|
const response = await fetch(url);
|
|
@@ -90,46 +61,31 @@ async function fetchServerConfig() {
|
|
|
90
61
|
const data = await response.json();
|
|
91
62
|
const actual = data.configuration || data;
|
|
92
63
|
if (actual && typeof actual === 'object') {
|
|
93
|
-
const parseNumbers = (obj) => {
|
|
94
|
-
for (let k in obj) {
|
|
95
|
-
if (typeof obj[k] === 'object') parseNumbers(obj[k]);
|
|
96
|
-
else if (!isNaN(obj[k]) && obj[k] !== "") obj[k] = parseFloat(obj[k]);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
64
|
+
const parseNumbers = (obj) => { for (let k in obj) { if (typeof obj[k] === 'object') parseNumbers(obj[k]); else if (!isNaN(obj[k]) && obj[k] !== "") obj[k] = parseFloat(obj[k]); } };
|
|
99
65
|
parseNumbers(actual);
|
|
100
66
|
if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
|
|
101
67
|
if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
|
|
102
68
|
if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
|
|
103
|
-
if (actual.scales) {
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
console.log("Dashboard: Configurazione caricata dal server.");
|
|
107
|
-
return;
|
|
69
|
+
if (actual.scales) { for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; } }
|
|
70
|
+
console.log("Dashboard: Configurazione caricata."); return;
|
|
108
71
|
}
|
|
109
72
|
}
|
|
110
73
|
} catch (e) { }
|
|
111
74
|
}
|
|
112
75
|
}
|
|
113
76
|
|
|
114
|
-
// ==========================================================================
|
|
115
|
-
// 4. MATEMATICA E GESTIONE DATI
|
|
116
|
-
// ==========================================================================
|
|
117
77
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
118
78
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
119
79
|
function msToKts(ms) { return ms * 1.94384; }
|
|
120
80
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
121
81
|
function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
|
|
122
82
|
|
|
123
|
-
// Calcolo media circolare vettoriale con rilevamento stabilità
|
|
124
83
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
125
|
-
const now = Date.now();
|
|
126
|
-
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
84
|
+
const now = Date.now(); const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
127
85
|
if (validData.length === 0) return null;
|
|
128
|
-
let sSin = 0, sCos = 0;
|
|
129
|
-
validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
86
|
+
let sSin = 0, sCos = 0; validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
130
87
|
let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
|
|
131
|
-
let
|
|
132
|
-
let isStable = (timeSpan >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
88
|
+
let isStable = (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
133
89
|
let avgDeg = Math.round(radToDeg(Math.atan2(sSin, sCos)));
|
|
134
90
|
return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
|
|
135
91
|
}
|
|
@@ -140,23 +96,18 @@ function processIncomingData(path, val) {
|
|
|
140
96
|
if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
|
|
141
97
|
if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
|
|
142
98
|
if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
|
|
143
|
-
|
|
144
|
-
// Calcolo TWD (True Wind Direction)
|
|
145
99
|
if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
|
|
146
|
-
let twdRad = 0;
|
|
147
|
-
if (
|
|
148
|
-
else
|
|
149
|
-
|
|
150
|
-
if (twdRad < 0) twdRad += (2 * Math.PI);
|
|
151
|
-
} else return;
|
|
152
|
-
store.smoothBuf.twd.push({ val: twdRad, time: now });
|
|
153
|
-
store.longBuf.twd.push({ val: twdRad, time: now });
|
|
100
|
+
let twdRad = 0; if (path === "environment.wind.directionTrue") twdRad = val;
|
|
101
|
+
else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) { twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI); if (twdRad < 0) twdRad += (2 * Math.PI); }
|
|
102
|
+
else return;
|
|
103
|
+
store.smoothBuf.twd.push({ val: twdRad, time: now }); store.longBuf.twd.push({ val: twdRad, time: now });
|
|
154
104
|
}
|
|
155
105
|
}
|
|
156
106
|
|
|
157
107
|
// ==========================================================================
|
|
158
|
-
//
|
|
108
|
+
// 4. RENDERING LOOP E GRAFICA
|
|
159
109
|
// ==========================================================================
|
|
110
|
+
|
|
160
111
|
function startDisplayLoop() {
|
|
161
112
|
renderInterval = setInterval(() => {
|
|
162
113
|
const now = Date.now();
|
|
@@ -174,7 +125,6 @@ function startDisplayLoop() {
|
|
|
174
125
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
175
126
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
176
127
|
|
|
177
|
-
// Calcolo Drift Reale (COG-HDG)
|
|
178
128
|
if (store.raw["navigation.courseOverGroundTrue"] && store.raw["navigation.headingTrue"]) {
|
|
179
129
|
let drift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
|
|
180
130
|
if (curSog < CONFIG.averages.minSpeed) drift = 0;
|
|
@@ -188,23 +138,12 @@ function startDisplayLoop() {
|
|
|
188
138
|
awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true),
|
|
189
139
|
twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true),
|
|
190
140
|
twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
|
|
191
|
-
|
|
192
|
-
const upUI = (el, obj) => {
|
|
193
|
-
if (!obj) { el.innerHTML = "---°"; el.classList.remove('unstable-data'); }
|
|
194
|
-
else {
|
|
195
|
-
el.innerHTML = `${obj.val.toString().padStart(3, '0')}°`;
|
|
196
|
-
if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data'); else el.classList.add('unstable-data');
|
|
197
|
-
}
|
|
198
|
-
};
|
|
141
|
+
const upUI = (el, obj) => { if (!obj) { el.innerHTML = "---°"; el.classList.remove('unstable-data'); } else { el.innerHTML = `${obj.val.toString().padStart(3, '0')}°`; if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data'); else el.classList.add('unstable-data'); } };
|
|
199
142
|
upUI(ui.hdg, hObj); upUI(ui.cog, cObj); upUI(ui.awaAvg, awObj); upUI(ui.twaAvg, twObj); upUI(ui.twdAvg, twdObj);
|
|
200
|
-
|
|
201
143
|
if (hObj && twObj && hObj.val !== null) {
|
|
202
|
-
let tA = twObj.val * 2;
|
|
203
|
-
ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
144
|
+
let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
204
145
|
if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
205
|
-
|
|
206
|
-
if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
|
|
207
|
-
else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
|
|
146
|
+
if (hObj.stable && twObj.stable || curSog < CONFIG.averages.minSpeed) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); } else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
|
|
208
147
|
}
|
|
209
148
|
if (twdObj) { curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); }
|
|
210
149
|
lastAvgUIUpdate = now;
|
|
@@ -214,23 +153,6 @@ function startDisplayLoop() {
|
|
|
214
153
|
}, RENDER_INTERVAL_MS);
|
|
215
154
|
}
|
|
216
155
|
|
|
217
|
-
// ==========================================================================
|
|
218
|
-
// 6. CONNESSIONE SIGNALK
|
|
219
|
-
// ==========================================================================
|
|
220
|
-
function connect() {
|
|
221
|
-
if (simulationMode) return;
|
|
222
|
-
let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
|
|
223
|
-
try {
|
|
224
|
-
socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
|
|
225
|
-
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; };
|
|
226
|
-
socket.onmessage = (e) => { const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
|
|
227
|
-
socket.onclose = () => !simulationMode && setTimeout(connect, 5000);
|
|
228
|
-
} catch (e) { setTimeout(connect, 5000); }
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ==========================================================================
|
|
232
|
-
// 7. FUNZIONI GRAFICHE E SCALATURA
|
|
233
|
-
// ==========================================================================
|
|
234
156
|
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)}°`; }
|
|
235
157
|
|
|
236
158
|
function manageHistory(t, v) {
|
|
@@ -248,11 +170,8 @@ function calculateScale(type, data, mode) {
|
|
|
248
170
|
const s = CONFIG.scales[type] || { stdMax: 12, hercSpan: 4, step: 2 };
|
|
249
171
|
let aMin = Math.min(...data), aMax = Math.max(...data);
|
|
250
172
|
if (mode === 'hercules') {
|
|
251
|
-
let avg = (aMin + aMax) / 2;
|
|
252
|
-
let
|
|
253
|
-
if (span % 2 !== 0) span += 1;
|
|
254
|
-
let min = Math.max(0, Math.floor(avg - (span / 2)));
|
|
255
|
-
return { min, max: min + span };
|
|
173
|
+
let avg = (aMin + aMax) / 2; let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin)); if (span % 2 !== 0) span += 1;
|
|
174
|
+
let min = Math.max(0, Math.floor(avg - (span / 2))); return { min, max: min + span };
|
|
256
175
|
} else return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
|
|
257
176
|
}
|
|
258
177
|
|
|
@@ -262,86 +181,83 @@ function updateScaleLabels(t, min, max) {
|
|
|
262
181
|
}
|
|
263
182
|
|
|
264
183
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
265
|
-
const svg = document.getElementById(id);
|
|
184
|
+
const svg = document.getElementById(id);
|
|
185
|
+
if (!svg || d.length < 2) return;
|
|
186
|
+
|
|
266
187
|
const w = 200, h = 40, range = max - min || 1;
|
|
267
|
-
|
|
268
|
-
|
|
188
|
+
const lClass = isHercules ? 'line-hercules' : '';
|
|
189
|
+
|
|
190
|
+
// 1. Griglie
|
|
191
|
+
let grids = "";
|
|
192
|
+
[0.25, 0.5, 0.75].forEach(p => { grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />`; });
|
|
193
|
+
for (let m = 1; m < CONFIG.graphs.historyMinutes; m++) {
|
|
194
|
+
const x = w - (m / CONFIG.graphs.historyMinutes) * w;
|
|
195
|
+
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(255,255,255,0.05)" stroke-width="0.5" />`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. Calcolo Percorsi
|
|
269
199
|
let pD = "", cS = "";
|
|
270
200
|
d.forEach((v, i) => {
|
|
271
|
-
const x = (i/(CONFIG.graphs.samples-1))*w, y = h-(Math.max(0,Math.min(1,(v-min)/range))*h);
|
|
201
|
+
const x = (i/(CONFIG.graphs.samples-1))*w, y = h-(Math.max(0,Math.min(1,(v-min)/range))*h);
|
|
202
|
+
pD += `${i===0?'M':'L'} ${x} ${y} `;
|
|
203
|
+
|
|
204
|
+
// Logica segmenti colorati per TWS
|
|
272
205
|
if (isTws && i > 0) {
|
|
273
206
|
const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
|
|
274
|
-
let c = "#f1c40f";
|
|
275
|
-
|
|
207
|
+
let c = "#f1c40f";
|
|
208
|
+
if (v >= CONFIG.graphs.reef2) c = "#e74c3c";
|
|
209
|
+
else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
|
|
210
|
+
// Lo spessore ora è rimosso da qui e gestito dal CSS tramite la classe
|
|
211
|
+
cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${lClass}" />`;
|
|
276
212
|
}
|
|
277
213
|
});
|
|
278
|
-
|
|
279
|
-
|
|
214
|
+
|
|
215
|
+
// 3. Rendering finale
|
|
216
|
+
const aP = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`;
|
|
217
|
+
const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#f1c40f' };
|
|
218
|
+
|
|
219
|
+
if (isTws) {
|
|
220
|
+
svg.innerHTML = `${grids}<path d="${aP}" fill="rgba(241,196,15,0.12)" stroke="none" />${cS}`;
|
|
221
|
+
} else {
|
|
222
|
+
svg.innerHTML = `${grids}<path d="${aP}" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${lClass}" fill="none" stroke="${clrs[id]}" />`;
|
|
223
|
+
}
|
|
280
224
|
}
|
|
281
225
|
|
|
282
226
|
// ==========================================================================
|
|
283
|
-
//
|
|
227
|
+
// 5. EVENTI E INTERAZIONI
|
|
284
228
|
// ==========================================================================
|
|
285
229
|
|
|
286
230
|
function toggleFocusMode(type, element) {
|
|
287
231
|
const container = document.querySelector('.main-container');
|
|
288
232
|
const parentPanel = element.closest('.side-panel');
|
|
289
233
|
const isLeft = parentPanel.classList.contains('left-panel');
|
|
290
|
-
|
|
291
234
|
isFocusActive = !isFocusActive;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
container.classList.add('focus-active');
|
|
295
|
-
container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right');
|
|
296
|
-
parentPanel.classList.add('has-focus');
|
|
297
|
-
element.classList.add('is-focused');
|
|
298
|
-
blockNextClick = true; // Impedisce al click di rilascio del tocco lungo di chiudere subito
|
|
299
|
-
} else {
|
|
300
|
-
container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
|
|
301
|
-
document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus'));
|
|
302
|
-
document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
|
|
303
|
-
}
|
|
235
|
+
if (isFocusActive) { container.classList.add('focus-active'); container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right'); parentPanel.classList.add('has-focus'); element.classList.add('is-focused'); blockNextClick = true; }
|
|
236
|
+
else { container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right'); document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus')); document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused')); }
|
|
304
237
|
}
|
|
305
238
|
|
|
306
239
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
307
240
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
el.addEventListener('dblclick', (e) => {
|
|
311
|
-
if (isFocusActive) return;
|
|
312
|
-
e.preventDefault();
|
|
313
|
-
graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
|
|
314
|
-
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
315
|
-
el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Gestione Pressione Prolungata (1s) -> Tactical Focus
|
|
319
|
-
const startPress = () => { if (!isFocusActive) pressTimer = setTimeout(() => toggleFocusMode(type, el), 1000); };
|
|
241
|
+
el.addEventListener('dblclick', (e) => { if (isFocusActive) return; e.preventDefault(); graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200); });
|
|
242
|
+
const startPress = (e) => { if (!isFocusActive) pressTimer = setTimeout(() => { toggleFocusMode(type, el); if (e.cancelable) e.preventDefault(); }, 1000); };
|
|
320
243
|
const cancelPress = () => { clearTimeout(pressTimer); };
|
|
321
|
-
el.addEventListener('mousedown', startPress); el.addEventListener('touchstart', startPress, {passive:
|
|
244
|
+
el.addEventListener('mousedown', startPress); el.addEventListener('touchstart', startPress, {passive: false});
|
|
322
245
|
['mouseup', 'mouseleave', 'touchend'].forEach(evt => el.addEventListener(evt, cancelPress));
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
el.addEventListener('click', (e) => {
|
|
326
|
-
if (blockNextClick) { blockNextClick = false; return; }
|
|
327
|
-
if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el);
|
|
328
|
-
});
|
|
246
|
+
el.addEventListener('click', (e) => { if (blockNextClick) { blockNextClick = false; return; } if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); });
|
|
247
|
+
el.addEventListener('contextmenu', (e) => { if (isFocusActive) e.preventDefault(); });
|
|
329
248
|
});
|
|
330
249
|
|
|
331
|
-
// Fullscreen (Hotspot)
|
|
332
250
|
if (ui.hotspot) { ui.hotspot.addEventListener('click', () => { const doc = document.documentElement, isF = document.fullscreenElement || document.webkitFullscreenElement; if (!isF) { if (doc.requestFullscreen) doc.requestFullscreen(); else if (doc.webkitRequestFullscreen) doc.webkitRequestFullscreen(); } else { if (document.exitFullscreen) document.exitFullscreen(); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); } }); }
|
|
333
251
|
|
|
334
|
-
// Allarmi Audio e Depth
|
|
335
|
-
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'); }
|
|
336
|
-
function playBingBing() { if (!audioCtx) return; 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); }
|
|
337
|
-
|
|
338
252
|
// ==========================================================================
|
|
339
|
-
//
|
|
253
|
+
// 6. INIZIALIZZAZIONE
|
|
340
254
|
// ==========================================================================
|
|
341
|
-
(function
|
|
342
|
-
|
|
255
|
+
(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); } } })();
|
|
256
|
+
function connect() { if (simulationMode) return; let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp; try { socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`); socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; }; socket.onmessage = (e) => { const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); }; socket.onclose = () => !simulationMode && setTimeout(connect, 5000); } catch (e) { setTimeout(connect, 5000); } }
|
|
343
257
|
async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
|
|
344
258
|
window.addEventListener('load', init);
|
|
259
|
+
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'); }
|
|
260
|
+
function playBingBing() { if (!audioCtx) return; 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); }
|
|
345
261
|
|
|
346
262
|
// Simulatore (Triple click su Depth)
|
|
347
263
|
ui.depth.closest('.data-box').addEventListener('click', (function() {
|
package/index.html
CHANGED
|
@@ -16,44 +16,41 @@
|
|
|
16
16
|
|
|
17
17
|
<div class="main-container">
|
|
18
18
|
|
|
19
|
-
<!--
|
|
20
|
-
<!-- COLONNA SINISTRA: Dati Rotta e Velocità -->
|
|
21
|
-
<!-- ======================================================= -->
|
|
19
|
+
<!-- COLONNA SINISTRA: Dati Rotta e Velocità -->
|
|
22
20
|
<div class="side-panel left-panel">
|
|
23
|
-
|
|
24
|
-
<!-- STW: Velocità attraverso l'acqua -->
|
|
21
|
+
<!-- STW con Sparkline -->
|
|
25
22
|
<div class="data-box">
|
|
26
23
|
<div class="label-row"><span class="label">STW</span><span class="unit">kts</span></div>
|
|
27
24
|
<span class="value" id="stw">0.0</span>
|
|
28
25
|
<div class="graph-wrapper">
|
|
29
26
|
<svg class="sparkline" id="stw-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
|
|
30
|
-
<div class="scale-labels
|
|
27
|
+
<div class="scale-labels" id="stw-scale"></div>
|
|
31
28
|
</div>
|
|
32
29
|
</div>
|
|
33
30
|
|
|
34
|
-
<!-- SOG
|
|
31
|
+
<!-- SOG con Sparkline -->
|
|
35
32
|
<div class="data-box">
|
|
36
33
|
<div class="label-row"><span class="label">SOG</span><span class="unit">kts</span></div>
|
|
37
34
|
<span class="value" id="sog">0.0</span>
|
|
38
35
|
<div class="graph-wrapper">
|
|
39
36
|
<svg class="sparkline" id="sog-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
|
|
40
|
-
<div class="scale-labels
|
|
37
|
+
<div class="scale-labels" id="sog-scale"></div>
|
|
41
38
|
</div>
|
|
42
39
|
</div>
|
|
43
40
|
|
|
44
|
-
<!-- HEADING
|
|
41
|
+
<!-- HEADING MEAN -->
|
|
45
42
|
<div class="data-box">
|
|
46
43
|
<div class="label-row"><span class="label">HEADING MEAN</span></div>
|
|
47
44
|
<span class="value value-large" id="hdg">000°</span>
|
|
48
45
|
</div>
|
|
49
46
|
|
|
50
|
-
<!-- COG
|
|
47
|
+
<!-- COG MEAN -->
|
|
51
48
|
<div class="data-box">
|
|
52
49
|
<div class="label-row"><span class="label">COG MEAN</span></div>
|
|
53
50
|
<span class="value value-large" id="cog">000°</span>
|
|
54
51
|
</div>
|
|
55
52
|
|
|
56
|
-
<!-- TACK:
|
|
53
|
+
<!-- TACK: Previsione mure opposte -->
|
|
57
54
|
<div class="data-box">
|
|
58
55
|
<div class="label-row"><span class="label">TACK</span></div>
|
|
59
56
|
<div class="dual-value-container">
|
|
@@ -69,152 +66,107 @@
|
|
|
69
66
|
</div>
|
|
70
67
|
</div>
|
|
71
68
|
|
|
72
|
-
<!--
|
|
73
|
-
<!-- CENTRO: Strumento Vento SVG (Ingrandito e Ottimizzato) -->
|
|
74
|
-
<!-- ======================================================= -->
|
|
69
|
+
<!-- CENTRO: Strumento Vento SVG -->
|
|
75
70
|
<div class="center-panel">
|
|
76
|
-
<!-- ViewBox ottimizzato (35 38 330 375) per ingrandire il diametro del quadrante -->
|
|
77
71
|
<svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
|
|
78
72
|
<defs>
|
|
79
|
-
<!-- Gradienti e Maschere -->
|
|
80
73
|
<linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" /><stop offset="100%" style="stop-color:#888888;stop-opacity:1" /></linearGradient>
|
|
81
74
|
<linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" /><stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" /><stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" /><stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" /><stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" /></linearGradient>
|
|
82
75
|
<clipPath id="leeway-clip"><rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" /></clipPath>
|
|
83
|
-
|
|
84
|
-
<!-- Filtro Glow per l'area di interazione centrale -->
|
|
85
|
-
<filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
86
|
-
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
87
|
-
</filter>
|
|
76
|
+
<filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" /></filter>
|
|
88
77
|
</defs>
|
|
89
78
|
|
|
90
|
-
|
|
91
|
-
<circle cx="200" cy="200" r="160" fill="#050505" />
|
|
92
|
-
<circle cx="200" cy="200" r="125" fill="#121212" />
|
|
93
|
-
|
|
94
|
-
<!-- Settori Vento (Rosso/Verde/Arancio) -->
|
|
79
|
+
<circle cx="200" cy="200" r="160" fill="#050505" /><circle cx="200" cy="200" r="125" fill="#121212" />
|
|
95
80
|
<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"/>
|
|
96
81
|
<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"/>
|
|
97
82
|
<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"/>
|
|
98
83
|
|
|
99
|
-
<!-- Tacche (Generate da Javascript) e Etichette Gradi -->
|
|
100
84
|
<g id="ticks"></g>
|
|
101
85
|
<g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="hanging" font-family="Arial" font-weight="bold">
|
|
102
|
-
<text font-size="16" transform="translate(200, 65)">0</text>
|
|
103
|
-
<text font-size="16" transform="translate(335, 200) rotate(90)">90</text>
|
|
104
|
-
<text font-size="16" transform="translate(65, 200) rotate(-90)">90</text>
|
|
105
|
-
<text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
|
|
86
|
+
<text font-size="16" transform="translate(200, 65)">0</text><text font-size="16" transform="translate(335, 200) rotate(90)">90</text><text font-size="16" transform="translate(65, 200) rotate(-90)">90</text><text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
|
|
106
87
|
<text font-size="11" transform="translate(267.5, 83) rotate(30)">30</text><text font-size="11" transform="translate(317, 132.5) rotate(60)">60</text>
|
|
107
88
|
<text font-size="11" transform="translate(317, 267.5) rotate(120)">120</text><text font-size="11" transform="translate(267.5, 317) rotate(150)">150</text>
|
|
108
89
|
<text font-size="11" transform="translate(132.5, 83) rotate(-30)">30</text><text font-size="11" transform="translate(83, 132.5) rotate(-60)">60</text>
|
|
109
90
|
<text font-size="11" transform="translate(83, 267.5) rotate(-120)">120</text><text font-size="11" transform="translate(132.5, 317) rotate(-150)">150</text>
|
|
110
91
|
</g>
|
|
111
92
|
|
|
112
|
-
<!-- Puntatore Track (Blu) -->
|
|
113
93
|
<g id="track-pointer" transform="rotate(0, 200, 200)"><path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" /></g>
|
|
114
|
-
|
|
115
|
-
<!-- Pulsante Fullscreen Hotspot (Disco cliccabile al centro) -->
|
|
116
94
|
<circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
|
|
117
|
-
|
|
118
|
-
<!-- Icona Barca (Centrata visivamente con traslazione Y+5) -->
|
|
119
95
|
<path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z" fill="url(#axiom-grad)" transform="translate(0, 5)" style="pointer-events: none;" />
|
|
120
96
|
|
|
121
|
-
<!-- Display Velocità Vento Apparente (Al centro sotto la barca) -->
|
|
122
97
|
<g id="aws-display-group" transform="translate(200, 265)">
|
|
123
98
|
<text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
|
|
124
99
|
<text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
|
|
125
100
|
</g>
|
|
126
|
-
|
|
127
|
-
<!-- Lancette AWA (A) e TWA (T) -->
|
|
128
101
|
<g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.85"><path d="M200,80 L211,95 L200,145 L189,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" /><text x="200" y="102" fill="#000" font-size="11" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text></g>
|
|
129
102
|
<g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.85"><path d="M200,90 L206,98 L200,125 L194,98 Z" fill="#ffff00" stroke="#000" stroke-width="0.8" /><text x="200" y="104" fill="#000" font-size="8" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text></g>
|
|
130
103
|
|
|
131
|
-
<!-- Barra LEEWAY / DRIFT (Posizionata più in alto a Y=372) -->
|
|
132
104
|
<g transform="translate(75, 395)">
|
|
133
105
|
<text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0°</text>
|
|
134
|
-
<rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
|
|
135
|
-
<rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
|
|
106
|
+
<rect x="0" y="0" width="250" height="12" fill="#222" rx="3" /><rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
|
|
136
107
|
<g stroke="#555" stroke-width="1">
|
|
137
|
-
<line x1="0" y1="-2" x2="0" y2="14" /><line x1="31.25" y1="2" x2="31.25" y2="10" /><line x1="62.5" y1="2" x2="62.5" y2="10" />
|
|
138
|
-
<line x1="93.75" y1="3" x2="93.75" y2="9" /><line x1="125" y1="-2" x2="125" y2="14" /><line x1="156.25" y1="3" x2="156.25" y2="9" />
|
|
139
|
-
<line x1="187.5" y1="2" x2="187.5" y2="10" /><line x1="218.75" y1="2" x2="218.75" y2="10" /><line x1="250" y1="-2" x2="250" y2="14" />
|
|
140
|
-
</g>
|
|
141
|
-
<g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
|
|
142
|
-
<text x="0" y="24">-20°</text><text x="62.5" y="24">-10</text><text x="125" y="24">0°</text><text x="187.5" y="24">10</text><text x="250" y="24">20°</text>
|
|
108
|
+
<line x1="0" y1="-2" x2="0" y2="14" /><line x1="31.25" y1="2" x2="31.25" y2="10" /><line x1="62.5" y1="2" x2="62.5" y2="10" /><line x1="93.75" y1="3" x2="93.75" y2="9" /><line x1="125" y1="-2" x2="125" y2="14" /><line x1="156.25" y1="3" x2="156.25" y2="9" /><line x1="187.5" y1="2" x2="187.5" y2="10" /><line x1="218.75" y1="2" x2="218.75" y2="10" /><line x1="250" y1="-2" x2="250" y2="14" />
|
|
143
109
|
</g>
|
|
110
|
+
<g fill="#555" font-size="8" text-anchor="middle" font-weight="bold"><text x="0" y="24">-20°</text><text x="62.5" y="24">-10</text><text x="125" y="24">0°</text><text x="187.5" y="24">10</text><text x="250" y="24">20°</text></g>
|
|
144
111
|
</g>
|
|
145
112
|
</svg>
|
|
146
113
|
</div>
|
|
147
114
|
|
|
148
|
-
<!--
|
|
149
|
-
<!-- COLONNA DESTRA: Profondità e Vento Reale -->
|
|
150
|
-
<!-- ======================================================= -->
|
|
115
|
+
<!-- COLONNA DESTRA: Dati Vento e Profondità -->
|
|
151
116
|
<div class="side-panel right-panel">
|
|
152
|
-
|
|
153
|
-
<!-- DEPTH: Profondità sotto il trasduttore -->
|
|
117
|
+
<!-- DEPTH -->
|
|
154
118
|
<div class="data-box">
|
|
155
119
|
<div class="label-row"><span class="unit">m</span><span class="label">DEPTH</span></div>
|
|
156
120
|
<span class="value" id="depth">--.-</span>
|
|
157
121
|
<div class="graph-wrapper">
|
|
158
|
-
<div class="scale-labels
|
|
122
|
+
<div class="scale-labels" id="depth-scale"></div>
|
|
159
123
|
<svg class="sparkline" id="depth-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
|
|
160
124
|
</div>
|
|
161
125
|
</div>
|
|
162
126
|
|
|
163
|
-
<!-- TWS
|
|
127
|
+
<!-- TWS -->
|
|
164
128
|
<div class="data-box">
|
|
165
129
|
<div class="label-row"><span class="unit">kts</span><span class="label">TWS</span></div>
|
|
166
130
|
<span class="value" id="tws">0.0</span>
|
|
167
131
|
<div class="graph-wrapper">
|
|
168
|
-
<div class="scale-labels
|
|
132
|
+
<div class="scale-labels" id="tws-scale"></div>
|
|
169
133
|
<svg class="sparkline" id="tws-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
|
|
170
134
|
</div>
|
|
171
135
|
</div>
|
|
172
136
|
|
|
173
|
-
<!-- TWA
|
|
137
|
+
<!-- TWA MEAN -->
|
|
174
138
|
<div class="data-box">
|
|
175
139
|
<div class="label-row"><span class="label">TWA MEAN</span></div>
|
|
176
140
|
<span class="value value-large" id="twa-avg">---°</span>
|
|
177
141
|
</div>
|
|
178
142
|
|
|
179
|
-
<!-- AWA
|
|
143
|
+
<!-- AWA MEAN -->
|
|
180
144
|
<div class="data-box">
|
|
181
145
|
<div class="label-row"><span class="label">AWA MEAN</span></div>
|
|
182
146
|
<span class="value value-large" id="awa-avg">---°</span>
|
|
183
147
|
</div>
|
|
184
148
|
|
|
185
|
-
<!-- TWD
|
|
149
|
+
<!-- TWD MEAN con Bussola -->
|
|
186
150
|
<div class="data-box">
|
|
187
151
|
<div class="label-row"><span class="label">TWD MEAN</span></div>
|
|
188
152
|
<div class="value-with-compass">
|
|
189
153
|
<svg class="mini-compass" viewBox="0 0 40 40">
|
|
190
|
-
<!-- Sfondo e bordo bussola -->
|
|
191
154
|
<circle cx="20" cy="20" r="19" fill="#151515" stroke="#444" stroke-width="1.5"/>
|
|
192
|
-
|
|
193
|
-
<!-- Etichetta Nord (N) -->
|
|
194
|
-
<text x="20" y="8" fill="#e74c3c" font-size="6" text-anchor="middle" font-weight="900" font-family="Arial">N</text>
|
|
195
|
-
|
|
196
|
-
<!-- Tacche cardinali (E, S, W) -->
|
|
155
|
+
<text x="20" y="8" fill="#e74c3c" font-size="6" text-anchor="middle" font-weight="900">N</text>
|
|
197
156
|
<g stroke="#555" stroke-width="0.8">
|
|
198
|
-
<line x1="20" y1="11" x2="20" y2="13"
|
|
199
|
-
<line x1="30" y1="20" x2="27" y2="20"/> <!-- Est -->
|
|
200
|
-
<line x1="20" y1="30" x2="20" y2="27"/> <!-- Sud -->
|
|
201
|
-
<line x1="10" y1="20" x2="13" y2="20"/> <!-- Ovest -->
|
|
157
|
+
<line x1="20" y1="11" x2="20" y2="13"/><line x1="30" y1="20" x2="27" y2="20"/><line x1="20" y1="30" x2="20" y2="27"/><line x1="10" y1="20" x2="13" y2="20"/>
|
|
202
158
|
</g>
|
|
203
|
-
|
|
204
|
-
<!-- Freccia TWD (Gialla, forma a punta di freccia aeronautica) -->
|
|
205
159
|
<g id="twd-arrow" transform="rotate(0, 20, 20)">
|
|
206
160
|
<path d="M20,10 L16,26 L20,23 L24,26 Z" fill="#ffff00" stroke="#000" stroke-width="0.5"/>
|
|
207
161
|
</g>
|
|
208
|
-
|
|
209
|
-
<!-- Perno centrale -->
|
|
210
162
|
<circle cx="20" cy="20" r="1.5" fill="#555" />
|
|
211
163
|
</svg>
|
|
212
164
|
<span class="value value-large" id="twd-avg">---°</span>
|
|
213
165
|
</div>
|
|
214
166
|
</div>
|
|
215
167
|
</div>
|
|
168
|
+
|
|
216
169
|
</div>
|
|
217
|
-
|
|
218
170
|
<script src="app.js"></script>
|
|
219
171
|
</body>
|
|
220
172
|
</html>
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -10,6 +10,9 @@ body {
|
|
|
10
10
|
height: 100vh;
|
|
11
11
|
width: 100vw;
|
|
12
12
|
overflow: hidden;
|
|
13
|
+
/* Impedisce selezioni accidentali su tutto il corpo dell'app */
|
|
14
|
+
-webkit-user-select: none;
|
|
15
|
+
user-select: none;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
.main-container {
|
|
@@ -19,16 +22,16 @@ body {
|
|
|
19
22
|
padding: 5px;
|
|
20
23
|
box-sizing: border-box;
|
|
21
24
|
gap: 8px;
|
|
22
|
-
/*
|
|
23
|
-
grid-template-columns:
|
|
25
|
+
/* Desktop Default: 3 colonne */
|
|
26
|
+
grid-template-columns: 1.5fr 3fr 1.5fr;
|
|
24
27
|
grid-template-rows: 100%;
|
|
25
28
|
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
/* Ottimizzazione per schermi molto larghi (
|
|
29
|
-
@media (min-aspect-ratio: 1.
|
|
31
|
+
/* Ottimizzazione per schermi molto larghi (16:10, 16:9) */
|
|
32
|
+
@media (min-aspect-ratio: 1.5) {
|
|
30
33
|
.main-container {
|
|
31
|
-
grid-template-columns:
|
|
34
|
+
grid-template-columns: 2fr 3fr 2fr;
|
|
32
35
|
gap: 15px;
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -39,10 +42,11 @@ body {
|
|
|
39
42
|
.side-panel {
|
|
40
43
|
display: flex;
|
|
41
44
|
flex-direction: column;
|
|
42
|
-
background: rgba(255, 255, 255, 0.
|
|
43
|
-
border-radius:
|
|
45
|
+
background: rgba(255, 255, 255, 0.03);
|
|
46
|
+
border-radius: 12px;
|
|
44
47
|
z-index: 10;
|
|
45
48
|
height: 100%;
|
|
49
|
+
overflow: hidden;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
.left-panel { grid-column: 1; }
|
|
@@ -61,16 +65,17 @@ body {
|
|
|
61
65
|
.data-box {
|
|
62
66
|
position: relative;
|
|
63
67
|
border-bottom: 1px solid #222;
|
|
64
|
-
padding: 8px
|
|
68
|
+
padding: 8px 12px;
|
|
65
69
|
display: flex;
|
|
66
70
|
flex-direction: column;
|
|
67
71
|
width: 100%;
|
|
68
72
|
box-sizing: border-box;
|
|
69
|
-
container-type: size;
|
|
73
|
+
container-type: size;
|
|
70
74
|
flex: 1;
|
|
75
|
+
overflow: hidden; /* Evita sconfinamenti su iPhone */
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
/*
|
|
78
|
+
/* Altezze Proporzionali Desktop */
|
|
74
79
|
.data-box:nth-child(1), .data-box:nth-child(2) { height: 25vh; }
|
|
75
80
|
.data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 16.666vh; }
|
|
76
81
|
|
|
@@ -80,142 +85,73 @@ body {
|
|
|
80
85
|
.focus-active { grid-template-columns: 1fr 1fr !important; }
|
|
81
86
|
.focus-active .side-panel:not(.has-focus) { display: none !important; }
|
|
82
87
|
|
|
88
|
+
/* Simmetria: se il focus è a sx, il vento va a dx e viceversa */
|
|
83
89
|
.focus-active.focus-side-left .side-panel.has-focus { grid-column: 1; }
|
|
84
90
|
.focus-active.focus-side-left .center-panel { grid-column: 2; }
|
|
85
91
|
.focus-active.focus-side-right .center-panel { grid-column: 1; }
|
|
86
92
|
.focus-active.focus-side-right .side-panel.has-focus { grid-column: 2; }
|
|
87
93
|
|
|
94
|
+
/* Box Focalizzato a tutto schermo */
|
|
88
95
|
.focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
|
|
89
|
-
.focus-active .has-focus .data-box.is-focused {
|
|
96
|
+
.focus-active .has-focus .data-box.is-focused {
|
|
97
|
+
height: 100vh !important;
|
|
98
|
+
border: none;
|
|
99
|
+
background: rgba(255, 255, 255, 0.05);
|
|
100
|
+
}
|
|
90
101
|
|
|
91
102
|
.focus-active .is-focused .value { font-size: clamp(3rem, 18cqh, 8rem) !important; margin-top: 10px; }
|
|
92
|
-
.focus-active .is-focused .scale-labels { font-size: 22px !important; min-width:
|
|
93
|
-
|
|
94
|
-
/* Bussola Esplosa in Focus Mode */
|
|
95
|
-
.focus-active .is-focused .mini-compass {
|
|
96
|
-
width: 45vh !important;
|
|
97
|
-
height: 45vh !important;
|
|
98
|
-
}
|
|
103
|
+
.focus-active .is-focused .scale-labels { font-size: 22px !important; min-width: 40px !important; }
|
|
99
104
|
|
|
105
|
+
/* Grafica Linee Sottili in Focus */
|
|
100
106
|
.focus-active .is-focused .sparkline path { stroke-width: 0.8px !important; }
|
|
101
|
-
.focus-active .is-focused .line-hercules { stroke-width: 1.2px !important; filter: drop-shadow(0 0 3px #ff0000); }
|
|
102
107
|
|
|
103
108
|
/* ==========================================================================
|
|
104
109
|
4. TIPOGRAFIA DINAMICA (ELASTICA CQH)
|
|
105
110
|
========================================================================== */
|
|
106
111
|
.label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
|
|
107
|
-
.label { color: #666; font-size: 0.
|
|
108
|
-
.unit { color: #888; font-size: 0.
|
|
112
|
+
.label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
|
|
113
|
+
.unit { color: #888; font-size: 0.6rem; font-weight: bold; }
|
|
109
114
|
|
|
110
|
-
.value {
|
|
111
|
-
color: #fff;
|
|
112
|
-
font-size: clamp(1.2rem, 22cqh, 3rem);
|
|
113
|
-
font-weight: 600;
|
|
114
|
-
line-height: 0.9;
|
|
115
|
-
letter-spacing: -1px;
|
|
116
|
-
transition: color 0.3s ease;
|
|
117
|
-
padding-bottom: 5px;
|
|
118
|
-
}
|
|
115
|
+
.value { color: #fff; font-size: clamp(1.2rem, 22cqh, 3rem); font-weight: 600; line-height: 0.9; letter-spacing: -1px; transition: color 0.3s ease; padding-bottom: 5px; }
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
margin-bottom: auto;
|
|
123
|
-
font-size: clamp(1.5rem, 35cqh, 4rem);
|
|
124
|
-
line-height: 0.85;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/* TACK Layout - Centratura verticale e compattezza */
|
|
128
|
-
.dual-value-container {
|
|
129
|
-
display: flex;
|
|
130
|
-
justify-content: space-between;
|
|
131
|
-
align-items: center;
|
|
132
|
-
width: 100%;
|
|
133
|
-
/* La "molla" auto sopra e sotto lo tiene al centro esatto del box */
|
|
117
|
+
/* Allineamento centrato per box senza grafico */
|
|
118
|
+
.value-large, .dual-value-container, .value-with-compass {
|
|
134
119
|
margin-top: auto;
|
|
135
120
|
margin-bottom: auto;
|
|
136
|
-
padding-bottom: 5px;
|
|
137
121
|
}
|
|
138
122
|
|
|
139
|
-
.
|
|
140
|
-
display: flex;
|
|
141
|
-
flex-direction: column;
|
|
142
|
-
width: 48%;
|
|
143
|
-
/* Rimosso height 100% per non farlo allungare fino al titolo */
|
|
144
|
-
}
|
|
123
|
+
.value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
|
|
145
124
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
margin-bottom: 2px; /* Distanza fissa e piccola dal numero sottostante */
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.value.dual-val {
|
|
155
|
-
font-size: clamp(1rem, 22cqh, 1.8rem);
|
|
156
|
-
padding-bottom: 0;
|
|
157
|
-
line-height: 0.9;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/* BUSSOLA TWD: Centrata e proporzionata */
|
|
161
|
-
.value-with-compass {
|
|
162
|
-
display: flex;
|
|
163
|
-
justify-content: space-between;
|
|
164
|
-
align-items: center;
|
|
165
|
-
width: 100%;
|
|
166
|
-
margin-top: auto;
|
|
167
|
-
margin-bottom: auto;
|
|
168
|
-
gap: 8px;
|
|
169
|
-
}
|
|
125
|
+
/* TACK E TWD BUSSOLA */
|
|
126
|
+
.dual-value-container { display: flex; justify-content: space-between; width: 100%; }
|
|
127
|
+
.dual-value-col { display: flex; flex-direction: column; width: 48%; }
|
|
128
|
+
.dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
|
|
129
|
+
.value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
|
|
170
130
|
|
|
131
|
+
.value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 10px; }
|
|
171
132
|
.mini-compass {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
background: #000;
|
|
176
|
-
border-radius: 50%;
|
|
177
|
-
flex-shrink: 0;
|
|
178
|
-
border: 1.5px solid #333;
|
|
179
|
-
box-shadow: inset 0 0 10px rgba(0,0,0,0.8);
|
|
180
|
-
transition: all 0.4s ease;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.value-with-compass .value-large {
|
|
184
|
-
margin: 0;
|
|
185
|
-
flex-grow: 1;
|
|
186
|
-
text-align: right;
|
|
187
|
-
font-size: clamp(1.2rem, 35cqh, 4rem);
|
|
133
|
+
width: clamp(40px, 60cqh, 120px); height: clamp(40px, 60cqh, 120px);
|
|
134
|
+
background: #000; border-radius: 50%; flex-shrink: 0; border: 1.5px solid #333;
|
|
135
|
+
box-shadow: inset 0 0 10px rgba(0,0,0,0.8); transition: all 0.4s ease;
|
|
188
136
|
}
|
|
137
|
+
.value-with-compass .value-large { margin: 0; flex-grow: 1; text-align: right; font-size: clamp(1.2rem, 35cqh, 4rem); }
|
|
189
138
|
|
|
190
139
|
/* ==========================================================================
|
|
191
140
|
5. GRAFICI E SCALE
|
|
192
141
|
========================================================================== */
|
|
193
|
-
.graph-wrapper {
|
|
194
|
-
position: relative;
|
|
195
|
-
width: 100%;
|
|
196
|
-
flex-grow: 1;
|
|
197
|
-
min-height: 20px;
|
|
198
|
-
margin-top: 4px;
|
|
199
|
-
display: flex;
|
|
200
|
-
align-items: stretch;
|
|
201
|
-
gap: 4px;
|
|
202
|
-
}
|
|
142
|
+
.graph-wrapper { position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px; display: flex; align-items: stretch; gap: 6px; }
|
|
203
143
|
.sparkline { flex-grow: 1; height: 100%; background: rgba(255, 255, 255, 0.03); border-radius: 4px; }
|
|
204
|
-
.sparkline path { stroke-width: 1px !important; }
|
|
205
144
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
color: #666;
|
|
212
|
-
font-weight: bold;
|
|
213
|
-
min-width: 16px;
|
|
214
|
-
height: 100%;
|
|
215
|
-
line-height: 1;
|
|
216
|
-
transition: all 0.3s ease;
|
|
145
|
+
/* Uniforma lo spessore di TUTTI i tipi di linea nei grafici (Standard) */
|
|
146
|
+
.sparkline path,
|
|
147
|
+
.sparkline line {
|
|
148
|
+
stroke-width: 1px !important;
|
|
149
|
+
transition: stroke-width 0.3s ease;
|
|
217
150
|
}
|
|
218
151
|
|
|
152
|
+
.scale-labels { display: flex; flex-direction: column; justify-content: space-between; font-size: 10px; color: #777; font-weight: bold; min-width: 16px; height: 100%; line-height: 1; transition: all 0.3s ease; }
|
|
153
|
+
|
|
154
|
+
/* Simmetria interna */
|
|
219
155
|
.left-panel .scale-labels { order: 2; text-align: left; }
|
|
220
156
|
.left-panel .sparkline { order: 1; }
|
|
221
157
|
.right-panel .scale-labels { order: 1; text-align: right; }
|
|
@@ -226,35 +162,50 @@ body {
|
|
|
226
162
|
#depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
|
|
227
163
|
#tws-graph { stroke: #f1c40f; fill: rgba(241, 196, 15, 0.12); }
|
|
228
164
|
|
|
229
|
-
/*
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
165
|
+
/* HERCULES MODE */
|
|
166
|
+
.line-hercules {
|
|
167
|
+
filter: drop-shadow(0 0 5px #ff0000);
|
|
168
|
+
stroke-width: 1.8px !important;
|
|
169
|
+
}
|
|
233
170
|
.box-hercules { background: rgba(255, 0, 0, 0.08) !important; transition: all 0.3s ease; }
|
|
234
|
-
.box-hercules::after { content: "HERCULES"; position: absolute; bottom: 4px; right: 35px; font-size: 7px; color: #ff4444; font-weight: 900;
|
|
171
|
+
.box-hercules::after { content: "HERCULES"; position: absolute; bottom: 4px; right: 35px; font-size: 7px; color: #ff4444; font-weight: 900; opacity: 0.8; }
|
|
235
172
|
|
|
173
|
+
/* ==========================================================================
|
|
174
|
+
6. STATI E ANIMAZIONI
|
|
175
|
+
========================================================================== */
|
|
236
176
|
#status { position: absolute; top: 5px; right: 15px; font-size: 0.5rem; text-transform: uppercase; z-index: 1000; }
|
|
237
177
|
.online { color: #2ecc71; opacity: 0.5; }
|
|
238
178
|
.offline { color: #e74c3c; font-weight: bold; }
|
|
239
|
-
|
|
240
179
|
#awa-pointer, #twa-pointer, #track-pointer, #twd-arrow { transition: all 0.6s cubic-bezier(0.1, 0.7, 0.1, 1); }
|
|
241
180
|
#leeway-mask-rect { transition: none; }
|
|
242
181
|
.alarm-warning { color: #f1c40f !important; }
|
|
243
182
|
.alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
|
|
244
183
|
@keyframes blink-unstable { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
|
|
245
184
|
.unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
|
|
246
|
-
|
|
247
185
|
#wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
|
|
248
186
|
|
|
249
187
|
/* ==========================================================================
|
|
250
188
|
7. RESPONSIVE (PORTRAIT)
|
|
251
189
|
========================================================================== */
|
|
252
190
|
@media (max-aspect-ratio: 0.9 / 1) {
|
|
253
|
-
.main-container {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
.
|
|
259
|
-
.
|
|
191
|
+
.main-container {
|
|
192
|
+
grid-template-columns: 1fr 1fr !important;
|
|
193
|
+
grid-template-rows: 45vh 55vh !important; /* Calibrato per iPhone */
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.center-panel { grid-row: 1; grid-column: 1 / span 2; padding: 5px 0; }
|
|
197
|
+
.left-panel { grid-row: 2; grid-column: 1; }
|
|
198
|
+
.right-panel { grid-row: 2; grid-column: 2; }
|
|
199
|
+
|
|
200
|
+
/* Focus Mode Verticale */
|
|
201
|
+
.main-container.focus-active { grid-template-columns: 100% !important; }
|
|
202
|
+
.focus-active .center-panel { grid-row: 1; height: 45vh !important; }
|
|
203
|
+
.focus-active .side-panel.has-focus { grid-row: 2; height: 55vh !important; }
|
|
204
|
+
|
|
205
|
+
/* Altezze box iPhone */
|
|
206
|
+
.data-box:nth-child(1), .data-box:nth-child(2) { height: 13.5vh; }
|
|
207
|
+
.data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 9.3vh; }
|
|
208
|
+
|
|
209
|
+
.value-large { font-size: clamp(1.2rem, 35cqh, 2.5rem); margin: auto 0; }
|
|
210
|
+
.value.dual-val { font-size: clamp(0.9rem, 30cqh, 1.4rem); }
|
|
260
211
|
}
|