@sailingrotevista/rotevista-dash 2.0.15 → 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 +215 -355
- 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;
|
|
@@ -42,9 +21,13 @@ let socket, renderInterval, simInterval;
|
|
|
42
21
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
43
22
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
44
23
|
|
|
45
|
-
// Variabili di stato per filtri e
|
|
24
|
+
// Variabili di stato per filtri e logica tattica
|
|
46
25
|
let smoothedLeeway = 0;
|
|
47
26
|
let rotationTrend = 0;
|
|
27
|
+
let lastShortAvgVal = null;
|
|
28
|
+
let lastInstantTwa = null;
|
|
29
|
+
let isNavigating = false;
|
|
30
|
+
|
|
48
31
|
let pressTimer, isFocusActive = false, blockNextClick = false;
|
|
49
32
|
|
|
50
33
|
const graphModes = {
|
|
@@ -78,34 +61,7 @@ const ui = {
|
|
|
78
61
|
};
|
|
79
62
|
|
|
80
63
|
// ==========================================================================
|
|
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
|
|
64
|
+
// 3. MATEMATICA E UTILS
|
|
109
65
|
// ==========================================================================
|
|
110
66
|
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
111
67
|
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
@@ -118,110 +74,156 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
|
118
74
|
if (validData.length === 0) return null;
|
|
119
75
|
let sSin = 0, sCos = 0; validData.forEach(item => { sSin += Math.sin(item.val); sCos += Math.cos(item.val); });
|
|
120
76
|
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);
|
|
77
|
+
let isStable = (validData.length > 2) && (validData[validData.length - 1].time - validData[0].time >= windowMs - CONFIG.averages.stabilityTolerance) && (R > CONFIG.averages.stabilityThreshold);
|
|
122
78
|
let avgDeg = Math.round(radToDeg(Math.atan2(sSin, sCos)));
|
|
123
79
|
return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
|
|
124
80
|
}
|
|
125
81
|
|
|
126
|
-
function processIncomingData(path, val) {
|
|
127
|
-
const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
|
|
128
|
-
if (path === "navigation.headingTrue") { store.smoothBuf.hdg.push({ val: val, time: now }); store.longBuf.hdg.push({ val: val, time: now }); }
|
|
129
|
-
if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
|
|
130
|
-
if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
|
|
131
|
-
if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
|
|
132
|
-
if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
|
|
133
|
-
let twdRad = 0; if (path === "environment.wind.directionTrue") twdRad = val;
|
|
134
|
-
else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) {
|
|
135
|
-
twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
|
|
136
|
-
if (twdRad < 0) twdRad += (2 * Math.PI);
|
|
137
|
-
} else return;
|
|
138
|
-
store.smoothBuf.twd.push({ val: twdRad, time: now }); store.longBuf.twd.push({ val: twdRad, time: now });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
82
|
// ==========================================================================
|
|
143
|
-
//
|
|
83
|
+
// 4. LOGICA FISICA (VENTO E SICUREZZA)
|
|
144
84
|
// ==========================================================================
|
|
145
85
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
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
|
+
}
|
|
150
92
|
|
|
151
|
-
|
|
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
|
+
}
|
|
152
99
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
|
157
133
|
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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'); }});
|
|
170
167
|
}
|
|
171
168
|
} else {
|
|
172
|
-
|
|
173
|
-
cwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
174
|
-
ccwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
169
|
+
[compassDots.cw, compassDots.ccw, gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
|
|
175
170
|
}
|
|
176
171
|
}
|
|
177
172
|
|
|
173
|
+
// ==========================================================================
|
|
174
|
+
// 5. DATA ROUTING
|
|
175
|
+
// ==========================================================================
|
|
176
|
+
function processIncomingData(path, val) {
|
|
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();
|
|
181
|
+
if (path === "navigation.headingTrue") { store.smoothBuf.hdg.push({ val: val, time: now }); store.longBuf.hdg.push({ val: val, time: now }); }
|
|
182
|
+
if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ==========================================================================
|
|
186
|
+
// 6. MOTORE RENDERING UI
|
|
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
|
+
|
|
178
190
|
function startDisplayLoop() {
|
|
179
191
|
renderInterval = setInterval(() => {
|
|
180
192
|
const now = Date.now();
|
|
181
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 };
|
|
182
|
-
for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { pathsToWatch[p]
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
186
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); }
|
|
187
|
-
|
|
188
|
-
// --- 1. RENDERING TWS CON COLORE DINAMICO ---
|
|
189
208
|
if (store.raw["environment.wind.speedTrue"] !== undefined) {
|
|
190
|
-
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
|
|
191
|
-
ui.tws.
|
|
192
|
-
let twsColor = "#fff"; // Default: Bianco
|
|
193
|
-
if (twsKts >= CONFIG.graphs.reef2) twsColor = "#e74c3c"; // Rosso
|
|
194
|
-
else if (twsKts >= CONFIG.graphs.reef1) twsColor = "#e67e22"; // Arancio
|
|
195
|
-
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");
|
|
196
211
|
manageHistory('tws', twsKts);
|
|
197
212
|
}
|
|
198
213
|
if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
|
|
199
214
|
|
|
200
|
-
// Render Quadrante
|
|
201
215
|
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
|
|
202
216
|
if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, smAwa.val); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
|
|
203
217
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
204
218
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
205
219
|
|
|
206
|
-
// --- CALCOLO E SMUSSAMENTO LEEWAY ---
|
|
207
220
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
208
|
-
let
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
if (curSog < CONFIG.averages.minSpeed) {
|
|
212
|
-
smoothedLeeway = 0;
|
|
213
|
-
} else {
|
|
214
|
-
smoothedLeeway = (smoothedLeeway * 0.9) + (rawDrift * 0.1);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
218
|
-
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
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)`);
|
|
219
224
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
220
|
-
} else
|
|
221
|
-
updateLeewayDisplay(0);
|
|
222
|
-
}
|
|
225
|
+
} else updateLeewayDisplay(0);
|
|
223
226
|
|
|
224
|
-
// --- 2. RENDERING MEDIE E BUSSOLA TATTICA ---
|
|
225
227
|
if (now - lastAvgUIUpdate > 3000) {
|
|
226
228
|
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
|
|
227
229
|
cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
|
|
@@ -230,51 +232,19 @@ function startDisplayLoop() {
|
|
|
230
232
|
twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
|
|
231
233
|
|
|
232
234
|
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');
|
|
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');
|
|
247
239
|
}
|
|
248
240
|
};
|
|
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);
|
|
255
|
-
|
|
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);
|
|
256
242
|
if (hObj && twObj && hObj.val !== null) {
|
|
257
|
-
|
|
243
|
+
const tA = twObj.val * 2;
|
|
244
|
+
ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
258
245
|
if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
259
|
-
let tStable = (hObj.stable && twObj.stable) || (curSog < CONFIG.averages.minSpeed);
|
|
260
|
-
if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
|
|
261
|
-
else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Rotazione Bussola Tattica e Colore Sincronizzato
|
|
265
|
-
if (twdObj && hObj) {
|
|
266
|
-
updateWindTrend(); // Aggiorna i pallini
|
|
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);
|
|
277
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)`); }
|
|
278
248
|
lastAvgUIUpdate = now;
|
|
279
249
|
}
|
|
280
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(); }
|
|
@@ -283,244 +253,134 @@ function startDisplayLoop() {
|
|
|
283
253
|
}
|
|
284
254
|
|
|
285
255
|
// ==========================================================================
|
|
286
|
-
//
|
|
256
|
+
// 7. CONFIG E GRAFICI
|
|
287
257
|
// ==========================================================================
|
|
288
|
-
function
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
}
|
|
297
279
|
}
|
|
298
280
|
|
|
299
|
-
// ==========================================================================
|
|
300
|
-
// 7. FUNZIONI GRAFICHE E DISEGNO (BIANCO -> ARANCIO -> ROSSO)
|
|
301
|
-
// ==========================================================================
|
|
302
|
-
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)}°`; }
|
|
303
|
-
|
|
304
281
|
function manageHistory(t, v) {
|
|
305
|
-
const n = Date.now();
|
|
306
|
-
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;
|
|
307
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; }
|
|
308
284
|
const mode = graphModes[t], cfg = calculateScale(t, store.histories[t], mode);
|
|
309
|
-
|
|
310
|
-
if (mode === 'hercules') box.classList.add('box-hercules'); else box.classList.remove('box-hercules');
|
|
311
|
-
updateScaleLabels(t, cfg.min, cfg.max);
|
|
312
|
-
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');
|
|
313
286
|
}
|
|
314
287
|
|
|
315
288
|
function calculateScale(type, data, mode) {
|
|
316
|
-
const s = CONFIG.scales[type] || { stdMax: 12, hercSpan: 4, step: 2 };
|
|
317
|
-
let aMin = Math.
|
|
318
|
-
|
|
319
|
-
let avg = (aMin + aMax) / 2; let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin)); if (span % 2 !== 0) span += 1;
|
|
320
|
-
let min = Math.max(0, Math.floor(avg - (span / 2))); return { min, max: min + span };
|
|
321
|
-
} 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) };
|
|
322
292
|
}
|
|
323
293
|
|
|
324
|
-
function updateScaleLabels(t, min, max) {
|
|
325
|
-
const el = document.getElementById(t + '-scale'); if (!el) return;
|
|
326
|
-
el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`;
|
|
327
|
-
}
|
|
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>`; }
|
|
328
295
|
|
|
329
296
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
330
297
|
const svg = document.getElementById(id); if (!svg || d.length < 2) return;
|
|
331
298
|
const w = 200, h = 40, range = max - min || 1;
|
|
332
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" />`; });
|
|
333
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" />`; }
|
|
334
|
-
let pD = ""
|
|
301
|
+
let pD = ""; let cS = "";
|
|
335
302
|
d.forEach((v, i) => {
|
|
336
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} `;
|
|
337
304
|
if (isTws && i > 0) {
|
|
338
305
|
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";
|
|
306
|
+
let c = (v >= CONFIG.graphs.reef2) ? "#e74c3c" : (v >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff");
|
|
341
307
|
cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
|
|
342
308
|
}
|
|
343
309
|
});
|
|
344
|
-
const
|
|
345
|
-
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]}" />`;
|
|
346
312
|
}
|
|
347
313
|
|
|
348
314
|
// ==========================================================================
|
|
349
|
-
// 8.
|
|
315
|
+
// 8. INTERAZIONI
|
|
350
316
|
// ==========================================================================
|
|
351
317
|
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
352
318
|
|
|
353
319
|
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');
|
|
320
|
+
const container = document.querySelector('.main-container'); const parentPanel = element.closest('.side-panel'); const isLeft = parentPanel.classList.contains('left-panel');
|
|
357
321
|
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
|
-
}
|
|
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')); }
|
|
369
324
|
}
|
|
370
325
|
|
|
371
326
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
372
|
-
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const handleInteraction = (e) => {
|
|
376
|
-
if (e.cancelable) e.preventDefault();
|
|
377
|
-
const currentTime = new Date().getTime();
|
|
378
|
-
const tapDelay = currentTime - lastTapTime;
|
|
379
|
-
|
|
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;
|
|
380
330
|
pressTimer = setTimeout(() => { if (!isFocusActive) { toggleFocusMode(type, el); lastTapTime = 0; } }, 1000);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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));
|
|
397
|
-
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));
|
|
398
335
|
});
|
|
399
336
|
|
|
400
|
-
// ==========================================================================
|
|
401
|
-
// GESTIONE HOTSPOT: Click (Fullscreen) e Long Press (Night Mode)
|
|
402
|
-
// ==========================================================================
|
|
403
337
|
if (ui.hotspot) {
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
ui.hotspot.addEventListener('pointerdown', (e) => {
|
|
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
|
-
});
|
|
415
|
-
|
|
416
|
-
ui.hotspot.addEventListener('pointerup', (e) => {
|
|
417
|
-
if (pressTimer) {
|
|
418
|
-
clearTimeout(pressTimer);
|
|
419
|
-
pressTimer = null;
|
|
420
|
-
const doc = document.documentElement;
|
|
421
|
-
const isF = document.fullscreenElement || document.webkitFullscreenElement;
|
|
422
|
-
if (!isF) {
|
|
423
|
-
if (doc.requestFullscreen) doc.requestFullscreen();
|
|
424
|
-
else if (doc.webkitRequestFullscreen) doc.webkitRequestFullscreen();
|
|
425
|
-
} else {
|
|
426
|
-
if (document.exitFullscreen) document.exitFullscreen();
|
|
427
|
-
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
ui.hotspot.addEventListener('pointerleave', () => {
|
|
433
|
-
if (pressTimer) {
|
|
434
|
-
clearTimeout(pressTimer);
|
|
435
|
-
pressTimer = null;
|
|
436
|
-
}
|
|
437
|
-
});
|
|
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; } });
|
|
438
341
|
}
|
|
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); } } })();
|
|
440
|
-
|
|
441
|
-
async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
|
|
442
|
-
window.addEventListener('load', init);
|
|
443
|
-
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'); }
|
|
444
|
-
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); }
|
|
445
342
|
|
|
446
343
|
// ==========================================================================
|
|
447
|
-
// 9.
|
|
344
|
+
// 9. CONNESSIONE E SIMULATORE
|
|
448
345
|
// ==========================================================================
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
location.reload();
|
|
462
|
-
}
|
|
463
|
-
dC = 0;
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
})());
|
|
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
|
+
}
|
|
356
|
+
|
|
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; } }; })());
|
|
467
358
|
|
|
468
359
|
function startDynamicSimulation() {
|
|
469
360
|
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
|
-
|
|
361
|
+
let sim = { hdg: 45, tws: 12, twd: 45, depth: 12, stw: 5, leeway: 0, startTime: Date.now() };
|
|
481
362
|
simInterval = setInterval(() => {
|
|
482
363
|
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
483
|
-
|
|
484
|
-
// Vento: Rotazione lenta + Salto netto a 120s
|
|
485
364
|
if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
|
|
486
365
|
sim.twd = (sim.twd + (Math.sin(elapsed / 20) * 0.5) + 360) % 360;
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
sim.
|
|
492
|
-
|
|
493
|
-
// Barca segue il vento pigramente
|
|
494
|
-
sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
|
|
495
|
-
|
|
496
|
-
let twaRel = (sim.twd - sim.hdg + 360) % 360;
|
|
497
|
-
if (twaRel > 180) twaRel -= 360;
|
|
498
|
-
|
|
499
|
-
// Polare smussata
|
|
500
|
-
let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125));
|
|
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);
|
|
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);
|
|
510
372
|
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 =
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
processIncomingData("
|
|
515
|
-
processIncomingData("
|
|
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
|
-
|
|
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 });
|
|
525
378
|
}, 1000);
|
|
526
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
|
+
}
|