@sailingrotevista/rotevista-dash 2.0.4 → 2.0.5
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 +64 -109
- package/index.html +172 -52
- package/package.json +1 -1
- package/style.css +1 -1
package/app.js
CHANGED
|
@@ -7,17 +7,17 @@ let CONFIG = {
|
|
|
7
7
|
depthWarning: 5.0
|
|
8
8
|
},
|
|
9
9
|
averages: {
|
|
10
|
-
smoothWindow: 2000,
|
|
11
|
-
longWindow: 60000,
|
|
12
|
-
stabilityTolerance: 2000,
|
|
13
|
-
stabilityThreshold: 0.90,
|
|
14
|
-
minSpeed: 0.5
|
|
10
|
+
smoothWindow: 2000,
|
|
11
|
+
longWindow: 60000,
|
|
12
|
+
stabilityTolerance: 2000,
|
|
13
|
+
stabilityThreshold: 0.90,
|
|
14
|
+
minSpeed: 0.5
|
|
15
15
|
},
|
|
16
16
|
graphs: {
|
|
17
|
-
reef1: 15.0,
|
|
18
|
-
reef2: 20.0,
|
|
19
|
-
historyMinutes: 5,
|
|
20
|
-
samples: 60
|
|
17
|
+
reef1: 15.0,
|
|
18
|
+
reef2: 20.0,
|
|
19
|
+
historyMinutes: 5,
|
|
20
|
+
samples: 60
|
|
21
21
|
},
|
|
22
22
|
scales: {
|
|
23
23
|
stw: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
@@ -42,10 +42,8 @@ let socket, renderInterval, simInterval;
|
|
|
42
42
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
43
43
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
44
44
|
|
|
45
|
-
// Gestione Interazioni (Long Press, Focus, Ghost Clicks)
|
|
46
45
|
let pressTimer, isFocusActive = false, blockNextClick = false;
|
|
47
46
|
|
|
48
|
-
// Modalità Scale (Standard/Hercules) salvate nel browser
|
|
49
47
|
const graphModes = {
|
|
50
48
|
stw: localStorage.getItem('mode_stw') || 'standard',
|
|
51
49
|
sog: localStorage.getItem('mode_sog') || 'standard',
|
|
@@ -69,19 +67,20 @@ const ui = {
|
|
|
69
67
|
tws: document.getElementById('tws'), depth: document.getElementById('depth'),
|
|
70
68
|
twaAvg: document.getElementById('twa-avg'), awaAvg: document.getElementById('awa-avg'),
|
|
71
69
|
twdAvg: document.getElementById('twd-avg'), twdArrow: document.getElementById('twd-arrow'),
|
|
70
|
+
twdBoat: document.getElementById('twd-boat-wrap'),
|
|
71
|
+
twdChevron: document.getElementById('twd-wind-chevron'),
|
|
72
72
|
leewayMask: document.getElementById('leeway-mask-rect'), leewayVal: document.getElementById('leeway-val'),
|
|
73
73
|
tackHdg: document.getElementById('tack-hdg'), tackCog: document.getElementById('tack-cog'),
|
|
74
74
|
status: document.getElementById('status'), hotspot: document.getElementById('fullscreen-hotspot')
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
// ==========================================================================
|
|
78
|
-
// 3.
|
|
78
|
+
// 3. CARICAMENTO CONFIGURAZIONE DAL SERVER
|
|
79
79
|
// ==========================================================================
|
|
80
80
|
async function fetchServerConfig() {
|
|
81
81
|
if (!window.location.protocol.includes("http")) return;
|
|
82
82
|
const pluginID = 'rotevista-dash';
|
|
83
83
|
const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
|
|
84
|
-
|
|
85
84
|
for (let url of possibleUrls) {
|
|
86
85
|
try {
|
|
87
86
|
const response = await fetch(url);
|
|
@@ -89,23 +88,13 @@ async function fetchServerConfig() {
|
|
|
89
88
|
const data = await response.json();
|
|
90
89
|
const actual = data.configuration || data;
|
|
91
90
|
if (actual && typeof actual === 'object') {
|
|
92
|
-
|
|
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
|
-
};
|
|
91
|
+
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
92
|
parseNumbers(actual);
|
|
100
|
-
// Merge nel sistema locale
|
|
101
93
|
if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
|
|
102
94
|
if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
|
|
103
95
|
if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
|
|
104
|
-
if (actual.scales) {
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
console.log("Dashboard: Configurazione caricata dal server.");
|
|
108
|
-
return;
|
|
96
|
+
if (actual.scales) { for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; } }
|
|
97
|
+
console.log("SUCCESS: Config loaded."); return;
|
|
109
98
|
}
|
|
110
99
|
}
|
|
111
100
|
} catch (e) { }
|
|
@@ -121,16 +110,12 @@ function msToKts(ms) { return ms * 1.94384; }
|
|
|
121
110
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
122
111
|
function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
|
|
123
112
|
|
|
124
|
-
// Calcolo media circolare vettoriale
|
|
125
113
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
126
|
-
const now = Date.now();
|
|
127
|
-
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
114
|
+
const now = Date.now(); const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
128
115
|
if (validData.length === 0) return null;
|
|
129
|
-
let sSin = 0, sCos = 0;
|
|
130
|
-
validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
116
|
+
let sSin = 0, sCos = 0; validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
131
117
|
let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
|
|
132
|
-
let
|
|
133
|
-
let isStable = (timeSpan >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
118
|
+
let isStable = (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
134
119
|
let avgDeg = Math.round(radToDeg(Math.atan2(sSin, sCos)));
|
|
135
120
|
return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
|
|
136
121
|
}
|
|
@@ -141,45 +126,47 @@ function processIncomingData(path, val) {
|
|
|
141
126
|
if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
|
|
142
127
|
if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
|
|
143
128
|
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)
|
|
146
129
|
if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
|
|
147
|
-
let twdRad = 0;
|
|
148
|
-
if (path === "environment.wind.directionTrue") twdRad = val;
|
|
130
|
+
let twdRad = 0; if (path === "environment.wind.directionTrue") twdRad = val;
|
|
149
131
|
else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) {
|
|
150
132
|
twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
|
|
151
133
|
if (twdRad < 0) twdRad += (2 * Math.PI);
|
|
152
134
|
} else return;
|
|
153
|
-
store.smoothBuf.twd.push({ val: twdRad, time: now });
|
|
154
|
-
store.longBuf.twd.push({ val: twdRad, time: now });
|
|
135
|
+
store.smoothBuf.twd.push({ val: twdRad, time: now }); store.longBuf.twd.push({ val: twdRad, time: now });
|
|
155
136
|
}
|
|
156
137
|
}
|
|
157
138
|
|
|
158
139
|
// ==========================================================================
|
|
159
|
-
// 5. MOTORE
|
|
140
|
+
// 5. MOTORE RENDERING PRINCIPALE
|
|
160
141
|
// ==========================================================================
|
|
161
142
|
function startDisplayLoop() {
|
|
162
143
|
renderInterval = setInterval(() => {
|
|
163
144
|
const now = Date.now();
|
|
164
|
-
|
|
165
|
-
// 5.1 Watchdog
|
|
166
145
|
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 };
|
|
167
146
|
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]; } }
|
|
168
147
|
|
|
169
|
-
// 5.2 Renders Istantanei
|
|
170
148
|
if (store.raw["navigation.speedThroughWater"] !== undefined) { const v = msToKts(store.raw["navigation.speedThroughWater"]); ui.stw.innerText = v.toFixed(1); manageHistory('stw', v); }
|
|
171
149
|
let curSog = 0; if (store.raw["navigation.speedOverGround"] !== undefined) { curSog = msToKts(store.raw["navigation.speedOverGround"]); ui.sog.innerText = curSog.toFixed(1); manageHistory('sog', curSog); }
|
|
172
150
|
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); }
|
|
173
|
-
|
|
151
|
+
|
|
152
|
+
// --- 1. RENDERING TWS CON COLORE DINAMICO (BIANCO -> ARANCIO -> ROSSO) ---
|
|
153
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined) {
|
|
154
|
+
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
|
|
155
|
+
ui.tws.innerText = twsKts.toFixed(1);
|
|
156
|
+
let twsColor = "#fff"; // Default: Bianco
|
|
157
|
+
if (twsKts >= CONFIG.graphs.reef2) twsColor = "#e74c3c"; // Rosso
|
|
158
|
+
else if (twsKts >= CONFIG.graphs.reef1) twsColor = "#e67e22"; // Arancio
|
|
159
|
+
ui.tws.style.color = twsColor;
|
|
160
|
+
manageHistory('tws', twsKts);
|
|
161
|
+
}
|
|
174
162
|
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
175
163
|
|
|
176
|
-
//
|
|
164
|
+
// Render Quadrante
|
|
177
165
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
|
|
178
166
|
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, smAwa.val); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
179
167
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
180
168
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
181
169
|
|
|
182
|
-
// Drift Reale (COG-HDG)
|
|
183
170
|
if (store.raw["navigation.courseOverGroundTrue"] && store.raw["navigation.headingTrue"]) {
|
|
184
171
|
let drift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
|
|
185
172
|
if (curSog < CONFIG.averages.minSpeed) drift = 0;
|
|
@@ -187,7 +174,7 @@ function startDisplayLoop() {
|
|
|
187
174
|
let ds = drift > 180 ? drift - 360 : drift; updateLeewayDisplay(Math.max(-20, Math.min(20, ds)));
|
|
188
175
|
} else updateLeewayDisplay(0);
|
|
189
176
|
|
|
190
|
-
//
|
|
177
|
+
// --- 2. RENDERING MEDIE E BUSSOLA TATTICA ---
|
|
191
178
|
if (now - lastAvgUIUpdate > 3000) {
|
|
192
179
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
|
|
193
180
|
cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
|
|
@@ -204,7 +191,6 @@ function startDisplayLoop() {
|
|
|
204
191
|
};
|
|
205
192
|
upUI(ui.hdg, hObj); upUI(ui.cog, cObj); upUI(ui.awaAvg, awObj); upUI(ui.twaAvg, twObj); upUI(ui.twdAvg, twdObj);
|
|
206
193
|
|
|
207
|
-
// TACK
|
|
208
194
|
if (hObj && twObj && hObj.val !== null) {
|
|
209
195
|
let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
210
196
|
if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
@@ -212,7 +198,19 @@ function startDisplayLoop() {
|
|
|
212
198
|
if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
|
|
213
199
|
else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
|
|
214
200
|
}
|
|
215
|
-
|
|
201
|
+
|
|
202
|
+
// Rotazione Bussola Tattica e Colore Sincronizzato
|
|
203
|
+
if (twdObj && hObj) {
|
|
204
|
+
curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val);
|
|
205
|
+
ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`);
|
|
206
|
+
ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`);
|
|
207
|
+
|
|
208
|
+
let currentTwsKts = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
209
|
+
let reefColor = "#fff";
|
|
210
|
+
if (currentTwsKts >= CONFIG.graphs.reef2) reefColor = "#e74c3c";
|
|
211
|
+
else if (currentTwsKts >= CONFIG.graphs.reef1) reefColor = "#e67e22";
|
|
212
|
+
if (ui.twdChevron) ui.twdChevron.setAttribute('stroke', reefColor);
|
|
213
|
+
}
|
|
216
214
|
lastAvgUIUpdate = now;
|
|
217
215
|
}
|
|
218
216
|
for (let b in store.smoothBuf) { while (store.smoothBuf[b].length > 0 && (now - store.smoothBuf[b][0].time) > CONFIG.averages.smoothWindow) store.smoothBuf[b].shift(); }
|
|
@@ -221,7 +219,7 @@ function startDisplayLoop() {
|
|
|
221
219
|
}
|
|
222
220
|
|
|
223
221
|
// ==========================================================================
|
|
224
|
-
// 6. CONNESSIONE SIGNALK
|
|
222
|
+
// 6. CONNESSIONE SIGNALK
|
|
225
223
|
// ==========================================================================
|
|
226
224
|
function connect() {
|
|
227
225
|
if (simulationMode) return;
|
|
@@ -229,13 +227,13 @@ function connect() {
|
|
|
229
227
|
try {
|
|
230
228
|
socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
|
|
231
229
|
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))); };
|
|
230
|
+
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))); };
|
|
233
231
|
socket.onclose = () => !simulationMode && setTimeout(connect, 5000);
|
|
234
232
|
} catch (e) { setTimeout(connect, 5000); }
|
|
235
233
|
}
|
|
236
234
|
|
|
237
235
|
// ==========================================================================
|
|
238
|
-
// 7. FUNZIONI GRAFICHE E
|
|
236
|
+
// 7. FUNZIONI GRAFICHE E DISEGNO (BIANCO -> ARANCIO -> ROSSO)
|
|
239
237
|
// ==========================================================================
|
|
240
238
|
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)}°`; }
|
|
241
239
|
|
|
@@ -274,33 +272,31 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
274
272
|
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} `;
|
|
275
273
|
if (isTws && i > 0) {
|
|
276
274
|
const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
|
|
277
|
-
let c = "#
|
|
275
|
+
let c = "#fff"; // Default: Bianco
|
|
276
|
+
if (v >= CONFIG.graphs.reef2) c = "#e74c3c"; else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
|
|
278
277
|
cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
|
|
279
278
|
}
|
|
280
279
|
});
|
|
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': '#
|
|
282
|
-
svg.innerHTML = isTws ? `${grids}<path d="${aP}" fill="rgba(
|
|
280
|
+
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': '#ffffff' };
|
|
281
|
+
svg.innerHTML = isTws ? `${grids}<path d="${aP}" fill="rgba(255,255,255,0.08)" 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]}" />`;
|
|
283
282
|
}
|
|
284
283
|
|
|
285
284
|
// ==========================================================================
|
|
286
|
-
// 8. EVENTI E INTERAZIONI (
|
|
285
|
+
// 8. EVENTI E INTERAZIONI (SMART TOUCH MANAGER)
|
|
287
286
|
// ==========================================================================
|
|
288
|
-
|
|
289
|
-
// Uccide il menu contestuale di Android/iOS
|
|
290
287
|
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
291
288
|
|
|
292
289
|
function toggleFocusMode(type, element) {
|
|
293
290
|
const container = document.querySelector('.main-container');
|
|
294
291
|
const parentPanel = element.closest('.side-panel');
|
|
295
292
|
const isLeft = parentPanel.classList.contains('left-panel');
|
|
296
|
-
|
|
297
293
|
isFocusActive = !isFocusActive;
|
|
298
|
-
|
|
299
294
|
if (isFocusActive) {
|
|
300
295
|
container.classList.add('focus-active');
|
|
301
296
|
container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right');
|
|
302
297
|
parentPanel.classList.add('has-focus');
|
|
303
298
|
element.classList.add('is-focused');
|
|
299
|
+
blockNextClick = true;
|
|
304
300
|
} else {
|
|
305
301
|
container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
|
|
306
302
|
document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus'));
|
|
@@ -308,85 +304,44 @@ function toggleFocusMode(type, element) {
|
|
|
308
304
|
}
|
|
309
305
|
}
|
|
310
306
|
|
|
311
|
-
// Configurazione Interazioni per i 4 grafici principali
|
|
312
307
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
313
308
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
314
|
-
|
|
315
|
-
let lastTapTime = 0;
|
|
316
|
-
let tapTimeout;
|
|
309
|
+
let lastTapTime = 0, tapTimeout;
|
|
317
310
|
|
|
318
311
|
const handleInteraction = (e) => {
|
|
319
|
-
// Impedisce al browser di fare qualsiasi cosa (zoom, menu, click fantasma)
|
|
320
312
|
if (e.cancelable) e.preventDefault();
|
|
321
|
-
|
|
322
313
|
const currentTime = new Date().getTime();
|
|
323
314
|
const tapDelay = currentTime - lastTapTime;
|
|
324
315
|
|
|
325
|
-
|
|
326
|
-
// Avviamo un timer per la pressione lunga
|
|
327
|
-
pressTimer = setTimeout(() => {
|
|
328
|
-
if (!isFocusActive) {
|
|
329
|
-
toggleFocusMode(type, el);
|
|
330
|
-
lastTapTime = 0; // Reset per non innescare click singoli al rilascio
|
|
331
|
-
}
|
|
332
|
-
}, 800); // 800ms per attivare il Focus
|
|
316
|
+
pressTimer = setTimeout(() => { if (!isFocusActive) { toggleFocusMode(type, el); lastTapTime = 0; } }, 1000);
|
|
333
317
|
|
|
334
|
-
// --- 2. GESTIONE DOPPIO E SINGOLO TOCCO ---
|
|
335
|
-
// Se tocchiamo di nuovo entro 300ms è un DOUBLE TAP
|
|
336
318
|
if (tapDelay < 300 && tapDelay > 0) {
|
|
337
|
-
clearTimeout(tapTimeout);
|
|
319
|
+
clearTimeout(tapTimeout);
|
|
338
320
|
if (!isFocusActive) {
|
|
339
|
-
// Toggle Hercules Mode
|
|
340
321
|
graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
|
|
341
322
|
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
342
323
|
el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
|
|
343
324
|
}
|
|
344
325
|
lastTapTime = 0;
|
|
345
326
|
} else {
|
|
346
|
-
// Se è passato più tempo, potrebbe essere un SINGOLO TAP
|
|
347
327
|
lastTapTime = currentTime;
|
|
348
|
-
tapTimeout = setTimeout(() => {
|
|
349
|
-
// Se siamo in focus mode, il singolo tap esce
|
|
350
|
-
if (isFocusActive && el.classList.contains('is-focused')) {
|
|
351
|
-
toggleFocusMode(type, el);
|
|
352
|
-
}
|
|
353
|
-
}, 350); // Attesa per vedere se arriva il secondo tap
|
|
328
|
+
tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); }, 350);
|
|
354
329
|
}
|
|
355
330
|
};
|
|
356
|
-
|
|
357
|
-
const stopInteraction = () => {
|
|
358
|
-
clearTimeout(pressTimer);
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
// Usiamo PointerEvents: funzionano identici per Mouse e Touch
|
|
362
331
|
el.addEventListener('pointerdown', handleInteraction);
|
|
363
|
-
el.addEventListener('pointerup',
|
|
364
|
-
el.addEventListener('pointerleave',
|
|
332
|
+
el.addEventListener('pointerup', () => clearTimeout(pressTimer));
|
|
333
|
+
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
365
334
|
});
|
|
366
335
|
|
|
367
|
-
|
|
368
|
-
if (ui.hotspot) {
|
|
369
|
-
ui.hotspot.addEventListener('click', (e) => {
|
|
370
|
-
e.preventDefault();
|
|
371
|
-
const doc = document.documentElement, isF = document.fullscreenElement || document.webkitFullscreenElement;
|
|
372
|
-
if (!isF) { if (doc.requestFullscreen) doc.requestFullscreen(); else if (doc.webkitRequestFullscreen) doc.webkitRequestFullscreen(); }
|
|
373
|
-
else { if (document.exitFullscreen) document.exitFullscreen(); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); }
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// ==========================================================================
|
|
378
|
-
// 9. INIZIALIZZAZIONE
|
|
379
|
-
// ==========================================================================
|
|
336
|
+
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(); } }); }
|
|
380
337
|
(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); } } })();
|
|
381
338
|
|
|
382
339
|
async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
|
|
383
340
|
window.addEventListener('load', init);
|
|
384
|
-
|
|
385
|
-
// Allarmi e Audio
|
|
386
341
|
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'); }
|
|
387
342
|
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); }
|
|
388
343
|
|
|
389
|
-
// Simulatore
|
|
344
|
+
// Simulatore
|
|
390
345
|
ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
391
346
|
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(); ui.status.innerText = "SIM ATTIVO"; simInterval = setInterval(() => { processIncomingData("navigation.headingTrue", degToRad(Math.random()*360)); processIncomingData("navigation.speedOverGround", ktsToMs(8)); }, 200); } else location.reload(); dC = 0; } };
|
|
392
347
|
})());
|
package/index.html
CHANGED
|
@@ -16,11 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
<div class="main-container">
|
|
18
18
|
|
|
19
|
-
<!--
|
|
19
|
+
<!-- ======================================================= -->
|
|
20
|
+
<!-- COLONNA SINISTRA: Dati Rotta e Velocità -->
|
|
21
|
+
<!-- ======================================================= -->
|
|
20
22
|
<div class="side-panel left-panel">
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
<!-- STW: Velocità attraverso l'acqua -->
|
|
22
25
|
<div class="data-box">
|
|
23
|
-
<div class="label-row"
|
|
26
|
+
<div class="label-row">
|
|
27
|
+
<span class="label">STW</span>
|
|
28
|
+
<span class="unit">kts</span>
|
|
29
|
+
</div>
|
|
24
30
|
<span class="value" id="stw">0.0</span>
|
|
25
31
|
<div class="graph-wrapper">
|
|
26
32
|
<svg class="sparkline" id="stw-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
|
|
@@ -28,9 +34,12 @@
|
|
|
28
34
|
</div>
|
|
29
35
|
</div>
|
|
30
36
|
|
|
31
|
-
<!-- SOG
|
|
37
|
+
<!-- SOG: Velocità sul fondo (GPS) -->
|
|
32
38
|
<div class="data-box">
|
|
33
|
-
<div class="label-row"
|
|
39
|
+
<div class="label-row">
|
|
40
|
+
<span class="label">SOG</span>
|
|
41
|
+
<span class="unit">kts</span>
|
|
42
|
+
</div>
|
|
34
43
|
<span class="value" id="sog">0.0</span>
|
|
35
44
|
<div class="graph-wrapper">
|
|
36
45
|
<svg class="sparkline" id="sog-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
|
|
@@ -38,21 +47,27 @@
|
|
|
38
47
|
</div>
|
|
39
48
|
</div>
|
|
40
49
|
|
|
41
|
-
<!-- HEADING
|
|
50
|
+
<!-- HEADING: Prua Bussola (Media 60s) -->
|
|
42
51
|
<div class="data-box">
|
|
43
|
-
<div class="label-row"
|
|
52
|
+
<div class="label-row">
|
|
53
|
+
<span class="label">HEADING (MEAN)</span>
|
|
54
|
+
</div>
|
|
44
55
|
<span class="value value-large" id="hdg">000°</span>
|
|
45
56
|
</div>
|
|
46
57
|
|
|
47
|
-
<!-- COG
|
|
58
|
+
<!-- COG: Rotta sul fondo (Media 60s) -->
|
|
48
59
|
<div class="data-box">
|
|
49
|
-
<div class="label-row"
|
|
60
|
+
<div class="label-row">
|
|
61
|
+
<span class="label">COG (MEAN)</span>
|
|
62
|
+
</div>
|
|
50
63
|
<span class="value value-large" id="cog">000°</span>
|
|
51
64
|
</div>
|
|
52
65
|
|
|
53
|
-
<!-- TACK:
|
|
66
|
+
<!-- TACK: Calcolo mure opposte (Previsione) -->
|
|
54
67
|
<div class="data-box">
|
|
55
|
-
<div class="label-row"
|
|
68
|
+
<div class="label-row">
|
|
69
|
+
<span class="label">TACK</span>
|
|
70
|
+
</div>
|
|
56
71
|
<div class="dual-value-container">
|
|
57
72
|
<div class="dual-value-col">
|
|
58
73
|
<span class="dual-label">HDG</span>
|
|
@@ -64,59 +79,136 @@
|
|
|
64
79
|
</div>
|
|
65
80
|
</div>
|
|
66
81
|
</div>
|
|
82
|
+
|
|
67
83
|
</div>
|
|
68
84
|
|
|
69
|
-
<!--
|
|
85
|
+
<!-- ======================================================= -->
|
|
86
|
+
<!-- CENTRO: Strumento Vento SVG (Ingrandito e Ottimizzato) -->
|
|
87
|
+
<!-- ======================================================= -->
|
|
70
88
|
<div class="center-panel">
|
|
89
|
+
<!-- ViewBox ottimizzato per ingrandire il diametro (Zoom in) -->
|
|
71
90
|
<svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
|
|
72
91
|
<defs>
|
|
73
|
-
|
|
74
|
-
<linearGradient id="
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
<!-- Gradienti e Maschere per i settori del vento -->
|
|
93
|
+
<linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
94
|
+
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
|
95
|
+
<stop offset="100%" style="stop-color:#888888;stop-opacity:1" />
|
|
96
|
+
</linearGradient>
|
|
97
|
+
<linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
98
|
+
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
99
|
+
<stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" />
|
|
100
|
+
<stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" />
|
|
101
|
+
<stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" />
|
|
102
|
+
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
103
|
+
</linearGradient>
|
|
104
|
+
<clipPath id="leeway-clip">
|
|
105
|
+
<rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" />
|
|
106
|
+
</clipPath>
|
|
107
|
+
|
|
108
|
+
<!-- Filtro Glow per l'area di interazione (Hotspot) centrale -->
|
|
109
|
+
<filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
110
|
+
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
111
|
+
</filter>
|
|
77
112
|
</defs>
|
|
78
113
|
|
|
79
|
-
|
|
114
|
+
<!-- Sfondo circolare del quadrante -->
|
|
115
|
+
<circle cx="200" cy="200" r="160" fill="#050505" />
|
|
116
|
+
<circle cx="200" cy="200" r="125" fill="#121212" />
|
|
117
|
+
|
|
118
|
+
<!-- Settori Vento (Rosso/Sinitra, Verde/Dritta, Arancio/Poppa) -->
|
|
80
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"/>
|
|
81
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"/>
|
|
82
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"/>
|
|
83
122
|
|
|
123
|
+
<!-- Tacche Gradate (Generate dinamicamente da Javascript) -->
|
|
84
124
|
<g id="ticks"></g>
|
|
125
|
+
|
|
126
|
+
<!-- Etichette fisse dei Gradi -->
|
|
85
127
|
<g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="hanging" font-family="Arial" font-weight="bold">
|
|
86
|
-
<text font-size="16" transform="translate(200, 65)">0</text
|
|
87
|
-
<text font-size="
|
|
88
|
-
<text font-size="
|
|
89
|
-
<text font-size="
|
|
90
|
-
|
|
128
|
+
<text font-size="16" transform="translate(200, 65)">0</text>
|
|
129
|
+
<text font-size="16" transform="translate(335, 200) rotate(90)">90</text>
|
|
130
|
+
<text font-size="16" transform="translate(65, 200) rotate(-90)">90</text>
|
|
131
|
+
<text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
|
|
132
|
+
|
|
133
|
+
<!-- Dettagli 30-150 Gradi -->
|
|
134
|
+
<text font-size="11" transform="translate(267.5, 83) rotate(30)">30</text>
|
|
135
|
+
<text font-size="11" transform="translate(317, 132.5) rotate(60)">60</text>
|
|
136
|
+
<text font-size="11" transform="translate(317, 267.5) rotate(120)">120</text>
|
|
137
|
+
<text font-size="11" transform="translate(267.5, 317) rotate(150)">150</text>
|
|
138
|
+
<text font-size="11" transform="translate(132.5, 83) rotate(-30)">30</text>
|
|
139
|
+
<text font-size="11" transform="translate(83, 132.5) rotate(-60)">60</text>
|
|
140
|
+
<text font-size="11" transform="translate(83, 267.5) rotate(-120)">120</text>
|
|
141
|
+
<text font-size="11" transform="translate(132.5, 317) rotate(-150)">150</text>
|
|
91
142
|
</g>
|
|
92
143
|
|
|
93
|
-
|
|
144
|
+
<!-- Puntatore Track / COG (Blu) -->
|
|
145
|
+
<g id="track-pointer" transform="rotate(0, 200, 200)">
|
|
146
|
+
<path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" />
|
|
147
|
+
</g>
|
|
148
|
+
|
|
149
|
+
<!-- Grande area sensibile al tocco (Hotspot) con Glow per Fullscreen -->
|
|
94
150
|
<circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
|
|
151
|
+
|
|
152
|
+
<!-- Icona Barca Centrale (Spinta Y+5 per centratura visiva perfetta) -->
|
|
95
153
|
<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;" />
|
|
96
154
|
|
|
155
|
+
<!-- Display Centrale Numerico: Vento Apparente -->
|
|
97
156
|
<g id="aws-display-group" transform="translate(200, 265)">
|
|
98
157
|
<text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
|
|
99
158
|
<text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
|
|
100
159
|
</g>
|
|
101
|
-
|
|
102
|
-
|
|
160
|
+
|
|
161
|
+
<!-- Lancette Dinamiche: AWA (Apparente) e TWA (Reale) -->
|
|
162
|
+
<g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
|
|
163
|
+
<path d="M200,80 L211,95 L200,145 L189,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" />
|
|
164
|
+
<text x="200" y="102" fill="#000" font-size="11" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
|
|
165
|
+
</g>
|
|
166
|
+
|
|
167
|
+
<g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
|
|
168
|
+
<path d="M200,90 L206,98 L200,125 L194,98 Z" fill="#ffff00" stroke="#000" stroke-width="0.8" />
|
|
169
|
+
<text x="200" y="104" fill="#000" font-size="8" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
|
|
170
|
+
</g>
|
|
103
171
|
|
|
172
|
+
<!-- Barra LEEWAY / SCARROCCIO Inferiore -->
|
|
104
173
|
<g transform="translate(75, 395)">
|
|
105
174
|
<text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0°</text>
|
|
106
|
-
<rect x="0" y="0" width="250" height="12" fill="#222" rx="3"
|
|
175
|
+
<rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
|
|
176
|
+
<rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
|
|
177
|
+
|
|
178
|
+
<!-- Scala gradi Leeway -->
|
|
107
179
|
<g stroke="#555" stroke-width="1">
|
|
108
|
-
<line x1="0" y1="-2" x2="0" y2="14"
|
|
180
|
+
<line x1="0" y1="-2" x2="0" y2="14" />
|
|
181
|
+
<line x1="31.25" y1="2" x2="31.25" y2="10" />
|
|
182
|
+
<line x1="62.5" y1="2" x2="62.5" y2="10" />
|
|
183
|
+
<line x1="93.75" y1="3" x2="93.75" y2="9" />
|
|
184
|
+
<line x1="125" y1="-2" x2="125" y2="14" />
|
|
185
|
+
<line x1="156.25" y1="3" x2="156.25" y2="9" />
|
|
186
|
+
<line x1="187.5" y1="2" x2="187.5" y2="10" />
|
|
187
|
+
<line x1="218.75" y1="2" x2="218.75" y2="10" />
|
|
188
|
+
<line x1="250" y1="-2" x2="250" y2="14" />
|
|
189
|
+
</g>
|
|
190
|
+
<g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
|
|
191
|
+
<text x="0" y="24">-20°</text>
|
|
192
|
+
<text x="62.5" y="24">-10</text>
|
|
193
|
+
<text x="125" y="24">0°</text>
|
|
194
|
+
<text x="187.5" y="24">10</text>
|
|
195
|
+
<text x="250" y="24">20°</text>
|
|
109
196
|
</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>
|
|
111
197
|
</g>
|
|
112
198
|
</svg>
|
|
113
199
|
</div>
|
|
114
200
|
|
|
115
|
-
<!--
|
|
201
|
+
<!-- ======================================================= -->
|
|
202
|
+
<!-- COLONNA DESTRA: Profondità e Vento Reale -->
|
|
203
|
+
<!-- ======================================================= -->
|
|
116
204
|
<div class="side-panel right-panel">
|
|
117
|
-
|
|
205
|
+
|
|
206
|
+
<!-- DEPTH: Profondità sotto il trasduttore -->
|
|
118
207
|
<div class="data-box">
|
|
119
|
-
<div class="label-row"
|
|
208
|
+
<div class="label-row">
|
|
209
|
+
<span class="unit">m</span>
|
|
210
|
+
<span class="label">DEPTH</span>
|
|
211
|
+
</div>
|
|
120
212
|
<span class="value" id="depth">--.-</span>
|
|
121
213
|
<div class="graph-wrapper">
|
|
122
214
|
<div class="scale-labels" id="depth-scale"></div>
|
|
@@ -124,9 +216,12 @@
|
|
|
124
216
|
</div>
|
|
125
217
|
</div>
|
|
126
218
|
|
|
127
|
-
<!-- TWS -->
|
|
219
|
+
<!-- TWS: Velocità del vento reale -->
|
|
128
220
|
<div class="data-box">
|
|
129
|
-
<div class="label-row"
|
|
221
|
+
<div class="label-row">
|
|
222
|
+
<span class="unit">kts</span>
|
|
223
|
+
<span class="label">TWS</span>
|
|
224
|
+
</div>
|
|
130
225
|
<span class="value" id="tws">0.0</span>
|
|
131
226
|
<div class="graph-wrapper">
|
|
132
227
|
<div class="scale-labels" id="tws-scale"></div>
|
|
@@ -134,39 +229,64 @@
|
|
|
134
229
|
</div>
|
|
135
230
|
</div>
|
|
136
231
|
|
|
137
|
-
<!-- TWA
|
|
232
|
+
<!-- TWA: Angolo del vento reale -->
|
|
138
233
|
<div class="data-box">
|
|
139
|
-
<div class="label-row"
|
|
234
|
+
<div class="label-row">
|
|
235
|
+
<span class="label">TWA (MEAN)</span>
|
|
236
|
+
</div>
|
|
140
237
|
<span class="value value-large" id="twa-avg">---°</span>
|
|
141
238
|
</div>
|
|
142
239
|
|
|
143
|
-
<!-- AWA
|
|
240
|
+
<!-- AWA: Angolo del vento apparente -->
|
|
144
241
|
<div class="data-box">
|
|
145
|
-
<div class="label-row"
|
|
242
|
+
<div class="label-row">
|
|
243
|
+
<span class="label">AWA (MEAN)</span>
|
|
244
|
+
</div>
|
|
146
245
|
<span class="value value-large" id="awa-avg">---°</span>
|
|
147
246
|
</div>
|
|
148
247
|
|
|
149
|
-
<!-- TWD
|
|
248
|
+
<!-- TWD: Direzione geografica reale del vento (con Bussola) -->
|
|
150
249
|
<div class="data-box">
|
|
151
|
-
<div class="label-row"
|
|
250
|
+
<div class="label-row">
|
|
251
|
+
<span class="label">TWD (MEAN)</span>
|
|
252
|
+
</div>
|
|
152
253
|
<div class="value-with-compass">
|
|
153
254
|
<svg class="mini-compass" viewBox="0 0 40 40">
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
255
|
+
<!-- Cerchio esterno fisso -->
|
|
256
|
+
<circle cx="20" cy="20" r="18.5" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1.2"/>
|
|
257
|
+
|
|
258
|
+
<!-- Punti Cardinali Nord/Sud -->
|
|
259
|
+
<text x="20" y="8" fill="#e74c3c" font-size="5.5" text-anchor="middle" font-weight="900">N</text>
|
|
260
|
+
<text x="20" y="36" fill="rgba(255,255,255,0.3)" font-size="5.5" text-anchor="middle" font-weight="900">S</text>
|
|
261
|
+
|
|
262
|
+
<!-- Tacche Orizzontali (Est / Ovest) -->
|
|
263
|
+
<g stroke="rgba(255,255,255,0.2)" stroke-width="1">
|
|
264
|
+
<line x1="2" y1="20" x2="5" y2="20" /> <!-- Ovest -->
|
|
265
|
+
<line x1="35" y1="20" x2="38" y2="20" /> <!-- Est -->
|
|
266
|
+
</g>
|
|
267
|
+
|
|
268
|
+
<!-- 1. SAGOMA BARCA: Ruota con l'Heading (id: twd-boat-wrap) -->
|
|
269
|
+
<g id="twd-boat-wrap" transform="rotate(0, 20, 20)">
|
|
270
|
+
<path d="M 20,17 L 17,26 L 20,24.5 L 23,26 Z" fill="white" opacity="0.2" />
|
|
271
|
+
</g>
|
|
272
|
+
|
|
273
|
+
<!-- 2. INDICATORE VENTO: Ruota con il TWD (id: twd-arrow) -->
|
|
274
|
+
<g id="twd-arrow" transform="rotate(0, 20, 20)">
|
|
275
|
+
<path id="twd-wind-chevron" d="M 17,5 L 20,7.5 L 23,5"
|
|
276
|
+
fill="none"
|
|
277
|
+
stroke="#ffff00"
|
|
278
|
+
stroke-width="2.2"
|
|
279
|
+
stroke-linecap="round"
|
|
280
|
+
stroke-linejoin="round" />
|
|
281
|
+
</g>
|
|
282
|
+
</svg>
|
|
164
283
|
<span class="value value-large" id="twd-avg">---°</span>
|
|
165
284
|
</div>
|
|
166
285
|
</div>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
</div>
|
|
286
|
+
|
|
287
|
+
</div> <!-- /Destra -->
|
|
288
|
+
</div> <!-- /Main Container -->
|
|
289
|
+
|
|
170
290
|
<script src="app.js"></script>
|
|
171
291
|
</body>
|
|
172
292
|
</html>
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -154,7 +154,7 @@ body {
|
|
|
154
154
|
#status { position: absolute; top: 5px; right: 15px; font-size: 0.5rem; text-transform: uppercase; z-index: 1000; }
|
|
155
155
|
.online { color: #2ecc71; opacity: 0.5; }
|
|
156
156
|
.offline { color: #e74c3c; font-weight: bold; }
|
|
157
|
-
#awa-pointer, #twa-pointer, #track-pointer, #twd-arrow {
|
|
157
|
+
#awa-pointer, #twa-pointer, #track-pointer, #twd-arrow, #twd-boat-wrap {transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);}
|
|
158
158
|
#leeway-mask-rect { transition: none; }
|
|
159
159
|
.alarm-warning { color: #f1c40f !important; }
|
|
160
160
|
.alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
|