@sailingrotevista/rotevista-dash 2.0.15 → 2.0.17
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 +356 -367
- package/index.html +37 -83
- package/package.json +1 -1
- package/style.css +82 -248
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;
|
|
@@ -38,14 +17,20 @@ const SIM_SAMPLE_INTERVAL = 1000;
|
|
|
38
17
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
39
18
|
// ==========================================================================
|
|
40
19
|
let simulationMode = false;
|
|
20
|
+
let displayModeSog = 'SOG'; // 'SOG' o 'VMG'
|
|
41
21
|
let socket, renderInterval, simInterval;
|
|
42
22
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
43
23
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
24
|
+
let curBoatCompassRot = 0, curWindCompassRot = 0;
|
|
25
|
+
|
|
26
|
+
let smoothedLeeway = 0, rotationTrend = 0;
|
|
27
|
+
let lastShortAvgVal = null, lastInstantTwa = null;
|
|
28
|
+
let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
|
|
29
|
+
let twDirty = false;
|
|
30
|
+
let reconnectDelay = 1000;
|
|
31
|
+
let isNavigating = false;
|
|
44
32
|
|
|
45
|
-
|
|
46
|
-
let smoothedLeeway = 0;
|
|
47
|
-
let rotationTrend = 0;
|
|
48
|
-
let pressTimer, isFocusActive = false, blockNextClick = false;
|
|
33
|
+
let pressTimer, isFocusActive = false;
|
|
49
34
|
|
|
50
35
|
const graphModes = {
|
|
51
36
|
stw: localStorage.getItem('mode_stw') || 'standard',
|
|
@@ -58,8 +43,8 @@ const store = {
|
|
|
58
43
|
raw: {}, timestamps: {},
|
|
59
44
|
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
60
45
|
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
61
|
-
histories: { stw: [], sog: [], depth: [], tws: [] },
|
|
62
|
-
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 }
|
|
46
|
+
histories: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
|
|
47
|
+
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0 }
|
|
63
48
|
};
|
|
64
49
|
|
|
65
50
|
const ui = {
|
|
@@ -78,34 +63,7 @@ const ui = {
|
|
|
78
63
|
};
|
|
79
64
|
|
|
80
65
|
// ==========================================================================
|
|
81
|
-
// 3.
|
|
82
|
-
// ==========================================================================
|
|
83
|
-
async function fetchServerConfig() {
|
|
84
|
-
if (!window.location.protocol.includes("http")) return;
|
|
85
|
-
const pluginID = 'rotevista-dash';
|
|
86
|
-
const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
|
|
87
|
-
for (let url of possibleUrls) {
|
|
88
|
-
try {
|
|
89
|
-
const response = await fetch(url);
|
|
90
|
-
if (response.ok) {
|
|
91
|
-
const data = await response.json();
|
|
92
|
-
const actual = data.configuration || data;
|
|
93
|
-
if (actual && typeof actual === 'object') {
|
|
94
|
-
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]); } };
|
|
95
|
-
parseNumbers(actual);
|
|
96
|
-
if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
|
|
97
|
-
if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
|
|
98
|
-
if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
|
|
99
|
-
if (actual.scales) { for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; } }
|
|
100
|
-
console.log("SUCCESS: Config loaded."); return;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
} catch (e) { }
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ==========================================================================
|
|
108
|
-
// 4. MATEMATICA E GESTIONE DATI
|
|
66
|
+
// 3. UTILITIES DI SISTEMA (MATEMATICA E BUFFER)
|
|
109
67
|
// ==========================================================================
|
|
110
68
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
111
69
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
@@ -113,116 +71,212 @@ function msToKts(ms) { return ms * 1.94384; }
|
|
|
113
71
|
function ktsToMs(kts) { return kts / 1.94384; }
|
|
114
72
|
function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
|
|
115
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Gestione sicura dei buffer per prevenire memory leak.
|
|
76
|
+
*/
|
|
77
|
+
function safePush(buffer, val, time, maxLen = 200) {
|
|
78
|
+
buffer.push({ val: val, time: time });
|
|
79
|
+
if (buffer.length > maxLen) { buffer.shift(); }
|
|
80
|
+
}
|
|
81
|
+
|
|
116
82
|
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
117
|
-
const now = Date.now();
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
|
|
118
85
|
if (validData.length === 0) return null;
|
|
119
|
-
let sSin = 0, sCos = 0;
|
|
86
|
+
let sSin = 0, sCos = 0;
|
|
87
|
+
validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
120
88
|
let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
|
|
121
|
-
let isStable = (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
122
|
-
let
|
|
123
|
-
return { val: signed ?
|
|
89
|
+
let isStable = (validData.length > 2) && (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
90
|
+
let avgRad = Math.atan2(sSin, sCos);
|
|
91
|
+
return { val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI), stable: isStable };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calcola il Vento Reale (Acqua e Terra) partendo dagli Apparenti.
|
|
96
|
+
*/
|
|
97
|
+
function computeTrueWind() {
|
|
98
|
+
const aws = store.raw["environment.wind.speedApparent"];
|
|
99
|
+
let awa = store.raw["environment.wind.angleApparent"];
|
|
100
|
+
const stw = store.raw["navigation.speedThroughWater"] || 0, sog = store.raw["navigation.speedOverGround"] || 0;
|
|
101
|
+
const hdg = store.raw["navigation.headingTrue"] || 0, cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
102
|
+
|
|
103
|
+
if (aws === undefined || awa === undefined) return;
|
|
104
|
+
if (awa > Math.PI) awa -= 2 * Math.PI;
|
|
105
|
+
|
|
106
|
+
// Logica Acqua (TWA/TWS)
|
|
107
|
+
const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
|
|
108
|
+
const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y), twa_water = Math.atan2(tw_water_y, tw_water_x);
|
|
109
|
+
|
|
110
|
+
// Logica Terra (TWD) con gestione deriva normalizzata
|
|
111
|
+
const drift_angle = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
|
|
112
|
+
const sog_vec_x = sog * Math.cos(drift_angle), sog_vec_y = sog * Math.sin(drift_angle);
|
|
113
|
+
const tw_ground_x = aws * Math.cos(awa) - sog_vec_x, tw_ground_y = aws * Math.sin(awa) - sog_vec_y;
|
|
114
|
+
let twd_ground = (hdg + Math.atan2(tw_ground_y, tw_ground_x) + 2 * Math.PI) % (2 * Math.PI);
|
|
115
|
+
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
store.raw["environment.wind.speedTrue"] = tws_water; store.raw["environment.wind.angleTrueWater"] = twa_water; store.raw["environment.wind.directionTrue"] = twd_ground;
|
|
118
|
+
|
|
119
|
+
safePush(store.smoothBuf.twa, twa_water, now); safePush(store.longBuf.twa, twa_water, now);
|
|
120
|
+
safePush(store.smoothBuf.twd, twd_ground, now); safePush(store.longBuf.twd, twd_ground, now);
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
function processIncomingData(path, val) {
|
|
127
124
|
const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
|
|
128
|
-
if (path === "navigation.
|
|
129
|
-
if (path === "
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
} else return;
|
|
138
|
-
store.smoothBuf.twd.push({ val: twdRad, time: now }); store.longBuf.twd.push({ val: twdRad, time: now });
|
|
139
|
-
}
|
|
125
|
+
if (path === "navigation.position") store.raw["navigation.position"] = val;
|
|
126
|
+
if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
|
|
127
|
+
|
|
128
|
+
const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
|
|
129
|
+
if (twPaths.includes(path)) twDirty = true;
|
|
130
|
+
if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
|
|
131
|
+
|
|
132
|
+
if (path === "navigation.headingTrue") { safePush(store.smoothBuf.hdg, val, now); safePush(store.longBuf.hdg, val, now); }
|
|
133
|
+
if (path === "navigation.courseOverGroundTrue") { safePush(store.smoothBuf.cog, val, now); safePush(store.longBuf.cog, val, now); }
|
|
140
134
|
}
|
|
141
135
|
|
|
142
136
|
// ==========================================================================
|
|
143
|
-
//
|
|
137
|
+
// 4. AUDIO E ALLARMI
|
|
144
138
|
// ==========================================================================
|
|
139
|
+
function playBingBing() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
|
|
145
140
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
141
|
+
function playGybeAlarm() {
|
|
142
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
143
|
+
const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
|
|
144
|
+
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); }
|
|
145
|
+
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); }
|
|
146
|
+
}
|
|
150
147
|
|
|
151
|
-
|
|
148
|
+
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'); }
|
|
152
149
|
|
|
153
|
-
|
|
150
|
+
// ==========================================================================
|
|
151
|
+
// 5. TREND E TATTICA
|
|
152
|
+
// ==========================================================================
|
|
153
|
+
/**
|
|
154
|
+
* Monitoraggio Trend Vento Professionale (Tattica + Meteo + Gybe Safety)
|
|
155
|
+
*/
|
|
156
|
+
function updateWindTrend() {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true);
|
|
159
|
+
const shortAvg = getCircularAverageFromBuffer(store.longBuf.twd, 5000, false);
|
|
160
|
+
const instantTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
161
|
+
|
|
162
|
+
if (!shortAvg || !twaAvgObj || instantTwaRad === undefined) return;
|
|
163
|
+
const instantTwaDeg = radToDeg(instantTwaRad);
|
|
164
|
+
const shortAvgDeg = radToDeg(shortAvg.val);
|
|
165
|
+
const twaAvgDeg = radToDeg(twaAvgObj.val);
|
|
166
|
+
|
|
167
|
+
if (lastShortAvgVal === null) { lastShortAvgVal = shortAvgDeg; lastInstantTwa = instantTwaDeg; return; }
|
|
168
|
+
const dt = (now - lastTrendTime) / 1000; lastTrendTime = now;
|
|
169
|
+
|
|
170
|
+
// --- GYBE SAFETY ---
|
|
171
|
+
const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
|
|
172
|
+
lastInstantTwa = instantTwaDeg;
|
|
173
|
+
if (gybeDetected && isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
|
|
154
174
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
175
|
+
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
176
|
+
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
177
|
+
|
|
178
|
+
if (now - lastGybeAlarmTime < 4000 && isNavigating) {
|
|
179
|
+
[compassDots.cw, compassDots.ccw, gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.add('is-gybing'); el.classList.remove('is-trending'); el.setAttribute('fill', '#ff0000'); }});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- TREND CALCULATION (°/sec) ---
|
|
184
|
+
let diff = (shortAvgDeg - lastShortAvgVal + 540) % 360 - 180; lastShortAvgVal = shortAvgDeg;
|
|
185
|
+
if (dt > 0) {
|
|
186
|
+
let currentRate = Math.max(-10, Math.min(10, diff / dt)); // Clamp a 10°/sec per evitare rumore
|
|
187
|
+
const alpha = Math.min(1, dt / 5);
|
|
188
|
+
rotationTrend = rotationTrend * (1 - alpha) + currentRate * alpha;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (Math.abs(rotationTrend) > 1.0) {
|
|
192
|
+
const pos = store.raw["navigation.position"], isSouth = pos && pos.latitude < 0;
|
|
193
|
+
let meteoColor = (!isSouth) ? (rotationTrend < 0 ? "#00ff00" : "#ff0000") : (rotationTrend > 0 ? "#00ff00" : "#ff0000");
|
|
194
|
+
let isLift = (twaAvgDeg > 0) ? (rotationTrend > 0) : (rotationTrend < 0);
|
|
195
|
+
const absTwa = Math.abs(twaAvgDeg);
|
|
196
|
+
if (absTwa >= 90 && absTwa < 160) isLift = !isLift; else if (absTwa >= 160) isLift = false;
|
|
197
|
+
const tacticColor = isLift ? "#00ff00" : "#ff0000";
|
|
198
|
+
|
|
199
|
+
if (rotationTrend > 0) {
|
|
200
|
+
if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
|
|
201
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
|
|
202
|
+
[compassDots.ccw, gaugeDots.ccw].forEach(el => { if(el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
|
|
203
|
+
} else {
|
|
204
|
+
if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
|
|
205
|
+
if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor); }
|
|
206
|
+
[compassDots.cw, gaugeDots.cw].forEach(el => { if(el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
|
|
170
207
|
}
|
|
171
208
|
} else {
|
|
172
|
-
|
|
173
|
-
cwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
174
|
-
ccwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
209
|
+
[compassDots.cw, compassDots.ccw, gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
212
|
|
|
213
|
+
// ==========================================================================
|
|
214
|
+
// 6. RENDERING E LOOP PRINCIPALE
|
|
215
|
+
// ==========================================================================
|
|
216
|
+
function updateLeewayDisplay(deg) { const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125); ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w); ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`; }
|
|
217
|
+
|
|
178
218
|
function startDisplayLoop() {
|
|
219
|
+
let tick = 0; // Contatore per i cicli di rendering
|
|
220
|
+
|
|
179
221
|
renderInterval = setInterval(() => {
|
|
180
222
|
const now = Date.now();
|
|
181
|
-
|
|
182
|
-
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]; } }
|
|
223
|
+
tick++;
|
|
183
224
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
225
|
+
// ==========================================================
|
|
226
|
+
// LIVE TIER (Ogni 1s) - Reattività, Numeri, Allarmi, Lancette
|
|
227
|
+
// ==========================================================
|
|
228
|
+
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 };
|
|
229
|
+
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]; } }
|
|
230
|
+
|
|
231
|
+
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0), sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
232
|
+
isNavigating = stwKts > CONFIG.averages.minSpeed || sogKts > CONFIG.averages.minSpeed;
|
|
233
|
+
|
|
234
|
+
// Numeri veloci
|
|
235
|
+
if (store.raw["navigation.speedThroughWater"] !== undefined) { ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts); }
|
|
236
|
+
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
237
|
+
const twaRad = store.raw["environment.wind.angleTrueWater"];
|
|
238
|
+
const vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
|
|
239
|
+
manageHistory('vmg', vmgKts); manageHistory('sog', sogKts);
|
|
240
|
+
if (displayModeSog === 'VMG') { ui.sog.innerText = vmgKts.toFixed(1); ui.sog.style.color = "#64ffda"; document.getElementById('sog-vmg-label').textContent = 'VMG'; }
|
|
241
|
+
else { ui.sog.innerText = sogKts.toFixed(1); ui.sog.style.color = (sogKts-stwKts > 0.3) ? "#2ecc71" : (sogKts-stwKts < -0.3 ? "#e74c3c" : "#fff"); document.getElementById('sog-vmg-label').textContent = 'SOG'; }
|
|
197
242
|
}
|
|
243
|
+
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); }
|
|
244
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined) { const twsKts = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = twsKts.toFixed(1); ui.tws.style.color = (twsKts >= CONFIG.graphs.reef2) ? "#e74c3c" : (twsKts >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff"); manageHistory('tws', twsKts); }
|
|
198
245
|
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
199
246
|
|
|
200
|
-
//
|
|
247
|
+
// Lancette (Sempre fluide ogni 1s)
|
|
201
248
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
|
|
202
|
-
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, smAwa.val); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
249
|
+
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
203
250
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
204
|
-
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
251
|
+
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
205
252
|
|
|
206
|
-
//
|
|
253
|
+
// Leeway (Ogni 1s)
|
|
207
254
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
208
|
-
let
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
smoothedLeeway = (smoothedLeeway * 0.9) + (rawDrift * 0.1);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
218
|
-
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
255
|
+
let driftRad = (store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI;
|
|
256
|
+
let driftDeg = radToDeg(driftRad);
|
|
257
|
+
if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
258
|
+
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
259
|
+
const isContaminated = Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7;
|
|
260
|
+
ui.leewayVal.style.color = isContaminated ? "#f39c12" : "#fff";
|
|
219
261
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
220
|
-
}
|
|
221
|
-
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
updateWindTrend(); // Trend calcolato ogni secondo
|
|
265
|
+
|
|
266
|
+
// ==========================================================
|
|
267
|
+
// HEAVY TIER (Ogni 2s) - Disegno Sparklines SVG
|
|
268
|
+
// ==========================================================
|
|
269
|
+
if (tick % 2 === 0) {
|
|
270
|
+
refreshGraph('stw');
|
|
271
|
+
refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog');
|
|
272
|
+
refreshGraph('depth');
|
|
273
|
+
refreshGraph('tws');
|
|
222
274
|
}
|
|
223
275
|
|
|
224
|
-
//
|
|
225
|
-
|
|
276
|
+
// ==========================================================
|
|
277
|
+
// SLOW TIER (Ogni 3s) - Medie MEAN e Calcoli TACK
|
|
278
|
+
// ==========================================================
|
|
279
|
+
if (tick % 3 === 0) {
|
|
226
280
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
|
|
227
281
|
cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
|
|
228
282
|
awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averages.longWindow, true),
|
|
@@ -230,297 +284,232 @@ function startDisplayLoop() {
|
|
|
230
284
|
twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
|
|
231
285
|
|
|
232
286
|
const upUI = (el, obj, isCompass = false) => {
|
|
233
|
-
if (!obj || obj.val === null) {
|
|
234
|
-
|
|
235
|
-
el.
|
|
236
|
-
|
|
237
|
-
let val = Math.round(obj.val);
|
|
238
|
-
let displayVal;
|
|
239
|
-
|
|
240
|
-
if (isCompass) displayVal = ((val + 360) % 360).toString().padStart(3, '0');
|
|
241
|
-
else displayVal = val.toString();
|
|
242
|
-
|
|
243
|
-
el.innerHTML = `${displayVal}°`;
|
|
244
|
-
|
|
245
|
-
if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data');
|
|
246
|
-
else el.classList.add('unstable-data');
|
|
287
|
+
if (!obj || obj.val === null) { el.innerHTML = "---°"; el.classList.remove('unstable-data'); }
|
|
288
|
+
else {
|
|
289
|
+
let valDeg = Math.round(radToDeg(obj.val)); el.innerHTML = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
290
|
+
if (obj.stable || !isNavigating) el.classList.remove('unstable-data'); else el.classList.add('unstable-data');
|
|
247
291
|
}
|
|
248
292
|
};
|
|
249
|
-
|
|
250
|
-
upUI(ui.hdg, hObj, true);
|
|
251
|
-
upUI(ui.cog, cObj, true);
|
|
252
|
-
upUI(ui.awaAvg, awObj, false);
|
|
253
|
-
upUI(ui.twaAvg, twObj, false);
|
|
254
|
-
upUI(ui.twdAvg, twdObj, true);
|
|
293
|
+
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);
|
|
255
294
|
|
|
256
295
|
if (hObj && twObj && hObj.val !== null) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
296
|
+
const tackHdgDeg = radToDeg((hObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
|
|
297
|
+
ui.tackHdg.innerHTML = `${Math.round((tackHdgDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
298
|
+
if (cObj) {
|
|
299
|
+
const tackCogDeg = radToDeg((cObj.val - (twObj.val * 2) + Math.PI * 2) % (Math.PI * 2));
|
|
300
|
+
ui.tackCog.innerHTML = `${Math.round((tackCogDeg + 360) % 360).toString().padStart(3, '0')}°`;
|
|
301
|
+
}
|
|
262
302
|
}
|
|
263
303
|
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val);
|
|
269
|
-
ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`);
|
|
270
|
-
ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`);
|
|
271
|
-
|
|
272
|
-
let currentTwsKts = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
273
|
-
let reefColor = "#fff";
|
|
274
|
-
if (currentTwsKts >= CONFIG.graphs.reef2) reefColor = "#e74c3c";
|
|
275
|
-
else if (currentTwsKts >= CONFIG.graphs.reef1) reefColor = "#e67e22";
|
|
276
|
-
if (ui.twdChevron) ui.twdChevron.setAttribute('stroke', reefColor);
|
|
304
|
+
const smHdg = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false), smTwd = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
305
|
+
if (smHdg && smTwd) {
|
|
306
|
+
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwd.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
307
|
+
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdg.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
277
308
|
}
|
|
278
|
-
lastAvgUIUpdate = now;
|
|
279
309
|
}
|
|
280
|
-
|
|
281
|
-
|
|
310
|
+
|
|
311
|
+
// Pulizia buffer
|
|
312
|
+
if (tick % 60 === 0) { // Ogni minuto pulisce tutto per sicurezza
|
|
313
|
+
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(); }
|
|
314
|
+
for (let b in store.longBuf) { while (store.longBuf[b].length > 0 && (now - store.longBuf[b][0].time) > CONFIG.averages.longWindow) store.longBuf[b].shift(); }
|
|
315
|
+
tick = 0; // Reset contatore
|
|
316
|
+
}
|
|
282
317
|
}, RENDER_INTERVAL_MS);
|
|
283
318
|
}
|
|
284
319
|
|
|
285
320
|
// ==========================================================================
|
|
286
|
-
//
|
|
321
|
+
// 7. CONFIGURAZIONE E GRAFICI
|
|
287
322
|
// ==========================================================================
|
|
288
|
-
function
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
323
|
+
async function fetchServerConfig() {
|
|
324
|
+
if (!window.location.protocol.includes("http")) return;
|
|
325
|
+
const pluginID = 'rotevista-dash';
|
|
326
|
+
const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
|
|
327
|
+
for (let url of possibleUrls) {
|
|
328
|
+
try {
|
|
329
|
+
const response = await fetch(url);
|
|
330
|
+
if (response.ok) {
|
|
331
|
+
const data = await response.json();
|
|
332
|
+
const actual = data.configuration || data;
|
|
333
|
+
if (actual && typeof actual === 'object') {
|
|
334
|
+
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]); } };
|
|
335
|
+
parseNumbers(actual);
|
|
336
|
+
if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
|
|
337
|
+
if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
|
|
338
|
+
if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
|
|
339
|
+
if (actual.scales) { for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; } }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch (e) { }
|
|
343
|
+
}
|
|
297
344
|
}
|
|
298
345
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
346
|
+
/**
|
|
347
|
+
* Gestisce l'archiviazione dei dati storici.
|
|
348
|
+
* Il disegno grafico viene ora gestito separatamente nel loop pesante.
|
|
349
|
+
*/
|
|
304
350
|
function manageHistory(t, v) {
|
|
305
351
|
const n = Date.now();
|
|
306
352
|
const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
353
|
+
|
|
354
|
+
if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
|
|
355
|
+
store.histories[t].push(v);
|
|
356
|
+
if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
|
|
357
|
+
store.lastUpdates[t] = n;
|
|
358
|
+
}
|
|
313
359
|
}
|
|
314
360
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Funzione dedicata al ridisegno dei grafici (chiamata dal loop HEAVY)
|
|
363
|
+
*/
|
|
364
|
+
function refreshGraph(t) {
|
|
365
|
+
const type = (t === 'vmg') ? 'sog' : t;
|
|
366
|
+
const data = store.histories[t];
|
|
367
|
+
if (!data || data.length < 2) return;
|
|
368
|
+
|
|
369
|
+
const mode = graphModes[type];
|
|
370
|
+
const cfg = calculateScale(type, data, mode);
|
|
371
|
+
|
|
372
|
+
// Aggiornamento stile box Hercules
|
|
373
|
+
const graphEl = document.getElementById(type + '-graph');
|
|
374
|
+
if (graphEl) {
|
|
375
|
+
const box = graphEl.closest('.data-box');
|
|
376
|
+
if (box) {
|
|
377
|
+
if (mode === 'hercules') box.classList.add('box-hercules');
|
|
378
|
+
else box.classList.remove('box-hercules');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
updateScaleLabels(type, cfg.min, cfg.max);
|
|
382
|
+
drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
322
383
|
}
|
|
323
384
|
|
|
324
|
-
function
|
|
325
|
-
const
|
|
326
|
-
|
|
385
|
+
function calculateScale(type, data, mode) {
|
|
386
|
+
const s = CONFIG.scales[type] || { stdMax: 12, hercSpan: 4, step: 2 }; let aMin = Math.min(...data), aMax = Math.max(...data);
|
|
387
|
+
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 }; }
|
|
388
|
+
else return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
|
|
327
389
|
}
|
|
328
390
|
|
|
391
|
+
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>`; }
|
|
392
|
+
|
|
329
393
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
330
394
|
const svg = document.getElementById(id); if (!svg || d.length < 2) return;
|
|
331
395
|
const w = 200, h = 40, range = max - min || 1;
|
|
332
396
|
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" />`; });
|
|
333
397
|
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" />`; }
|
|
334
|
-
let pD = ""
|
|
398
|
+
let pD = ""; let cS = "";
|
|
335
399
|
d.forEach((v, i) => {
|
|
336
400
|
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} `;
|
|
337
401
|
if (isTws && i > 0) {
|
|
338
402
|
const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
|
|
339
|
-
let c = "#
|
|
340
|
-
if (v >= CONFIG.graphs.reef2) c = "#e74c3c"; else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
|
|
403
|
+
let c = (v >= CONFIG.graphs.reef2) ? "#e74c3c" : (v >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff");
|
|
341
404
|
cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
|
|
342
405
|
}
|
|
343
406
|
});
|
|
344
|
-
const
|
|
345
|
-
|
|
407
|
+
const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#ffffff' };
|
|
408
|
+
const colorKey = id === 'sog-graph' && displayModeSog === 'VMG' ? '#64ffda' : clrs[id];
|
|
409
|
+
svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(255,255,255,0.08)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${colorKey}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${colorKey}" />`;
|
|
346
410
|
}
|
|
347
411
|
|
|
348
412
|
// ==========================================================================
|
|
349
|
-
// 8.
|
|
413
|
+
// 8. INTERAZIONI E SIMULATORE
|
|
350
414
|
// ==========================================================================
|
|
351
|
-
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
352
|
-
|
|
353
415
|
function toggleFocusMode(type, element) {
|
|
354
|
-
const container = document.querySelector('.main-container');
|
|
355
|
-
const parentPanel = element.closest('.side-panel');
|
|
356
|
-
const isLeft = parentPanel.classList.contains('left-panel');
|
|
416
|
+
const container = document.querySelector('.main-container'); const parentPanel = element.closest('.side-panel'); const isLeft = parentPanel.classList.contains('left-panel');
|
|
357
417
|
isFocusActive = !isFocusActive;
|
|
358
|
-
if (isFocusActive) {
|
|
359
|
-
|
|
360
|
-
container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right');
|
|
361
|
-
parentPanel.classList.add('has-focus');
|
|
362
|
-
element.classList.add('is-focused');
|
|
363
|
-
blockNextClick = true;
|
|
364
|
-
} else {
|
|
365
|
-
container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
|
|
366
|
-
document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus'));
|
|
367
|
-
document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
|
|
368
|
-
}
|
|
418
|
+
if (isFocusActive) { container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right'); parentPanel.classList.add('has-focus'); element.classList.add('is-focused'); }
|
|
419
|
+
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')); }
|
|
369
420
|
}
|
|
370
421
|
|
|
371
422
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
372
423
|
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
373
|
-
let lastTapTime = 0, tapTimeout;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (
|
|
377
|
-
const currentTime = new Date().getTime();
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (tapDelay < 300 && tapDelay > 0) {
|
|
383
|
-
clearTimeout(tapTimeout);
|
|
384
|
-
if (!isFocusActive) {
|
|
385
|
-
graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
|
|
386
|
-
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
387
|
-
el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
|
|
388
|
-
}
|
|
389
|
-
lastTapTime = 0;
|
|
390
|
-
} else {
|
|
391
|
-
lastTapTime = currentTime;
|
|
392
|
-
tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); }, 350);
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
el.addEventListener('pointerdown', handleInteraction);
|
|
396
|
-
el.addEventListener('pointerup', () => clearTimeout(pressTimer));
|
|
424
|
+
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
425
|
+
el.addEventListener('pointerdown', (e) => { isLongPressActive = false; pressTimer = setTimeout(() => { if (!isFocusActive) { isLongPressActive = true; toggleFocusMode(type, el); lastTapTime = 0; } }, 1000); });
|
|
426
|
+
el.addEventListener('pointerup', (e) => {
|
|
427
|
+
clearTimeout(pressTimer); if (isLongPressActive) return;
|
|
428
|
+
const currentTime = new Date().getTime(), tapDelay = currentTime - lastTapTime;
|
|
429
|
+
if (tapDelay < 300 && tapDelay > 0) { clearTimeout(tapTimeout); if (!isFocusActive) { graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); } lastTapTime = 0; }
|
|
430
|
+
else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(100, 255, 218, 0.1)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
|
|
431
|
+
});
|
|
397
432
|
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
398
433
|
});
|
|
399
434
|
|
|
400
|
-
// ==========================================================================
|
|
401
|
-
// GESTIONE HOTSPOT: Click (Fullscreen) e Long Press (Night Mode)
|
|
402
|
-
// ==========================================================================
|
|
403
435
|
if (ui.hotspot) {
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
pressTimer = setTimeout(() => {
|
|
409
|
-
document.body.classList.toggle('night-mode');
|
|
410
|
-
ui.hotspot.style.opacity = "0.5";
|
|
411
|
-
setTimeout(() => ui.hotspot.style.opacity = "1", 200);
|
|
412
|
-
pressTimer = null;
|
|
413
|
-
}, HOLD_DURATION);
|
|
414
|
-
});
|
|
436
|
+
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); });
|
|
437
|
+
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(); } } });
|
|
438
|
+
ui.hotspot.addEventListener('pointerleave', () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } });
|
|
439
|
+
}
|
|
415
440
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
441
|
+
function connect() {
|
|
442
|
+
if (simulationMode) return;
|
|
443
|
+
|
|
444
|
+
let addr = (window.location.protocol.includes("http")) ? window.location.host : CONFIG.server.fallbackIp;
|
|
445
|
+
const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws';
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
socket = new WebSocket(`${protocol}://${addr}/signalk/v1/stream?subscribe=self`);
|
|
449
|
+
|
|
450
|
+
socket.onopen = () => {
|
|
451
|
+
ui.status.className = "online";
|
|
452
|
+
ui.status.innerText = "ONLINE";
|
|
453
|
+
reconnectDelay = 1000; // Reset del ritardo dopo una connessione riuscita
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
socket.onmessage = (e) => {
|
|
457
|
+
if (simulationMode) return;
|
|
458
|
+
const d = JSON.parse(e.data);
|
|
459
|
+
if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value)));
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
socket.onclose = () => {
|
|
463
|
+
if (!simulationMode) {
|
|
464
|
+
ui.status.className = "offline";
|
|
465
|
+
ui.status.innerText = "RECONNECTING...";
|
|
466
|
+
|
|
467
|
+
// Exponential Backoff: aumenta il tempo ad ogni tentativo fallito
|
|
468
|
+
setTimeout(connect, reconnectDelay);
|
|
469
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); // Max 10 secondi
|
|
428
470
|
}
|
|
429
|
-
}
|
|
430
|
-
});
|
|
471
|
+
};
|
|
431
472
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
pressTimer = null;
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
(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); } } })();
|
|
473
|
+
socket.onerror = () => {
|
|
474
|
+
socket.close(); // Forza il trigger di onclose
|
|
475
|
+
};
|
|
440
476
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
477
|
+
} catch (e) {
|
|
478
|
+
setTimeout(connect, reconnectDelay);
|
|
479
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
445
482
|
|
|
446
|
-
|
|
447
|
-
// 9. MOTORE SIMULAZIONE DINAMICA E AVVIO (3 Click su Depth)
|
|
448
|
-
// ==========================================================================
|
|
449
|
-
ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
450
|
-
let dC = 0, lC = 0;
|
|
451
|
-
return function() {
|
|
452
|
-
const n = Date.now();
|
|
453
|
-
if (n - lC < 500) dC++; else dC = 1;
|
|
454
|
-
lC = n;
|
|
455
|
-
if (dC === 3) {
|
|
456
|
-
simulationMode = !simulationMode;
|
|
457
|
-
if (simulationMode) {
|
|
458
|
-
if (socket) socket.close();
|
|
459
|
-
startDynamicSimulation();
|
|
460
|
-
} else {
|
|
461
|
-
location.reload();
|
|
462
|
-
}
|
|
463
|
-
dC = 0;
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
})());
|
|
483
|
+
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; } }; })());
|
|
467
484
|
|
|
468
485
|
function startDynamicSimulation() {
|
|
469
486
|
ui.status.innerText = "SIM ATTIVO";
|
|
470
|
-
|
|
471
|
-
let sim = {
|
|
472
|
-
hdg: Math.random() * 360,
|
|
473
|
-
tws: 12,
|
|
474
|
-
twd: Math.random() * 360,
|
|
475
|
-
depth: 12,
|
|
476
|
-
stw: 5,
|
|
477
|
-
leeway: 0,
|
|
478
|
-
startTime: Date.now()
|
|
479
|
-
};
|
|
480
|
-
|
|
487
|
+
let sim = { hdg: 45, tws: 12, twd: 45, depth: 12, stw: 5, leeway: 0, currentSpeed: 1.5, currentDir: 90, startTime: Date.now() };
|
|
481
488
|
simInterval = setInterval(() => {
|
|
482
489
|
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
483
|
-
|
|
484
|
-
// Vento: Rotazione lenta + Salto netto a 120s
|
|
485
490
|
if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
|
|
486
491
|
sim.twd = (sim.twd + (Math.sin(elapsed / 20) * 0.5) + 360) % 360;
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
sim.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
|
|
502
|
-
|
|
503
|
-
// Leeway smussato
|
|
504
|
-
const rawLeeway = Math.sin(degToRad(twaRel)) * 4;
|
|
505
|
-
sim.leeway += (rawLeeway - sim.leeway) * 0.05;
|
|
506
|
-
|
|
507
|
-
// Vettori Reali
|
|
508
|
-
const cog = (sim.hdg - sim.leeway + 360) % 360;
|
|
509
|
-
const twaRad = degToRad(twaRel);
|
|
510
|
-
const aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
|
|
511
|
-
const awa = radToDeg(Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad)));
|
|
512
|
-
|
|
513
|
-
// Invio Dati (Con tutto il set necessario per la UI)
|
|
514
|
-
processIncomingData("environment.wind.speedTrue", ktsToMs(sim.tws));
|
|
515
|
-
processIncomingData("environment.wind.directionTrue", degToRad(sim.twd));
|
|
516
|
-
processIncomingData("environment.wind.angleTrueWater", degToRad(twaRel));
|
|
517
|
-
processIncomingData("environment.wind.speedApparent", ktsToMs(aws));
|
|
518
|
-
processIncomingData("environment.wind.angleApparent", degToRad(awa));
|
|
519
|
-
processIncomingData("environment.depth.belowTransducer", sim.depth);
|
|
520
|
-
processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
521
|
-
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw));
|
|
522
|
-
processIncomingData("navigation.speedOverGround", ktsToMs(sim.stw));
|
|
523
|
-
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
|
|
524
|
-
|
|
492
|
+
const inGust = (elapsed % 40) < 5; const targetTws = (inGust ? 18 : 10) + Math.sin(elapsed / 10) * 2;
|
|
493
|
+
sim.tws += (targetTws - sim.tws) * 0.05; sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
|
|
494
|
+
let twaRel = (sim.twd - sim.hdg + 360) % 360; if (twaRel > 180) twaRel -= 360;
|
|
495
|
+
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;
|
|
496
|
+
const rawLeeway = Math.sin(degToRad(twaRel)) * 4; sim.leeway += (rawLeeway - sim.leeway) * 0.05;
|
|
497
|
+
const bX = sim.stw * Math.sin(degToRad(sim.hdg + sim.leeway)), bY = sim.stw * Math.cos(degToRad(sim.hdg + sim.leeway));
|
|
498
|
+
const cX = sim.currentSpeed * Math.sin(degToRad(sim.currentDir)), cY = sim.currentSpeed * Math.cos(degToRad(sim.currentDir));
|
|
499
|
+
const sog = Math.sqrt(Math.pow(bX + cX, 2) + Math.pow(bY + cY, 2)), cog = (radToDeg(Math.atan2(bX + cX, bY + cY)) + 360) % 360;
|
|
500
|
+
const twaRad = degToRad(twaRel), aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
|
|
501
|
+
const awa = Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad));
|
|
502
|
+
processIncomingData("environment.wind.speedApparent", ktsToMs(aws)); processIncomingData("environment.wind.angleApparent", awa);
|
|
503
|
+
processIncomingData("environment.depth.belowTransducer", sim.depth); processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
504
|
+
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw)); processIncomingData("navigation.speedOverGround", ktsToMs(sog));
|
|
505
|
+
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog)); processIncomingData("navigation.position", { latitude: 45.0, longitude: 12.0 });
|
|
525
506
|
}, 1000);
|
|
526
507
|
}
|
|
508
|
+
|
|
509
|
+
// ==========================================================================
|
|
510
|
+
// 9. INIT
|
|
511
|
+
// ==========================================================================
|
|
512
|
+
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
513
|
+
(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); } } })();
|
|
514
|
+
async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
|
|
515
|
+
window.addEventListener('load', init);
|