@sailingrotevista/rotevista-dash 2.0.14 → 2.0.16
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 +224 -347
- package/index.html +4 -4
- package/package.json +1 -1
- package/style.css +11 -4
package/app.js
CHANGED
|
@@ -1,33 +1,12 @@
|
|
|
1
1
|
// ==========================================================================
|
|
2
|
-
// 1. CONFIGURAZIONE E DEFAULT
|
|
2
|
+
// 1. CONFIGURAZIONE E DEFAULT
|
|
3
3
|
// ==========================================================================
|
|
4
4
|
let CONFIG = {
|
|
5
|
-
alarms: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
},
|
|
9
|
-
|
|
10
|
-
smoothWindow: 2000,
|
|
11
|
-
longWindow: 60000,
|
|
12
|
-
stabilityTolerance: 2000,
|
|
13
|
-
stabilityThreshold: 0.90,
|
|
14
|
-
minSpeed: 0.5
|
|
15
|
-
},
|
|
16
|
-
graphs: {
|
|
17
|
-
reef1: 15.0,
|
|
18
|
-
reef2: 20.0,
|
|
19
|
-
historyMinutes: 5,
|
|
20
|
-
samples: 60
|
|
21
|
-
},
|
|
22
|
-
scales: {
|
|
23
|
-
stw: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
24
|
-
sog: { stdMax: 12, hercSpan: 4, step: 2 },
|
|
25
|
-
tws: { stdMax: 25, hercSpan: 10, step: 5 },
|
|
26
|
-
depth: { stdMax: 20, hercSpan: 10, step: 10 }
|
|
27
|
-
},
|
|
28
|
-
server: {
|
|
29
|
-
fallbackIp: "192.168.111.240:3000"
|
|
30
|
-
}
|
|
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 },
|
|
8
|
+
scales: { stw: { stdMax: 12, hercSpan: 4, step: 2 }, sog: { stdMax: 12, hercSpan: 4, step: 2 }, tws: { stdMax: 25, hercSpan: 10, step: 5 }, depth: { stdMax: 20, hercSpan: 10, step: 10 } },
|
|
9
|
+
server: { fallbackIp: "192.168.111.240:3000" }
|
|
31
10
|
};
|
|
32
11
|
|
|
33
12
|
const RENDER_INTERVAL_MS = 1000;
|
|
@@ -41,7 +20,14 @@ let simulationMode = false;
|
|
|
41
20
|
let socket, renderInterval, simInterval;
|
|
42
21
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
43
22
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
23
|
+
|
|
24
|
+
// Variabili di stato per filtri e logica tattica
|
|
44
25
|
let smoothedLeeway = 0;
|
|
26
|
+
let rotationTrend = 0;
|
|
27
|
+
let lastShortAvgVal = null;
|
|
28
|
+
let lastInstantTwa = null;
|
|
29
|
+
let isNavigating = false;
|
|
30
|
+
|
|
45
31
|
let pressTimer, isFocusActive = false, blockNextClick = false;
|
|
46
32
|
|
|
47
33
|
const graphModes = {
|
|
@@ -75,34 +61,7 @@ const ui = {
|
|
|
75
61
|
};
|
|
76
62
|
|
|
77
63
|
// ==========================================================================
|
|
78
|
-
// 3.
|
|
79
|
-
// ==========================================================================
|
|
80
|
-
async function fetchServerConfig() {
|
|
81
|
-
if (!window.location.protocol.includes("http")) return;
|
|
82
|
-
const pluginID = 'rotevista-dash';
|
|
83
|
-
const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
|
|
84
|
-
for (let url of possibleUrls) {
|
|
85
|
-
try {
|
|
86
|
-
const response = await fetch(url);
|
|
87
|
-
if (response.ok) {
|
|
88
|
-
const data = await response.json();
|
|
89
|
-
const actual = data.configuration || data;
|
|
90
|
-
if (actual && typeof actual === 'object') {
|
|
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]); } };
|
|
92
|
-
parseNumbers(actual);
|
|
93
|
-
if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
|
|
94
|
-
if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
|
|
95
|
-
if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
|
|
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;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
} catch (e) { }
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ==========================================================================
|
|
105
|
-
// 4. MATEMATICA E GESTIONE DATI
|
|
64
|
+
// 3. MATEMATICA E UTILS
|
|
106
65
|
// ==========================================================================
|
|
107
66
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
108
67
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
@@ -115,84 +74,156 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
|
115
74
|
if (validData.length === 0) return null;
|
|
116
75
|
let sSin = 0, sCos = 0; validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
117
76
|
let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
|
|
118
|
-
let isStable = (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
77
|
+
let isStable = (validData.length > 2) && (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
119
78
|
let avgDeg = Math.round(radToDeg(Math.atan2(sSin, sCos)));
|
|
120
79
|
return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
|
|
121
80
|
}
|
|
122
81
|
|
|
82
|
+
// ==========================================================================
|
|
83
|
+
// 4. LOGICA FISICA (VENTO E SICUREZZA)
|
|
84
|
+
// ==========================================================================
|
|
85
|
+
|
|
86
|
+
function playBingBing() {
|
|
87
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
88
|
+
const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n;
|
|
89
|
+
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); }
|
|
90
|
+
b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function playGybeAlarm() {
|
|
94
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
95
|
+
const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
|
|
96
|
+
function note(f, s, d) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.type = 'square'; o.frequency.value = f; g.gain.setValueAtTime(0.05, s); g.gain.exponentialRampToValueAtTime(0.001, s + d); o.start(s); o.stop(s + d); }
|
|
97
|
+
for (let i = 0; i < 4; i++) { note(1800, audioCtx.currentTime + (i * 0.15), 0.1); note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1); }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkDepthAlarm(m) {
|
|
101
|
+
ui.depth.classList.remove('alarm-warning', 'alarm-danger');
|
|
102
|
+
if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); }
|
|
103
|
+
else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function computeTrueWind() {
|
|
107
|
+
const aws = store.raw["environment.wind.speedApparent"];
|
|
108
|
+
const awa = store.raw["environment.wind.angleApparent"];
|
|
109
|
+
const stw = store.raw["navigation.speedThroughWater"] || 0;
|
|
110
|
+
const sog = store.raw["navigation.speedOverGround"] || 0;
|
|
111
|
+
const hdg = store.raw["navigation.headingTrue"] || 0;
|
|
112
|
+
const cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
113
|
+
if (aws === undefined || awa === undefined) return;
|
|
114
|
+
const tw_water_x = aws * Math.cos(awa) - stw;
|
|
115
|
+
const tw_water_y = aws * Math.sin(awa);
|
|
116
|
+
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
117
|
+
const twa_water = Math.atan2(tw_water_y, tw_water_x);
|
|
118
|
+
const drift_angle = cog - hdg;
|
|
119
|
+
const sog_vec_x = sog * Math.cos(drift_angle);
|
|
120
|
+
const sog_vec_y = sog * Math.sin(drift_angle);
|
|
121
|
+
const tw_ground_x = aws * Math.cos(awa) - sog_vec_x;
|
|
122
|
+
const tw_ground_y = aws * Math.sin(awa) - sog_vec_y;
|
|
123
|
+
const twd_ground = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
126
|
+
store.raw["environment.wind.angleTrueWater"] = twa_water;
|
|
127
|
+
store.raw["environment.wind.directionTrue"] = twd_ground;
|
|
128
|
+
store.smoothBuf.twa.push({ val: twa_water, time: now });
|
|
129
|
+
store.longBuf.twa.push({ val: twa_water, time: now });
|
|
130
|
+
store.smoothBuf.twd.push({ val: twd_ground, time: now });
|
|
131
|
+
store.longBuf.twd.push({ val: twd_ground, time: now });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function updateWindTrend() {
|
|
135
|
+
const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true);
|
|
136
|
+
const shortAvg = getCircularAverageFromBuffer(store.longBuf.twd, 5000, false);
|
|
137
|
+
const instantTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
138
|
+
if (!shortAvg || !twaAvgObj || instantTwaRad === undefined) return;
|
|
139
|
+
const instantTwaDeg = radToDeg(instantTwaRad);
|
|
140
|
+
if (lastShortAvgVal === null) { lastShortAvgVal = shortAvg.val; lastInstantTwa = instantTwaDeg; return; }
|
|
141
|
+
const gybeDetected = (Math.abs(instantTwaDeg) > 150 && Math.abs(lastInstantTwa) > 150 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
|
|
142
|
+
lastInstantTwa = instantTwaDeg;
|
|
143
|
+
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
144
|
+
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
145
|
+
if (gybeDetected && isNavigating) {
|
|
146
|
+
playGybeAlarm();
|
|
147
|
+
[compassDots.cw, compassDots.ccw, gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.add('is-gybing'); el.classList.remove('is-trending'); el.setAttribute('fill', '#ff0000'); }});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let diff = (shortAvg.val - lastShortAvgVal + 540) % 360 - 180;
|
|
151
|
+
lastShortAvgVal = shortAvg.val;
|
|
152
|
+
rotationTrend = (rotationTrend * 0.95) + (diff * 0.05);
|
|
153
|
+
if (Math.abs(rotationTrend) > 1.0) {
|
|
154
|
+
const pos = store.raw["navigation.position"];
|
|
155
|
+
const isSouthernHemisphere = pos && pos.latitude < 0;
|
|
156
|
+
let meteoColor = (!isSouthernHemisphere) ? (rotationTrend < 0 ? "#00ff00" : "#ff0000") : (rotationTrend > 0 ? "#00ff00" : "#ff0000");
|
|
157
|
+
let isLift = (twaAvgObj.val > 0) ? (rotationTrend > 0) : (rotationTrend < 0);
|
|
158
|
+
const tacticColor = isLift ? "#00ff00" : "#ff0000";
|
|
159
|
+
if (rotationTrend > 0) {
|
|
160
|
+
if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
|
|
161
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
|
|
162
|
+
[compassDots.ccw, gaugeDots.ccw].forEach(el => { if(el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
|
|
163
|
+
} else {
|
|
164
|
+
if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
|
|
165
|
+
if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor); }
|
|
166
|
+
[compassDots.cw, gaugeDots.cw].forEach(el => { if(el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
[compassDots.cw, compassDots.ccw, gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ==========================================================================
|
|
174
|
+
// 5. DATA ROUTING
|
|
175
|
+
// ==========================================================================
|
|
123
176
|
function processIncomingData(path, val) {
|
|
124
177
|
const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
|
|
178
|
+
if (path === "navigation.position") store.raw["navigation.position"] = val;
|
|
179
|
+
if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
|
|
180
|
+
if (path === "environment.wind.speedApparent" || path === "environment.wind.angleApparent" || path === "navigation.speedThroughWater") computeTrueWind();
|
|
125
181
|
if (path === "navigation.headingTrue") { store.smoothBuf.hdg.push({ val: val, time: now }); store.longBuf.hdg.push({ val: val, time: now }); }
|
|
126
182
|
if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
|
|
127
|
-
if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
|
|
128
|
-
if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
|
|
129
|
-
if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
|
|
130
|
-
let twdRad = 0; if (path === "environment.wind.directionTrue") twdRad = val;
|
|
131
|
-
else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) {
|
|
132
|
-
twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
|
|
133
|
-
if (twdRad < 0) twdRad += (2 * Math.PI);
|
|
134
|
-
} else return;
|
|
135
|
-
store.smoothBuf.twd.push({ val: twdRad, time: now }); store.longBuf.twd.push({ val: twdRad, time: now });
|
|
136
|
-
}
|
|
137
183
|
}
|
|
138
184
|
|
|
139
185
|
// ==========================================================================
|
|
140
|
-
//
|
|
186
|
+
// 6. MOTORE RENDERING UI
|
|
141
187
|
// ==========================================================================
|
|
188
|
+
function updateLeewayDisplay(deg) { const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125); ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w); ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`; }
|
|
189
|
+
|
|
142
190
|
function startDisplayLoop() {
|
|
143
191
|
renderInterval = setInterval(() => {
|
|
144
192
|
const now = Date.now();
|
|
145
193
|
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 };
|
|
146
|
-
for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { pathsToWatch[p]
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
194
|
+
for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { if (pathsToWatch[p] === ui.awsSvg) ui.awsSvg.textContent = "---"; else pathsToWatch[p].innerText = "---"; delete store.raw[p]; } }
|
|
195
|
+
|
|
196
|
+
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
|
|
197
|
+
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
198
|
+
isNavigating = stwKts > CONFIG.averages.minSpeed || sogKts > CONFIG.averages.minSpeed;
|
|
199
|
+
|
|
200
|
+
if (store.raw["navigation.speedThroughWater"] !== undefined) { ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts); }
|
|
201
|
+
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
202
|
+
ui.sog.innerText = sogKts.toFixed(1);
|
|
203
|
+
let sogColor = "#fff"; const currentEffect = sogKts - stwKts;
|
|
204
|
+
if (currentEffect > 0.3) sogColor = "#2ecc71"; else if (currentEffect < -0.3) sogColor = "#e74c3c";
|
|
205
|
+
ui.sog.style.color = sogColor; manageHistory('sog', sogKts);
|
|
206
|
+
}
|
|
150
207
|
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); }
|
|
151
|
-
|
|
152
|
-
// --- 1. RENDERING TWS CON COLORE DINAMICO (BIANCO -> ARANCIO -> ROSSO) ---
|
|
153
208
|
if (store.raw["environment.wind.speedTrue"] !== undefined) {
|
|
154
|
-
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
|
|
155
|
-
ui.tws.
|
|
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;
|
|
209
|
+
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = twsKts.toFixed(1);
|
|
210
|
+
ui.tws.style.color = (twsKts >= CONFIG.graphs.reef2) ? "#e74c3c" : (twsKts >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff");
|
|
160
211
|
manageHistory('tws', twsKts);
|
|
161
212
|
}
|
|
162
213
|
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
163
214
|
|
|
164
|
-
// Render Quadrante
|
|
165
215
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
|
|
166
216
|
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, smAwa.val); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
167
217
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
168
218
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
169
219
|
|
|
170
220
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Se la barca è ferma, azzera brutalmente. Altrimenti filtra il valore.
|
|
177
|
-
if (curSog < CONFIG.averages.minSpeed) {
|
|
178
|
-
smoothedLeeway = 0;
|
|
179
|
-
} else {
|
|
180
|
-
// Smoothing: 90% valore precedente, 10% nuovo valore
|
|
181
|
-
smoothedLeeway = (smoothedLeeway * 0.9) + (rawDrift * 0.1);
|
|
182
|
-
}
|
|
221
|
+
let drift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 540) % 360 - 180;
|
|
222
|
+
if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (drift * 0.1);
|
|
223
|
+
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
224
|
+
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
225
|
+
} else updateLeewayDisplay(0);
|
|
183
226
|
|
|
184
|
-
// 3. Renderizziamo il valore filtrato
|
|
185
|
-
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
186
|
-
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
187
|
-
|
|
188
|
-
// Limita il valore grafico e aggiorna il testo
|
|
189
|
-
let ds = Math.max(-20, Math.min(20, smoothedLeeway));
|
|
190
|
-
updateLeewayDisplay(ds);
|
|
191
|
-
} else {
|
|
192
|
-
updateLeewayDisplay(0);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// --- 2. RENDERING MEDIE E BUSSOLA TATTICA ---
|
|
196
227
|
if (now - lastAvgUIUpdate > 3000) {
|
|
197
228
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
|
|
198
229
|
cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
|
|
@@ -201,56 +232,19 @@ function startDisplayLoop() {
|
|
|
201
232
|
twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
|
|
202
233
|
|
|
203
234
|
const upUI = (el, obj, isCompass = false) => {
|
|
204
|
-
if (!obj || obj.val === null) {
|
|
205
|
-
|
|
206
|
-
el.
|
|
207
|
-
|
|
208
|
-
let val = Math.round(obj.val);
|
|
209
|
-
let displayVal;
|
|
210
|
-
|
|
211
|
-
if (isCompass) {
|
|
212
|
-
// Formattazione per HEADING, COG, TWD (0-360°)
|
|
213
|
-
displayVal = ((val + 360) % 360).toString().padStart(3, '0');
|
|
214
|
-
} else {
|
|
215
|
-
// Formattazione per AWA, TWA (-180 a 180°)
|
|
216
|
-
// Se è negativo, il segno viene mantenuto correttamente
|
|
217
|
-
displayVal = val.toString();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
el.innerHTML = `${displayVal}°`;
|
|
221
|
-
|
|
222
|
-
if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data');
|
|
223
|
-
else el.classList.add('unstable-data');
|
|
235
|
+
if (!obj || obj.val === null) { el.innerHTML = "---°"; el.classList.remove('unstable-data'); }
|
|
236
|
+
else {
|
|
237
|
+
let val = Math.round(obj.val); el.innerHTML = (isCompass ? ((val + 360) % 360).toString().padStart(3, '0') : val) + "°";
|
|
238
|
+
if (obj.stable || !isNavigating) el.classList.remove('unstable-data'); else el.classList.add('unstable-data');
|
|
224
239
|
}
|
|
225
240
|
};
|
|
226
|
-
|
|
227
|
-
upUI(ui.hdg, hObj, true); // Bussola
|
|
228
|
-
upUI(ui.cog, cObj, true); // Bussola
|
|
229
|
-
upUI(ui.awaAvg, awObj, false); // Angolo
|
|
230
|
-
upUI(ui.twaAvg, twObj, false); // Angolo
|
|
231
|
-
upUI(ui.twdAvg, twdObj, true); // Bussola (La direzione del vento è sempre 0-360)
|
|
232
|
-
|
|
241
|
+
upUI(ui.hdg, hObj, true); upUI(ui.cog, cObj, true); upUI(ui.awaAvg, awObj, false); upUI(ui.twaAvg, twObj, false); upUI(ui.twdAvg, twdObj, true);
|
|
233
242
|
if (hObj && twObj && hObj.val !== null) {
|
|
234
|
-
|
|
243
|
+
const tA = twObj.val * 2;
|
|
244
|
+
ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
235
245
|
if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
236
|
-
let tStable = (hObj.stable && twObj.stable) || (curSog < CONFIG.averages.minSpeed);
|
|
237
|
-
if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
|
|
238
|
-
else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Rotazione Bussola Tattica e Colore Sincronizzato
|
|
242
|
-
if (twdObj && hObj) {
|
|
243
|
-
updateWindTrend();
|
|
244
|
-
curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val);
|
|
245
|
-
ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`);
|
|
246
|
-
ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`);
|
|
247
|
-
|
|
248
|
-
let currentTwsKts = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
249
|
-
let reefColor = "#fff";
|
|
250
|
-
if (currentTwsKts >= CONFIG.graphs.reef2) reefColor = "#e74c3c";
|
|
251
|
-
else if (currentTwsKts >= CONFIG.graphs.reef1) reefColor = "#e67e22";
|
|
252
|
-
if (ui.twdChevron) ui.twdChevron.setAttribute('stroke', reefColor);
|
|
253
246
|
}
|
|
247
|
+
if (twdObj && hObj) { updateWindTrend(); curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`); }
|
|
254
248
|
lastAvgUIUpdate = now;
|
|
255
249
|
}
|
|
256
250
|
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(); }
|
|
@@ -259,251 +253,134 @@ function startDisplayLoop() {
|
|
|
259
253
|
}
|
|
260
254
|
|
|
261
255
|
// ==========================================================================
|
|
262
|
-
//
|
|
256
|
+
// 7. CONFIG E GRAFICI
|
|
263
257
|
// ==========================================================================
|
|
264
|
-
function
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
258
|
+
async function fetchServerConfig() {
|
|
259
|
+
if (!window.location.protocol.includes("http")) return;
|
|
260
|
+
const pluginID = 'rotevista-dash';
|
|
261
|
+
const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
|
|
262
|
+
for (let url of possibleUrls) {
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(url);
|
|
265
|
+
if (response.ok) {
|
|
266
|
+
const data = await response.json();
|
|
267
|
+
const actual = data.configuration || data;
|
|
268
|
+
if (actual && typeof actual === 'object') {
|
|
269
|
+
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]); } };
|
|
270
|
+
parseNumbers(actual);
|
|
271
|
+
if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
|
|
272
|
+
if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
|
|
273
|
+
if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
|
|
274
|
+
if (actual.scales) { for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; } }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch (e) { }
|
|
278
|
+
}
|
|
273
279
|
}
|
|
274
280
|
|
|
275
|
-
// ==========================================================================
|
|
276
|
-
// 7. FUNZIONI GRAFICHE E DISEGNO (BIANCO -> ARANCIO -> ROSSO)
|
|
277
|
-
// ==========================================================================
|
|
278
|
-
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)}°`; }
|
|
279
|
-
|
|
280
281
|
function manageHistory(t, v) {
|
|
281
|
-
const n = Date.now();
|
|
282
|
-
const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
282
|
+
const n = Date.now(); const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
283
283
|
if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) { store.histories[t].push(v); if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift(); store.lastUpdates[t] = n; }
|
|
284
284
|
const mode = graphModes[t], cfg = calculateScale(t, store.histories[t], mode);
|
|
285
|
-
|
|
286
|
-
if (mode === 'hercules') box.classList.add('box-hercules'); else box.classList.remove('box-hercules');
|
|
287
|
-
updateScaleLabels(t, cfg.min, cfg.max);
|
|
288
|
-
drawGraph(store.histories[t], t + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
285
|
+
updateScaleLabels(t, cfg.min, cfg.max); drawGraph(store.histories[t], t + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
289
286
|
}
|
|
290
287
|
|
|
291
288
|
function calculateScale(type, data, mode) {
|
|
292
|
-
const s = CONFIG.scales[type] || { stdMax: 12, hercSpan: 4, step: 2 };
|
|
293
|
-
let aMin = Math.
|
|
294
|
-
|
|
295
|
-
let avg = (aMin + aMax) / 2; let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin)); if (span % 2 !== 0) span += 1;
|
|
296
|
-
let min = Math.max(0, Math.floor(avg - (span / 2))); return { min, max: min + span };
|
|
297
|
-
} else return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
|
|
289
|
+
const s = CONFIG.scales[type] || { stdMax: 12, hercSpan: 4, step: 2 }; let aMin = Math.min(...data), aMax = Math.max(...data);
|
|
290
|
+
if (mode === 'hercules') { let avg = (aMin + aMax) / 2; let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin)); if (span % 2 !== 0) span += 1; let min = Math.max(0, Math.floor(avg - (span / 2))); return { min, max: min + span }; }
|
|
291
|
+
else return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
|
|
298
292
|
}
|
|
299
293
|
|
|
300
|
-
function updateScaleLabels(t, min, max) {
|
|
301
|
-
const el = document.getElementById(t + '-scale'); if (!el) return;
|
|
302
|
-
el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`;
|
|
303
|
-
}
|
|
294
|
+
function updateScaleLabels(t, min, max) { const el = document.getElementById(t + '-scale'); if (el) el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`; }
|
|
304
295
|
|
|
305
296
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
306
297
|
const svg = document.getElementById(id); if (!svg || d.length < 2) return;
|
|
307
298
|
const w = 200, h = 40, range = max - min || 1;
|
|
308
299
|
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" />`; });
|
|
309
300
|
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" />`; }
|
|
310
|
-
let pD = ""
|
|
301
|
+
let pD = ""; let cS = "";
|
|
311
302
|
d.forEach((v, i) => {
|
|
312
303
|
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} `;
|
|
313
304
|
if (isTws && i > 0) {
|
|
314
305
|
const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
|
|
315
|
-
let c = "#
|
|
316
|
-
if (v >= CONFIG.graphs.reef2) c = "#e74c3c"; else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
|
|
306
|
+
let c = (v >= CONFIG.graphs.reef2) ? "#e74c3c" : (v >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff");
|
|
317
307
|
cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
|
|
318
308
|
}
|
|
319
309
|
});
|
|
320
|
-
const
|
|
321
|
-
svg.innerHTML = isTws ? `${grids}<path d="${
|
|
310
|
+
const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#ffffff' };
|
|
311
|
+
svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(255,255,255,0.08)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${clrs[id]}" />`;
|
|
322
312
|
}
|
|
323
313
|
|
|
324
314
|
// ==========================================================================
|
|
325
|
-
// 8.
|
|
315
|
+
// 8. INTERAZIONI
|
|
326
316
|
// ==========================================================================
|
|
327
317
|
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
328
318
|
|
|
329
319
|
function toggleFocusMode(type, element) {
|
|
330
|
-
const container = document.querySelector('.main-container');
|
|
331
|
-
const parentPanel = element.closest('.side-panel');
|
|
332
|
-
const isLeft = parentPanel.classList.contains('left-panel');
|
|
320
|
+
const container = document.querySelector('.main-container'); const parentPanel = element.closest('.side-panel'); const isLeft = parentPanel.classList.contains('left-panel');
|
|
333
321
|
isFocusActive = !isFocusActive;
|
|
334
|
-
if (isFocusActive) {
|
|
335
|
-
|
|
336
|
-
container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right');
|
|
337
|
-
parentPanel.classList.add('has-focus');
|
|
338
|
-
element.classList.add('is-focused');
|
|
339
|
-
blockNextClick = true;
|
|
340
|
-
} else {
|
|
341
|
-
container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
|
|
342
|
-
document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus'));
|
|
343
|
-
document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
|
|
344
|
-
}
|
|
322
|
+
if (isFocusActive) { container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right'); parentPanel.classList.add('has-focus'); element.classList.add('is-focused'); }
|
|
323
|
+
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')); }
|
|
345
324
|
}
|
|
346
325
|
|
|
347
326
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
348
|
-
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const handleInteraction = (e) => {
|
|
352
|
-
if (e.cancelable) e.preventDefault();
|
|
353
|
-
const currentTime = new Date().getTime();
|
|
354
|
-
const tapDelay = currentTime - lastTapTime;
|
|
355
|
-
|
|
327
|
+
const el = document.getElementById(type + '-graph').closest('.data-box'); let lastTapTime = 0, tapTimeout;
|
|
328
|
+
el.addEventListener('pointerdown', (e) => {
|
|
329
|
+
if (e.cancelable) e.preventDefault(); const currentTime = new Date().getTime(); const tapDelay = currentTime - lastTapTime;
|
|
356
330
|
pressTimer = setTimeout(() => { if (!isFocusActive) { toggleFocusMode(type, el); lastTapTime = 0; } }, 1000);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
|
|
362
|
-
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
363
|
-
el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
|
|
364
|
-
}
|
|
365
|
-
lastTapTime = 0;
|
|
366
|
-
} else {
|
|
367
|
-
lastTapTime = currentTime;
|
|
368
|
-
tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); }, 350);
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
el.addEventListener('pointerdown', handleInteraction);
|
|
372
|
-
el.addEventListener('pointerup', () => clearTimeout(pressTimer));
|
|
373
|
-
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
331
|
+
if (tapDelay < 300 && tapDelay > 0) { clearTimeout(tapTimeout); if (!isFocusActive) { graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); } lastTapTime = 0; }
|
|
332
|
+
else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); }, 350); }
|
|
333
|
+
});
|
|
334
|
+
el.addEventListener('pointerup', () => clearTimeout(pressTimer)); el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
374
335
|
});
|
|
375
336
|
|
|
376
|
-
// ==========================================================================
|
|
377
|
-
// GESTIONE HOTSPOT: Click (Fullscreen) e Long Press (Night Mode)
|
|
378
|
-
// ==========================================================================
|
|
379
337
|
if (ui.hotspot) {
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
ui.hotspot.addEventListener('pointerdown', (e) => {
|
|
384
|
-
// Avvia il timer al tocco
|
|
385
|
-
pressTimer = setTimeout(() => {
|
|
386
|
-
document.body.classList.toggle('night-mode');
|
|
387
|
-
// Feedback visivo immediato al cambio modalità
|
|
388
|
-
ui.hotspot.style.opacity = "0.5";
|
|
389
|
-
setTimeout(() => ui.hotspot.style.opacity = "1", 200);
|
|
390
|
-
pressTimer = null; // Reset per evitare che il click scatti dopo
|
|
391
|
-
}, HOLD_DURATION);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
ui.hotspot.addEventListener('pointerup', (e) => {
|
|
395
|
-
if (pressTimer) {
|
|
396
|
-
clearTimeout(pressTimer);
|
|
397
|
-
pressTimer = null;
|
|
398
|
-
// Se arriviamo qui, è stato un click rapido -> Fullscreen
|
|
399
|
-
const doc = document.documentElement;
|
|
400
|
-
const isF = document.fullscreenElement || document.webkitFullscreenElement;
|
|
401
|
-
if (!isF) {
|
|
402
|
-
if (doc.requestFullscreen) doc.requestFullscreen();
|
|
403
|
-
else if (doc.webkitRequestFullscreen) doc.webkitRequestFullscreen();
|
|
404
|
-
} else {
|
|
405
|
-
if (document.exitFullscreen) document.exitFullscreen();
|
|
406
|
-
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
ui.hotspot.addEventListener('pointerleave', () => {
|
|
412
|
-
if (pressTimer) {
|
|
413
|
-
clearTimeout(pressTimer);
|
|
414
|
-
pressTimer = null;
|
|
415
|
-
}
|
|
416
|
-
});
|
|
338
|
+
ui.hotspot.addEventListener('pointerdown', (e) => { pressTimer = setTimeout(() => { document.body.classList.toggle('night-mode'); ui.hotspot.style.opacity = "0.5"; setTimeout(() => ui.hotspot.style.opacity = "1", 200); pressTimer = null; }, 1000); });
|
|
339
|
+
ui.hotspot.addEventListener('pointerup', (e) => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; 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(); } } });
|
|
340
|
+
ui.hotspot.addEventListener('pointerleave', () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } });
|
|
417
341
|
}
|
|
418
|
-
(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); } } })();
|
|
419
|
-
|
|
420
|
-
async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
|
|
421
|
-
window.addEventListener('load', init);
|
|
422
|
-
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'); }
|
|
423
|
-
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); }
|
|
424
|
-
|
|
425
|
-
// Simulatore su Depth (3 click rapidi)
|
|
426
|
-
ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
427
|
-
let dC = 0, lC = 0;
|
|
428
|
-
return function() {
|
|
429
|
-
const n = Date.now();
|
|
430
|
-
if (n - lC < 500) dC++; else dC = 1;
|
|
431
|
-
lC = n;
|
|
432
|
-
if (dC === 3) {
|
|
433
|
-
simulationMode = !simulationMode;
|
|
434
|
-
if (simulationMode) {
|
|
435
|
-
if (socket) socket.close();
|
|
436
|
-
startDynamicSimulation(); // Chiama la nuova funzione
|
|
437
|
-
} else {
|
|
438
|
-
location.reload();
|
|
439
|
-
}
|
|
440
|
-
dC = 0;
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
})());
|
|
444
342
|
|
|
445
343
|
// ==========================================================================
|
|
446
|
-
// 9.
|
|
344
|
+
// 9. CONNESSIONE E SIMULATORE
|
|
447
345
|
// ==========================================================================
|
|
448
|
-
function
|
|
449
|
-
|
|
346
|
+
function connect() {
|
|
347
|
+
if (simulationMode) return;
|
|
348
|
+
let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
|
|
349
|
+
try {
|
|
350
|
+
socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
|
|
351
|
+
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; };
|
|
352
|
+
socket.onmessage = (e) => { if (simulationMode) return; const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
|
|
353
|
+
socket.onclose = () => !simulationMode && setTimeout(connect, 5000);
|
|
354
|
+
} catch (e) { setTimeout(connect, 5000); }
|
|
355
|
+
}
|
|
450
356
|
|
|
451
|
-
|
|
452
|
-
let sim = {
|
|
453
|
-
hdg: Math.random() * 360,
|
|
454
|
-
tws: 12,
|
|
455
|
-
twd: Math.random() * 360,
|
|
456
|
-
depth: 12,
|
|
457
|
-
stw: 5,
|
|
458
|
-
leeway: 0, // Inerzia per il Leeway
|
|
459
|
-
startTime: Date.now()
|
|
460
|
-
};
|
|
357
|
+
ui.depth.closest('.data-box').addEventListener('click', (function() { let dC = 0, lC = 0; return function() { const n = Date.now(); if (n - lC < 500) dC++; else dC = 1; lC = n; if (dC === 3) { simulationMode = !simulationMode; if (simulationMode) { if (socket) socket.close(); startDynamicSimulation(); } else location.reload(); dC = 0; } }; })());
|
|
461
358
|
|
|
359
|
+
function startDynamicSimulation() {
|
|
360
|
+
ui.status.innerText = "SIM ATTIVO";
|
|
361
|
+
let sim = { hdg: 45, tws: 12, twd: 45, depth: 12, stw: 5, leeway: 0, startTime: Date.now() };
|
|
462
362
|
simInterval = setInterval(() => {
|
|
463
363
|
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
const targetTws = (inGust ? 18 : 10) + Math.sin(elapsed / 10) * 2;
|
|
473
|
-
sim.tws += (targetTws - sim.tws) * 0.05; // Smoothing lento
|
|
474
|
-
|
|
475
|
-
// 4. POLARE SEMPLIFICATA (STW in base al TWA)
|
|
476
|
-
let twaRel = (sim.twd - sim.hdg + 360) % 360;
|
|
477
|
-
if (twaRel > 180) twaRel -= 360;
|
|
478
|
-
|
|
479
|
-
let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125));
|
|
480
|
-
sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05; // Smoothing STW
|
|
481
|
-
|
|
482
|
-
// 5. CALCOLO LEEWAY CON FILTRO (Inerzia)
|
|
483
|
-
const rawLeeway = Math.sin(degToRad(twaRel)) * 4;
|
|
484
|
-
sim.leeway += (rawLeeway - sim.leeway) * 0.05; // Smoothing 0.05 per evitare i balli di +/- 3.1
|
|
485
|
-
|
|
486
|
-
// 6. CALCOLO VETTORIALE COG
|
|
487
|
-
const cog = (sim.hdg - sim.leeway + 360) % 360;
|
|
488
|
-
|
|
489
|
-
// 7. CALCOLO VENTO APPARENTE (AWS/AWA)
|
|
490
|
-
const twaRad = degToRad(twaRel);
|
|
364
|
+
if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
|
|
365
|
+
sim.twd = (sim.twd + (Math.sin(elapsed / 20) * 0.5) + 360) % 360;
|
|
366
|
+
const inGust = (elapsed % 40) < 5; const targetTws = (inGust ? 18 : 10) + Math.sin(elapsed / 10) * 2;
|
|
367
|
+
sim.tws += (targetTws - sim.tws) * 0.05; sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
|
|
368
|
+
let twaRel = (sim.twd - sim.hdg + 360) % 360; if (twaRel > 180) twaRel -= 360;
|
|
369
|
+
let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125)); sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
|
|
370
|
+
const rawLeeway = Math.sin(degToRad(twaRel)) * 4; sim.leeway += (rawLeeway - sim.leeway) * 0.05;
|
|
371
|
+
const cog = (sim.hdg - sim.leeway + 360) % 360; const twaRad = degToRad(twaRel);
|
|
491
372
|
const aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
|
|
492
|
-
const awa =
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
processIncomingData("
|
|
496
|
-
processIncomingData("
|
|
497
|
-
processIncomingData("environment.wind.angleTrueWater", degToRad(twaRel));
|
|
498
|
-
processIncomingData("environment.wind.speedApparent", ktsToMs(aws));
|
|
499
|
-
processIncomingData("environment.wind.angleApparent", degToRad(awa));
|
|
500
|
-
processIncomingData("environment.depth.belowTransducer", sim.depth);
|
|
501
|
-
processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
502
|
-
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw));
|
|
503
|
-
processIncomingData("navigation.speedOverGround", ktsToMs(sim.stw));
|
|
504
|
-
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
|
|
505
|
-
|
|
506
|
-
// Aggiorna display grafico
|
|
507
|
-
updateLeewayDisplay(sim.leeway);
|
|
373
|
+
const awa = Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad));
|
|
374
|
+
processIncomingData("environment.wind.speedApparent", ktsToMs(aws)); processIncomingData("environment.wind.angleApparent", awa);
|
|
375
|
+
processIncomingData("environment.depth.belowTransducer", sim.depth); processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
376
|
+
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw)); processIncomingData("navigation.speedOverGround", ktsToMs(sim.stw));
|
|
377
|
+
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog)); processIncomingData("navigation.position", { latitude: 45.0, longitude: 12.0 });
|
|
508
378
|
}, 1000);
|
|
509
379
|
}
|
|
380
|
+
|
|
381
|
+
// ==========================================================================
|
|
382
|
+
// 10. INIT
|
|
383
|
+
// ==========================================================================
|
|
384
|
+
(function genTicks() { const c = document.getElementById('ticks'); if (c) { for (let i = 0; i < 360; i += 10) { const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50)); l.setAttribute("stroke", m ? "#fff" : "#666"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
|
|
385
|
+
async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
|
|
386
|
+
window.addEventListener('load', init);
|
package/index.html
CHANGED
|
@@ -179,8 +179,8 @@
|
|
|
179
179
|
<g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
|
|
180
180
|
<path d="M200,90 L206,98 L200,125 L194,98 Z" fill="#ffff00" stroke="#000" stroke-width="0.8" />
|
|
181
181
|
<text x="200" y="104" fill="#000" font-size="8" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
|
|
182
|
-
<circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#
|
|
183
|
-
<circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#
|
|
182
|
+
<circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#ffffff" opacity="0.3" />
|
|
183
|
+
<circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#ffffff" opacity="0.3" />
|
|
184
184
|
</g>
|
|
185
185
|
|
|
186
186
|
<!-- Barra LEEWAY / SCARROCCIO Inferiore -->
|
|
@@ -291,8 +291,8 @@
|
|
|
291
291
|
stroke-width="2.2"
|
|
292
292
|
stroke-linecap="round"
|
|
293
293
|
stroke-linejoin="round" />
|
|
294
|
-
<circle id="trend-dot-cw" cx="24" cy="7.5" r="1.5" fill="#
|
|
295
|
-
<circle id="trend-dot-ccw" cx="16" cy="7.5" r="1.5" fill="#
|
|
294
|
+
<circle id="trend-dot-cw" cx="24" cy="7.5" r="1.5" fill="#ffffff" opacity="0.3" />
|
|
295
|
+
<circle id="trend-dot-ccw" cx="16" cy="7.5" r="1.5" fill="#ffffff" opacity="0.3" />
|
|
296
296
|
</g>
|
|
297
297
|
</svg>
|
|
298
298
|
<span class="value value-large" id="twd-avg">---°</span>
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -30,19 +30,19 @@ body {
|
|
|
30
30
|
I lati hanno un minimo vitale (180px) per proteggere i testi.
|
|
31
31
|
Il centro (auto) si adatta millimetricamente al diametro dell'SVG.
|
|
32
32
|
*/
|
|
33
|
-
grid-template-columns: minmax(180px, 1fr) minmax(auto,
|
|
33
|
+
grid-template-columns: minmax(180px, 1fr) minmax(auto, 1.5fr) minmax(180px, 1fr);
|
|
34
34
|
grid-template-rows: 100%;
|
|
35
35
|
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
36
36
|
justify-content: stretch;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/* Espansione colonne per schermi molto larghi (es. 16:10 / 16:9)
|
|
39
|
+
/* Espansione colonne per schermi molto larghi (es. 16:10 / 16:9)
|
|
40
40
|
@media (min-aspect-ratio: 1.5) {
|
|
41
41
|
.main-container {
|
|
42
|
-
grid-template-columns: minmax(200px, 1fr) auto minmax(200px, 1fr);
|
|
42
|
+
grid-template-columns: minmax(200px, 1fr) minmax(auto, 3fr) minmax(200px, 1fr);
|
|
43
43
|
gap: 15px;
|
|
44
44
|
}
|
|
45
|
-
}
|
|
45
|
+
}*/
|
|
46
46
|
|
|
47
47
|
/* ==========================================================================
|
|
48
48
|
2. PANNELLI E DATA-BOX
|
|
@@ -478,3 +478,10 @@ body.night-mode {
|
|
|
478
478
|
.is-trending {
|
|
479
479
|
animation: blink-trend 1s infinite !important;
|
|
480
480
|
}
|
|
481
|
+
|
|
482
|
+
/* Allarme Strambata: Rosso fisso con bagliore neon */
|
|
483
|
+
.is-gybing {
|
|
484
|
+
opacity: 1 !important;
|
|
485
|
+
animation: none !important; /* Rimuove il lampeggio */
|
|
486
|
+
filter: drop-shadow(0 0 8px #ff0000) !important;
|
|
487
|
+
}
|