@sailingrotevista/rotevista-dash 1.0.25 → 1.0.27
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 +135 -65
- package/package.json +1 -1
- package/style.css +53 -102
package/app.js
CHANGED
|
@@ -1,29 +1,51 @@
|
|
|
1
1
|
// ==========================================================================
|
|
2
|
-
// 1. CONFIGURAZIONE E
|
|
2
|
+
// 1. CONFIGURAZIONE E DEFAULT (Sincronizzato con il Plugin SignalK)
|
|
3
3
|
// ==========================================================================
|
|
4
4
|
let CONFIG = {
|
|
5
|
-
alarms: {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
alarms: {
|
|
6
|
+
depthDanger: 2.5,
|
|
7
|
+
depthWarning: 5.0
|
|
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
|
+
},
|
|
8
22
|
scales: {
|
|
9
|
-
stw:
|
|
10
|
-
sog:
|
|
11
|
-
tws:
|
|
23
|
+
stw: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
24
|
+
sog: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
25
|
+
tws: { stdMax: 25, hercSpan: 10, step: 5 },
|
|
12
26
|
depth: { stdMax: 20, hercSpan: 10, step: 10 }
|
|
13
27
|
},
|
|
14
|
-
server: {
|
|
28
|
+
server: {
|
|
29
|
+
fallbackIp: "192.168.111.240:3000"
|
|
30
|
+
}
|
|
15
31
|
};
|
|
16
32
|
|
|
17
33
|
const RENDER_INTERVAL_MS = 1000;
|
|
18
34
|
const TIMEOUT_MS = 5000;
|
|
19
35
|
const SIM_SAMPLE_INTERVAL = 1000;
|
|
20
36
|
|
|
37
|
+
// ==========================================================================
|
|
38
|
+
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
39
|
+
// ==========================================================================
|
|
21
40
|
let simulationMode = false;
|
|
22
41
|
let socket, renderInterval, simInterval;
|
|
23
42
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
24
43
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
44
|
+
|
|
45
|
+
// Gestione Interazioni (Long Press, Focus, Ghost Clicks)
|
|
25
46
|
let pressTimer, isFocusActive = false, blockNextClick = false;
|
|
26
47
|
|
|
48
|
+
// Modalità Scale (Standard/Hercules) salvate nel browser
|
|
27
49
|
const graphModes = {
|
|
28
50
|
stw: localStorage.getItem('mode_stw') || 'standard',
|
|
29
51
|
sog: localStorage.getItem('mode_sog') || 'standard',
|
|
@@ -31,7 +53,13 @@ const graphModes = {
|
|
|
31
53
|
depth: localStorage.getItem('mode_depth') || 'standard'
|
|
32
54
|
};
|
|
33
55
|
|
|
34
|
-
const store = {
|
|
56
|
+
const store = {
|
|
57
|
+
raw: {}, timestamps: {},
|
|
58
|
+
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
59
|
+
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
60
|
+
histories: { stw: [], sog: [], depth: [], tws: [] },
|
|
61
|
+
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 }
|
|
62
|
+
};
|
|
35
63
|
|
|
36
64
|
const ui = {
|
|
37
65
|
stw: document.getElementById('stw'), sog: document.getElementById('sog'),
|
|
@@ -47,13 +75,13 @@ const ui = {
|
|
|
47
75
|
};
|
|
48
76
|
|
|
49
77
|
// ==========================================================================
|
|
50
|
-
// 3.
|
|
78
|
+
// 3. COMUNICAZIONE CON IL SERVER (FETCH CONFIG)
|
|
51
79
|
// ==========================================================================
|
|
52
|
-
|
|
53
80
|
async function fetchServerConfig() {
|
|
54
81
|
if (!window.location.protocol.includes("http")) return;
|
|
55
82
|
const pluginID = 'rotevista-dash';
|
|
56
83
|
const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
|
|
84
|
+
|
|
57
85
|
for (let url of possibleUrls) {
|
|
58
86
|
try {
|
|
59
87
|
const response = await fetch(url);
|
|
@@ -61,31 +89,48 @@ async function fetchServerConfig() {
|
|
|
61
89
|
const data = await response.json();
|
|
62
90
|
const actual = data.configuration || data;
|
|
63
91
|
if (actual && typeof actual === 'object') {
|
|
64
|
-
|
|
92
|
+
// Normalizzazione dati (conversione stringhe -> numeri)
|
|
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
|
+
};
|
|
65
99
|
parseNumbers(actual);
|
|
100
|
+
// Merge nel sistema locale
|
|
66
101
|
if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
|
|
67
102
|
if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
|
|
68
103
|
if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
|
|
69
|
-
if (actual.scales) {
|
|
70
|
-
|
|
104
|
+
if (actual.scales) {
|
|
105
|
+
for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; }
|
|
106
|
+
}
|
|
107
|
+
console.log("Dashboard: Configurazione caricata dal server.");
|
|
108
|
+
return;
|
|
71
109
|
}
|
|
72
110
|
}
|
|
73
111
|
} catch (e) { }
|
|
74
112
|
}
|
|
75
113
|
}
|
|
76
114
|
|
|
115
|
+
// ==========================================================================
|
|
116
|
+
// 4. MATEMATICA E GESTIONE DATI
|
|
117
|
+
// ==========================================================================
|
|
77
118
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
78
119
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
79
120
|
function msToKts(ms) { return ms * 1.94384; }
|
|
80
121
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
81
122
|
function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
|
|
82
123
|
|
|
124
|
+
// Calcolo media circolare vettoriale
|
|
83
125
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
84
|
-
const now = Date.now();
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
85
128
|
if (validData.length === 0) return null;
|
|
86
|
-
let sSin = 0, sCos = 0;
|
|
129
|
+
let sSin = 0, sCos = 0;
|
|
130
|
+
validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
87
131
|
let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
|
|
88
|
-
let
|
|
132
|
+
let timeSpan = validData[validData.length - 1].time - validData[0].time;
|
|
133
|
+
let isStable = (timeSpan >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
89
134
|
let avgDeg = Math.round(radToDeg(Math.atan2(sSin, sCos)));
|
|
90
135
|
return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
|
|
91
136
|
}
|
|
@@ -96,35 +141,45 @@ function processIncomingData(path, val) {
|
|
|
96
141
|
if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
|
|
97
142
|
if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
|
|
98
143
|
if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
|
|
144
|
+
|
|
145
|
+
// Calcolo TWD (Vento Reale Direzione Geografica)
|
|
99
146
|
if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
|
|
100
|
-
let twdRad = 0;
|
|
101
|
-
|
|
102
|
-
else
|
|
103
|
-
|
|
147
|
+
let twdRad = 0;
|
|
148
|
+
if (path === "environment.wind.directionTrue") twdRad = val;
|
|
149
|
+
else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) {
|
|
150
|
+
twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
|
|
151
|
+
if (twdRad < 0) twdRad += (2 * Math.PI);
|
|
152
|
+
} else return;
|
|
153
|
+
store.smoothBuf.twd.push({ val: twdRad, time: now });
|
|
154
|
+
store.longBuf.twd.push({ val: twdRad, time: now });
|
|
104
155
|
}
|
|
105
156
|
}
|
|
106
157
|
|
|
107
158
|
// ==========================================================================
|
|
108
|
-
//
|
|
159
|
+
// 5. MOTORE DI RENDERING PRINCIPALE
|
|
109
160
|
// ==========================================================================
|
|
110
|
-
|
|
111
161
|
function startDisplayLoop() {
|
|
112
162
|
renderInterval = setInterval(() => {
|
|
113
163
|
const now = Date.now();
|
|
164
|
+
|
|
165
|
+
// 5.1 Watchdog
|
|
114
166
|
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 };
|
|
115
167
|
for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { pathsToWatch[p][pathsToWatch[p] === ui.awsSvg ? 'textContent' : 'innerText'] = "---"; delete store.raw[p]; } }
|
|
116
168
|
|
|
169
|
+
// 5.2 Renders Istantanei
|
|
117
170
|
if (store.raw["navigation.speedThroughWater"] !== undefined) { const v = msToKts(store.raw["navigation.speedThroughWater"]); ui.stw.innerText = v.toFixed(1); manageHistory('stw', v); }
|
|
118
171
|
let curSog = 0; if (store.raw["navigation.speedOverGround"] !== undefined) { curSog = msToKts(store.raw["navigation.speedOverGround"]); ui.sog.innerText = curSog.toFixed(1); manageHistory('sog', curSog); }
|
|
119
172
|
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); }
|
|
120
173
|
if (store.raw["environment.wind.speedTrue"] !== undefined) { const w = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = w.toFixed(1); manageHistory('tws', w); }
|
|
121
174
|
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
122
175
|
|
|
176
|
+
// 5.3 Render Quadrante (2s smoothing)
|
|
123
177
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
|
|
124
178
|
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, smAwa.val); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
125
179
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
126
180
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
127
181
|
|
|
182
|
+
// Drift Reale (COG-HDG)
|
|
128
183
|
if (store.raw["navigation.courseOverGroundTrue"] && store.raw["navigation.headingTrue"]) {
|
|
129
184
|
let drift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
|
|
130
185
|
if (curSog < CONFIG.averages.minSpeed) drift = 0;
|
|
@@ -132,18 +187,30 @@ function startDisplayLoop() {
|
|
|
132
187
|
let ds = drift > 180 ? drift - 360 : drift; updateLeewayDisplay(Math.max(-20, Math.min(20, ds)));
|
|
133
188
|
} else updateLeewayDisplay(0);
|
|
134
189
|
|
|
190
|
+
// 5.4 Render Medie Lunghe (Ogni 3s)
|
|
135
191
|
if (now - lastAvgUIUpdate > 3000) {
|
|
136
192
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
|
|
137
193
|
cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
|
|
138
194
|
awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true),
|
|
139
195
|
twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true),
|
|
140
196
|
twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
|
|
141
|
-
|
|
197
|
+
|
|
198
|
+
const upUI = (el, obj) => {
|
|
199
|
+
if (!obj) { el.innerHTML = "---°"; el.classList.remove('unstable-data'); }
|
|
200
|
+
else {
|
|
201
|
+
el.innerHTML = `${obj.val.toString().padStart(3, '0')}°`;
|
|
202
|
+
if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data'); else el.classList.add('unstable-data');
|
|
203
|
+
}
|
|
204
|
+
};
|
|
142
205
|
upUI(ui.hdg, hObj); upUI(ui.cog, cObj); upUI(ui.awaAvg, awObj); upUI(ui.twaAvg, twObj); upUI(ui.twdAvg, twdObj);
|
|
206
|
+
|
|
207
|
+
// TACK
|
|
143
208
|
if (hObj && twObj && hObj.val !== null) {
|
|
144
209
|
let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
145
210
|
if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
146
|
-
|
|
211
|
+
let tStable = (hObj.stable && twObj.stable) || (curSog < CONFIG.averages.minSpeed);
|
|
212
|
+
if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
|
|
213
|
+
else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
|
|
147
214
|
}
|
|
148
215
|
if (twdObj) { curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); }
|
|
149
216
|
lastAvgUIUpdate = now;
|
|
@@ -153,6 +220,23 @@ function startDisplayLoop() {
|
|
|
153
220
|
}, RENDER_INTERVAL_MS);
|
|
154
221
|
}
|
|
155
222
|
|
|
223
|
+
// ==========================================================================
|
|
224
|
+
// 6. CONNESSIONE SIGNALK (subscribe=self)
|
|
225
|
+
// ==========================================================================
|
|
226
|
+
function connect() {
|
|
227
|
+
if (simulationMode) return;
|
|
228
|
+
let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
|
|
229
|
+
try {
|
|
230
|
+
socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
|
|
231
|
+
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; };
|
|
232
|
+
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))); };
|
|
233
|
+
socket.onclose = () => !simulationMode && setTimeout(connect, 5000);
|
|
234
|
+
} catch (e) { setTimeout(connect, 5000); }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ==========================================================================
|
|
238
|
+
// 7. FUNZIONI GRAFICHE E SCALATURA
|
|
239
|
+
// ==========================================================================
|
|
156
240
|
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)}°`; }
|
|
157
241
|
|
|
158
242
|
function manageHistory(t, v) {
|
|
@@ -181,52 +265,30 @@ function updateScaleLabels(t, min, max) {
|
|
|
181
265
|
}
|
|
182
266
|
|
|
183
267
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
184
|
-
const svg = document.getElementById(id);
|
|
185
|
-
if (!svg || d.length < 2) return;
|
|
186
|
-
|
|
268
|
+
const svg = document.getElementById(id); if (!svg || d.length < 2) return;
|
|
187
269
|
const w = 200, h = 40, range = max - min || 1;
|
|
188
|
-
|
|
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
|
|
270
|
+
let grids = ""; [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" />`; });
|
|
271
|
+
for (let m = 1; m < CONFIG.graphs.historyMinutes; m++) { const x = w - (m / CONFIG.graphs.historyMinutes) * w; grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(255,255,255,0.05)" stroke-width="0.5" />`; }
|
|
199
272
|
let pD = "", cS = "";
|
|
200
273
|
d.forEach((v, i) => {
|
|
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
|
|
274
|
+
const x = (i/(CONFIG.graphs.samples-1))*w, y = h-(Math.max(0,Math.min(1,(v-min)/range))*h); pD += `${i===0?'M':'L'} ${x} ${y} `;
|
|
205
275
|
if (isTws && i > 0) {
|
|
206
276
|
const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
|
|
207
|
-
let c = "#f1c40f";
|
|
208
|
-
|
|
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}" />`;
|
|
277
|
+
let c = "#f1c40f"; if (v >= CONFIG.graphs.reef2) c = "#e74c3c"; else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
|
|
278
|
+
cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
|
|
212
279
|
}
|
|
213
280
|
});
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
}
|
|
281
|
+
const aP = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`, clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#f1c40f' };
|
|
282
|
+
svg.innerHTML = isTws ? `${grids}<path d="${aP}" fill="rgba(241,196,15,0.12)" stroke="none" />${cS}` : `${grids}<path d="${aP}" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${clrs[id]}" />`;
|
|
224
283
|
}
|
|
225
284
|
|
|
226
285
|
// ==========================================================================
|
|
227
|
-
//
|
|
286
|
+
// 8. EVENTI E INTERAZIONI (Touch, Focus e Fullscreen)
|
|
228
287
|
// ==========================================================================
|
|
229
288
|
|
|
289
|
+
// Blocco globale del menu contestuale per Android/iOS
|
|
290
|
+
window.addEventListener('contextmenu', e => e.preventDefault());
|
|
291
|
+
|
|
230
292
|
function toggleFocusMode(type, element) {
|
|
231
293
|
const container = document.querySelector('.main-container');
|
|
232
294
|
const parentPanel = element.closest('.side-panel');
|
|
@@ -238,24 +300,32 @@ function toggleFocusMode(type, element) {
|
|
|
238
300
|
|
|
239
301
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
240
302
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
303
|
+
|
|
304
|
+
// Doppio Click -> Hercules Zoom
|
|
241
305
|
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
|
-
|
|
306
|
+
|
|
307
|
+
// Long Press -> Tactical Focus
|
|
308
|
+
const startPress = () => { if (!isFocusActive) pressTimer = setTimeout(() => toggleFocusMode(type, el), 1000); };
|
|
243
309
|
const cancelPress = () => { clearTimeout(pressTimer); };
|
|
244
|
-
el.addEventListener('mousedown', startPress); el.addEventListener('touchstart', startPress, {passive:
|
|
245
|
-
['mouseup', 'mouseleave', 'touchend'].forEach(evt => el.addEventListener(evt, cancelPress));
|
|
310
|
+
el.addEventListener('mousedown', startPress); el.addEventListener('touchstart', startPress, {passive: true});
|
|
311
|
+
['mouseup', 'mouseleave', 'touchend', 'touchcancel'].forEach(evt => el.addEventListener(evt, cancelPress));
|
|
312
|
+
|
|
313
|
+
// Click -> Exit Focus o Ghost click filtering
|
|
246
314
|
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(); });
|
|
248
315
|
});
|
|
249
316
|
|
|
317
|
+
// Fullscreen via Hotspot
|
|
250
318
|
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(); } }); }
|
|
251
319
|
|
|
252
320
|
// ==========================================================================
|
|
253
|
-
//
|
|
321
|
+
// 9. INIZIALIZZAZIONE
|
|
254
322
|
// ==========================================================================
|
|
255
323
|
(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
|
-
|
|
324
|
+
|
|
257
325
|
async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
|
|
258
326
|
window.addEventListener('load', init);
|
|
327
|
+
|
|
328
|
+
// Allarmi e Audio
|
|
259
329
|
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
330
|
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); }
|
|
261
331
|
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -1,139 +1,92 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
1. BASE E STRUTTURA GENERALE
|
|
2
|
+
1. BASE E STRUTTURA GENERALE
|
|
3
3
|
========================================================================== */
|
|
4
4
|
body {
|
|
5
5
|
background-color: #000;
|
|
6
6
|
color: #fff;
|
|
7
7
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
|
8
|
-
margin: 0;
|
|
9
|
-
|
|
10
|
-
height: 100vh;
|
|
11
|
-
width: 100vw;
|
|
8
|
+
margin: 0; padding: 0;
|
|
9
|
+
height: 100vh; width: 100vw;
|
|
12
10
|
overflow: hidden;
|
|
13
|
-
/*
|
|
11
|
+
/* BLOCCO TOTALE GESTI DI SISTEMA (Android/iOS) */
|
|
12
|
+
-webkit-touch-callout: none;
|
|
14
13
|
-webkit-user-select: none;
|
|
15
14
|
user-select: none;
|
|
15
|
+
touch-action: none;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
.main-container {
|
|
19
19
|
display: grid;
|
|
20
|
-
width: 100%;
|
|
21
|
-
|
|
22
|
-
padding: 5px;
|
|
23
|
-
box-sizing: border-box;
|
|
20
|
+
width: 100%; height: 100%;
|
|
21
|
+
padding: 5px; box-sizing: border-box;
|
|
24
22
|
gap: 8px;
|
|
25
|
-
|
|
26
|
-
grid-template-columns: 1.5fr 3fr 1.5fr;
|
|
23
|
+
grid-template-columns: 1.7fr 3fr 1.7fr;
|
|
27
24
|
grid-template-rows: 100%;
|
|
28
|
-
transition: all 0.
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* Ottimizzazione per schermi molto larghi (16:10, 16:9) */
|
|
32
|
-
@media (min-aspect-ratio: 1.5) {
|
|
33
|
-
.main-container {
|
|
34
|
-
grid-template-columns: 2fr 3fr 2fr;
|
|
35
|
-
gap: 15px;
|
|
36
|
-
}
|
|
25
|
+
transition: all 0.4s ease;
|
|
37
26
|
}
|
|
38
27
|
|
|
39
28
|
/* ==========================================================================
|
|
40
29
|
2. PANNELLI E DATA-BOX
|
|
41
30
|
========================================================================== */
|
|
42
|
-
.side-panel {
|
|
43
|
-
display: flex;
|
|
44
|
-
flex-direction: column;
|
|
45
|
-
background: rgba(255, 255, 255, 0.03);
|
|
46
|
-
border-radius: 12px;
|
|
47
|
-
z-index: 10;
|
|
48
|
-
height: 100%;
|
|
49
|
-
overflow: hidden;
|
|
50
|
-
}
|
|
51
|
-
|
|
31
|
+
.side-panel { display: flex; flex-direction: column; background: rgba(255, 255, 255, 0.03); border-radius: 12px; height: 100%; overflow: hidden; }
|
|
52
32
|
.left-panel { grid-column: 1; }
|
|
33
|
+
.center-panel { grid-column: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; overflow: hidden; }
|
|
53
34
|
.right-panel { grid-column: 3; align-items: flex-end; text-align: right; }
|
|
54
35
|
|
|
55
|
-
.center-panel {
|
|
56
|
-
grid-column: 2;
|
|
57
|
-
display: flex;
|
|
58
|
-
flex-direction: column;
|
|
59
|
-
align-items: center;
|
|
60
|
-
justify-content: center;
|
|
61
|
-
height: 100%;
|
|
62
|
-
overflow: hidden;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
36
|
.data-box {
|
|
66
37
|
position: relative;
|
|
67
38
|
border-bottom: 1px solid #222;
|
|
68
39
|
padding: 8px 12px;
|
|
69
40
|
display: flex;
|
|
70
41
|
flex-direction: column;
|
|
71
|
-
width: 100%;
|
|
72
|
-
box-sizing: border-box;
|
|
42
|
+
width: 100%; box-sizing: border-box;
|
|
73
43
|
container-type: size;
|
|
74
44
|
flex: 1;
|
|
75
|
-
overflow: hidden;
|
|
45
|
+
overflow: hidden;
|
|
76
46
|
}
|
|
77
47
|
|
|
78
|
-
/* Altezze
|
|
48
|
+
/* Altezze Desktop */
|
|
79
49
|
.data-box:nth-child(1), .data-box:nth-child(2) { height: 25vh; }
|
|
80
50
|
.data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 16.666vh; }
|
|
81
51
|
|
|
82
52
|
/* ==========================================================================
|
|
83
|
-
3. TACTICAL FOCUS MODE (
|
|
53
|
+
3. TACTICAL FOCUS MODE (ORIZZONTALE)
|
|
84
54
|
========================================================================== */
|
|
85
55
|
.focus-active { grid-template-columns: 1fr 1fr !important; }
|
|
86
56
|
.focus-active .side-panel:not(.has-focus) { display: none !important; }
|
|
87
57
|
|
|
88
|
-
/* Simmetria
|
|
89
|
-
.focus-active.focus-side-left .side-panel.has-focus { grid-column: 1; }
|
|
90
|
-
.focus-active.focus-side-left .center-panel { grid-column: 2; }
|
|
91
|
-
.focus-active.focus-side-right .center-panel { grid-column: 1; }
|
|
92
|
-
.focus-active.focus-side-right .side-panel.has-focus { grid-column: 2; }
|
|
58
|
+
/* Simmetria Orizzontale */
|
|
59
|
+
.focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
|
|
60
|
+
.focus-active.focus-side-left .center-panel { grid-column: 2 !important; }
|
|
61
|
+
.focus-active.focus-side-right .center-panel { grid-column: 1 !important; }
|
|
62
|
+
.focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
|
|
93
63
|
|
|
94
|
-
/* Box Focalizzato a tutto schermo */
|
|
95
64
|
.focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
|
|
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
|
-
}
|
|
65
|
+
.focus-active .has-focus .data-box.is-focused { height: 100vh !important; border: none; background: rgba(255, 255, 255, 0.05); }
|
|
101
66
|
|
|
67
|
+
/* Tipografia e Grafica in Focus */
|
|
102
68
|
.focus-active .is-focused .value { font-size: clamp(3rem, 18cqh, 8rem) !important; margin-top: 10px; }
|
|
103
69
|
.focus-active .is-focused .scale-labels { font-size: 22px !important; min-width: 40px !important; }
|
|
104
|
-
|
|
105
|
-
/* Grafica Linee Sottili in Focus */
|
|
106
70
|
.focus-active .is-focused .sparkline path { stroke-width: 0.8px !important; }
|
|
107
71
|
|
|
108
72
|
/* ==========================================================================
|
|
109
|
-
4. TIPOGRAFIA DINAMICA (
|
|
73
|
+
4. TIPOGRAFIA DINAMICA (CQH)
|
|
110
74
|
========================================================================== */
|
|
111
75
|
.label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
|
|
112
76
|
.label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
|
|
113
77
|
.unit { color: #888; font-size: 0.6rem; font-weight: bold; }
|
|
78
|
+
.value { color: #fff; font-size: clamp(1.2rem, 22cqh, 3rem); font-weight: 600; line-height: 0.9; letter-spacing: -1px; padding-bottom: 5px; }
|
|
114
79
|
|
|
115
|
-
.value
|
|
116
|
-
|
|
117
|
-
/* Allineamento centrato per box senza grafico */
|
|
118
|
-
.value-large, .dual-value-container, .value-with-compass {
|
|
119
|
-
margin-top: auto;
|
|
120
|
-
margin-bottom: auto;
|
|
121
|
-
}
|
|
122
|
-
|
|
80
|
+
.value-large, .dual-value-container, .value-with-compass { margin-top: auto; margin-bottom: auto; }
|
|
123
81
|
.value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
|
|
124
82
|
|
|
125
|
-
/* TACK E TWD BUSSOLA */
|
|
126
83
|
.dual-value-container { display: flex; justify-content: space-between; width: 100%; }
|
|
127
84
|
.dual-value-col { display: flex; flex-direction: column; width: 48%; }
|
|
128
85
|
.dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
|
|
129
86
|
.value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
|
|
130
87
|
|
|
131
88
|
.value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 10px; }
|
|
132
|
-
.mini-compass {
|
|
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;
|
|
136
|
-
}
|
|
89
|
+
.mini-compass { width: clamp(40px, 60cqh, 120px); height: clamp(40px, 60cqh, 120px); background: #000; border-radius: 50%; flex-shrink: 0; border: 1.5px solid #333; box-shadow: inset 0 0 10px rgba(0,0,0,0.8); }
|
|
137
90
|
.value-with-compass .value-large { margin: 0; flex-grow: 1; text-align: right; font-size: clamp(1.2rem, 35cqh, 4rem); }
|
|
138
91
|
|
|
139
92
|
/* ==========================================================================
|
|
@@ -141,17 +94,9 @@ body {
|
|
|
141
94
|
========================================================================== */
|
|
142
95
|
.graph-wrapper { position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px; display: flex; align-items: stretch; gap: 6px; }
|
|
143
96
|
.sparkline { flex-grow: 1; height: 100%; background: rgba(255, 255, 255, 0.03); border-radius: 4px; }
|
|
97
|
+
.sparkline path, .sparkline line { stroke-width: 1px !important; transition: all 0.3s ease; }
|
|
144
98
|
|
|
145
|
-
|
|
146
|
-
.sparkline path,
|
|
147
|
-
.sparkline line {
|
|
148
|
-
stroke-width: 1px !important;
|
|
149
|
-
transition: stroke-width 0.3s ease;
|
|
150
|
-
}
|
|
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 */
|
|
99
|
+
.scale-labels { display: flex; flex-direction: column; justify-content: space-between; font-size: 10px; color: #777; font-weight: bold; min-width: 20px; height: 100%; line-height: 1; }
|
|
155
100
|
.left-panel .scale-labels { order: 2; text-align: left; }
|
|
156
101
|
.left-panel .sparkline { order: 1; }
|
|
157
102
|
.right-panel .scale-labels { order: 1; text-align: right; }
|
|
@@ -162,12 +107,8 @@ body {
|
|
|
162
107
|
#depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
|
|
163
108
|
#tws-graph { stroke: #f1c40f; fill: rgba(241, 196, 15, 0.12); }
|
|
164
109
|
|
|
165
|
-
|
|
166
|
-
.
|
|
167
|
-
filter: drop-shadow(0 0 5px #ff0000);
|
|
168
|
-
stroke-width: 1.8px !important;
|
|
169
|
-
}
|
|
170
|
-
.box-hercules { background: rgba(255, 0, 0, 0.08) !important; transition: all 0.3s ease; }
|
|
110
|
+
.line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
|
|
111
|
+
.box-hercules { background: rgba(255, 0, 0, 0.08) !important; }
|
|
171
112
|
.box-hercules::after { content: "HERCULES"; position: absolute; bottom: 4px; right: 35px; font-size: 7px; color: #ff4444; font-weight: 900; opacity: 0.8; }
|
|
172
113
|
|
|
173
114
|
/* ==========================================================================
|
|
@@ -185,27 +126,37 @@ body {
|
|
|
185
126
|
#wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
|
|
186
127
|
|
|
187
128
|
/* ==========================================================================
|
|
188
|
-
7. RESPONSIVE (PORTRAIT)
|
|
129
|
+
7. RESPONSIVE (PORTRAIT) - FIX DEFINITIVO 2x2 E FOCUS
|
|
189
130
|
========================================================================== */
|
|
190
131
|
@media (max-aspect-ratio: 0.9 / 1) {
|
|
191
132
|
.main-container {
|
|
192
133
|
grid-template-columns: 1fr 1fr !important;
|
|
193
|
-
grid-template-rows: 45vh 55vh !important;
|
|
134
|
+
grid-template-rows: 45vh 55vh !important;
|
|
194
135
|
}
|
|
195
136
|
|
|
196
|
-
|
|
197
|
-
.
|
|
198
|
-
.
|
|
137
|
+
/* Ordine Standard Portrait: Vento (1), SX (2), DX (3) */
|
|
138
|
+
.center-panel { grid-row: 1 !important; grid-column: 1 / span 2 !important; }
|
|
139
|
+
.left-panel { grid-row: 2 !important; grid-column: 1 !important; }
|
|
140
|
+
.right-panel { grid-row: 2 !important; grid-column: 2 !important; }
|
|
199
141
|
|
|
200
|
-
/*
|
|
201
|
-
.main-container.focus-active {
|
|
202
|
-
|
|
203
|
-
|
|
142
|
+
/* FOCUS MODE IN VERTICALE: Abbandoniamo la Grid, usiamo Flex per sicurezza */
|
|
143
|
+
.main-container.focus-active {
|
|
144
|
+
display: flex !important;
|
|
145
|
+
flex-direction: column !important;
|
|
146
|
+
}
|
|
147
|
+
.focus-active .center-panel {
|
|
148
|
+
flex: 0 0 45vh !important;
|
|
149
|
+
width: 100% !important;
|
|
150
|
+
}
|
|
151
|
+
.focus-active .side-panel.has-focus {
|
|
152
|
+
flex: 0 0 55vh !important;
|
|
153
|
+
width: 100% !important;
|
|
154
|
+
display: flex !important;
|
|
155
|
+
}
|
|
204
156
|
|
|
205
|
-
/*
|
|
157
|
+
/* Calibrazione altezze box per Portrait */
|
|
206
158
|
.data-box:nth-child(1), .data-box:nth-child(2) { height: 13.5vh; }
|
|
207
159
|
.data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 9.3vh; }
|
|
208
160
|
|
|
209
|
-
.value-large { font-size: clamp(1.2rem, 35cqh, 2.5rem);
|
|
210
|
-
.value.dual-val { font-size: clamp(0.9rem, 30cqh, 1.4rem); }
|
|
161
|
+
.value-large { font-size: clamp(1.2rem, 35cqh, 2.5rem); }
|
|
211
162
|
}
|