@sailingrotevista/rotevista-dash 6.2.7 → 7.0.1
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 +107 -586
- package/charts.js +274 -0
- package/gauge.js +81 -0
- package/index.html +3 -1
- package/index.js +21 -19
- package/package.json +1 -1
- package/radar.html +262 -119
- package/utils.js +139 -0
package/app.js
CHANGED
|
@@ -44,11 +44,9 @@ let simulationMode = false;
|
|
|
44
44
|
let displayModeSog = 'SOG';
|
|
45
45
|
let displayModeTws = 'TWS';
|
|
46
46
|
let socket, renderInterval, simInterval;
|
|
47
|
-
let lastAvgUIUpdate = 0
|
|
48
|
-
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
49
|
-
let curBoatCompassRot = 0, curWindCompassRot = 0;
|
|
47
|
+
let lastAvgUIUpdate = 0; // Chirurgico: Rimosse audioCtx e lastAlarmTime poiché sono già dichiarate in utils.js
|
|
50
48
|
|
|
51
|
-
let
|
|
49
|
+
let rotationTrend = 0, meteoTrend = 0;
|
|
52
50
|
let lastShortAvgVal = null, lastInstantTwa = null;
|
|
53
51
|
let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
|
|
54
52
|
let twDirty = false, isNavigating = false, reconnectDelay = 1000;
|
|
@@ -101,34 +99,8 @@ const ui = {
|
|
|
101
99
|
};
|
|
102
100
|
|
|
103
101
|
// ==========================================================================
|
|
104
|
-
// 3. UTILITIES (MATEMATICA, BUFFER E MEMORIA)
|
|
102
|
+
// 3. UTILITIES (MATEMATICA, BUFFER E MEMORIA) - [Esportate in utils.js]
|
|
105
103
|
// ==========================================================================
|
|
106
|
-
function radToDeg(rad) { return rad * (180 / Math.PI); }
|
|
107
|
-
function degToRad(deg) { return deg * (Math.PI / 180); }
|
|
108
|
-
function msToKts(ms) { return ms * 1.94384; }
|
|
109
|
-
function ktsToMs(kts) { return kts / 1.94384; }
|
|
110
|
-
|
|
111
|
-
function getShortestRotation(curr, target) {
|
|
112
|
-
let diff = (target - curr) % 360;
|
|
113
|
-
if (diff > 180) diff -= 360;
|
|
114
|
-
else if (diff < -180) diff += 360;
|
|
115
|
-
return curr + diff;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Scrive il testo nel DOM/SVG solo se il valore è realmente cambiato.
|
|
120
|
-
* Utilizza innerHTML perché è l'unico metodo sicuro al 100% per forzare il ridisegno dei testi negli SVG.
|
|
121
|
-
*/
|
|
122
|
-
function safeSetText(el, text) {
|
|
123
|
-
if (!el) return;
|
|
124
|
-
// Se l'elemento è parte di un SVG, usiamo textContent, altrimenti innerHTML
|
|
125
|
-
const isSVG = el instanceof SVGElement;
|
|
126
|
-
if (isSVG) {
|
|
127
|
-
if (el.textContent !== text) el.textContent = text;
|
|
128
|
-
} else {
|
|
129
|
-
if (el.innerHTML !== text) el.innerHTML = text;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
104
|
|
|
133
105
|
/**
|
|
134
106
|
* Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
|
|
@@ -155,68 +127,6 @@ function safePush(buffer, val, time) {
|
|
|
155
127
|
if (buffer.length > 36000) buffer.shift();
|
|
156
128
|
}
|
|
157
129
|
|
|
158
|
-
/**
|
|
159
|
-
* Media Circolare Vettoriale - Versione "Soft Outlier Rejection"
|
|
160
|
-
*/
|
|
161
|
-
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
|
|
162
|
-
now = now || Date.now();
|
|
163
|
-
const len = bufferArray.length;
|
|
164
|
-
if (len === 0) return null;
|
|
165
|
-
|
|
166
|
-
let sSin = 0, sCos = 0, count = 0;
|
|
167
|
-
let newestTime = 0, oldestTime = 0;
|
|
168
|
-
|
|
169
|
-
let pilotSin = 0, pilotCos = 0;
|
|
170
|
-
const pilotSamples = Math.min(len, 15);
|
|
171
|
-
for (let i = len - 1; i >= len - pilotSamples; i--) {
|
|
172
|
-
pilotSin += bufferArray[i].sin;
|
|
173
|
-
pilotCos += bufferArray[i].cos;
|
|
174
|
-
}
|
|
175
|
-
const pilotRad = Math.atan2(pilotSin, pilotCos);
|
|
176
|
-
const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
|
|
177
|
-
|
|
178
|
-
for (let i = len - 1; i >= 0; i--) {
|
|
179
|
-
const item = bufferArray[i];
|
|
180
|
-
if ((now - item.time) > windowMs) break;
|
|
181
|
-
|
|
182
|
-
let diffRad = Math.atan2(Math.sin(item.val - pilotRad), Math.cos(item.val - pilotRad));
|
|
183
|
-
let finalSin, finalCos;
|
|
184
|
-
|
|
185
|
-
if (Math.abs(diffRad) > limitRad) {
|
|
186
|
-
const clampedDiff = Math.sign(diffRad) * limitRad;
|
|
187
|
-
const clampedRad = pilotRad + clampedDiff;
|
|
188
|
-
finalSin = Math.sin(clampedRad);
|
|
189
|
-
const relativeCos = Math.cos(clampedRad);
|
|
190
|
-
finalCos = relativeCos;
|
|
191
|
-
} else {
|
|
192
|
-
finalSin = item.sin;
|
|
193
|
-
finalCos = item.cos;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
sSin += finalSin;
|
|
197
|
-
sCos += finalCos;
|
|
198
|
-
|
|
199
|
-
if (count === 0) newestTime = item.time;
|
|
200
|
-
oldestTime = item.time;
|
|
201
|
-
count++;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (count === 0) return null;
|
|
205
|
-
|
|
206
|
-
const R = Math.hypot(sSin, sCos) / count;
|
|
207
|
-
const avgRad = Math.atan2(sSin, sCos);
|
|
208
|
-
const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
|
|
209
|
-
const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
|
|
210
|
-
const safeR = Math.max(R, 1e-9);
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
val: finalVal,
|
|
214
|
-
stable: historyDuration > 10000 && R > CONFIG.averaging.stabilityThreshold,
|
|
215
|
-
dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
|
|
216
|
-
samples: count
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
130
|
function saveDashboardState() {
|
|
221
131
|
try {
|
|
222
132
|
const focusedBox = document.querySelector('.data-box.is-focused');
|
|
@@ -277,34 +187,8 @@ function loadDashboardState() {
|
|
|
277
187
|
}
|
|
278
188
|
|
|
279
189
|
// ==========================================================================
|
|
280
|
-
// 4. AUDIO E ALLARMI
|
|
190
|
+
// 4. AUDIO E ALLARMI - [Esportati in utils.js]
|
|
281
191
|
// ==========================================================================
|
|
282
|
-
function playBingBing() {
|
|
283
|
-
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
284
|
-
const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n;
|
|
285
|
-
function b(f, s) {
|
|
286
|
-
const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
|
|
287
|
-
o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f;
|
|
288
|
-
g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4);
|
|
289
|
-
o.start(s); o.stop(s + 0.5);
|
|
290
|
-
}
|
|
291
|
-
b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function playGybeAlarm() {
|
|
295
|
-
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
296
|
-
const n = Date.now(); if (n - lastAlarmTime < 2000) return; lastAlarmTime = n;
|
|
297
|
-
function note(f, s, d) {
|
|
298
|
-
const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
|
|
299
|
-
o.connect(g); g.connect(audioCtx.destination); o.type = 'square';
|
|
300
|
-
o.frequency.value = f; g.gain.setValueAtTime(0.05, s);
|
|
301
|
-
g.gain.exponentialRampToValueAtTime(0.001, s + d); o.start(s); o.stop(s + d);
|
|
302
|
-
}
|
|
303
|
-
for (let i = 0; i < 4; i++) {
|
|
304
|
-
note(1800, audioCtx.currentTime + (i * 0.15), 0.1);
|
|
305
|
-
note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
192
|
|
|
309
193
|
function checkDepthAlarm(m) {
|
|
310
194
|
ui.depth.classList.remove('alarm-warning', 'alarm-danger', 'blink-alarm');
|
|
@@ -316,12 +200,6 @@ function checkDepthAlarm(m) {
|
|
|
316
200
|
}
|
|
317
201
|
}
|
|
318
202
|
|
|
319
|
-
function updateLeewayDisplay(deg) {
|
|
320
|
-
const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125);
|
|
321
|
-
ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w);
|
|
322
|
-
ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
203
|
/**
|
|
326
204
|
* computeTrueWind: Calcola TWS, TWA e TWD strategico.
|
|
327
205
|
* Gestisce il fallback separato (Split-Fallback) in caso di dati parzialmente nativi di bordo.
|
|
@@ -670,22 +548,29 @@ function startDisplayLoop() {
|
|
|
670
548
|
updateWindTrend();
|
|
671
549
|
|
|
672
550
|
// --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
551
|
+
const isSocketOpen = socket && socket.readyState === WebSocket.OPEN;
|
|
552
|
+
|
|
553
|
+
if (isSocketOpen) {
|
|
554
|
+
ui.status.className = "online"; // Colore Verde
|
|
555
|
+
const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
556
|
+
const requiredMs = viewportMinutes * 60000;
|
|
557
|
+
const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
|
|
558
|
+
|
|
559
|
+
if (oldestStw) {
|
|
560
|
+
const availableMs = now - oldestStw.time;
|
|
561
|
+
if (availableMs >= requiredMs) {
|
|
562
|
+
ui.status.innerText = `ONLINE ${viewportMinutes}min`;
|
|
563
|
+
} else {
|
|
564
|
+
const availableMin = Math.max(1, Math.floor(availableMs / 60000));
|
|
565
|
+
ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
ui.status.innerText = `ONLINE`;
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
ui.status.className = "offline"; // Colore Rosso
|
|
572
|
+
ui.status.innerText = "OFFLINE"; // Chirurgico: Forza il testo a OFFLINE se il socket è chiuso, evitando scritte verdi in rosso
|
|
573
|
+
}
|
|
689
574
|
|
|
690
575
|
// --- WATCHDOG: CONTROLLO TIMEOUT ---
|
|
691
576
|
const watch = {
|
|
@@ -738,131 +623,93 @@ function startDisplayLoop() {
|
|
|
738
623
|
}
|
|
739
624
|
|
|
740
625
|
// --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
626
|
+
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
627
|
+
safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
|
|
628
|
+
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
629
|
+
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
630
|
+
}
|
|
746
631
|
|
|
747
|
-
|
|
632
|
+
// --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
|
|
633
|
+
|
|
634
|
+
// Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
|
|
635
|
+
const rawTws = store.raw["environment.wind.speedTrue"];
|
|
636
|
+
const rawAws = store.raw["environment.wind.speedApparent"];
|
|
748
637
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
|
|
776
|
-
ui.tws.style.setProperty('color', navyNight, 'important');
|
|
638
|
+
const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
|
|
639
|
+
const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
|
|
640
|
+
|
|
641
|
+
if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
|
|
642
|
+
if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
|
|
643
|
+
|
|
644
|
+
// Disegno testo casella destra (TWS o AWS)
|
|
645
|
+
if (rawTws !== undefined || rawAws !== undefined) {
|
|
646
|
+
const labelWind = document.getElementById('tws-aws-label');
|
|
647
|
+
const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
|
|
648
|
+
|
|
649
|
+
safeSetText(ui.tws, currentWind.toFixed(1));
|
|
650
|
+
if (labelWind) labelWind.textContent = displayModeTws;
|
|
651
|
+
|
|
652
|
+
if (currentWind >= CONFIG.graphs.reef2) {
|
|
653
|
+
ui.tws.style.setProperty('color', '#ff3b30', 'important');
|
|
654
|
+
} else if (currentWind >= CONFIG.graphs.reef1) {
|
|
655
|
+
ui.tws.style.setProperty('color', '#ff9800', 'important');
|
|
656
|
+
} else {
|
|
657
|
+
if (displayModeTws === 'AWS') {
|
|
658
|
+
ui.tws.style.setProperty('color', '#5c6bc0', 'important');
|
|
659
|
+
} else {
|
|
660
|
+
const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
|
|
661
|
+
ui.tws.style.setProperty('color', navyNight, 'important');
|
|
662
|
+
}
|
|
663
|
+
}
|
|
777
664
|
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
665
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (awsSvgEl) {
|
|
788
|
-
// Trucco anti-congelamento SVG: se il testo non combacia, lo forziamo usando
|
|
789
|
-
// textContent (standard SVG) invece di innerHTML (standard HTML).
|
|
790
|
-
if (awsSvgEl.textContent !== strVal) {
|
|
791
|
-
awsSvgEl.textContent = strVal;
|
|
666
|
+
// --- AGGIORNAMENTO DELLA BUSSOLA CENTRALE E MINI-COMPASS ---
|
|
667
|
+
updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
|
|
668
|
+
|
|
669
|
+
// --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
|
|
670
|
+
if (lastAvgUIUpdate++ % 10 === 0) {
|
|
671
|
+
saveDashboardState();
|
|
792
672
|
}
|
|
793
|
-
} else {
|
|
794
|
-
console.error("ERRORE: Elemento SVG bussola ('aws-val-svg') non trovato nel documento!");
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// --- PUNTATORI ANALOGICI ---
|
|
799
|
-
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
|
|
800
|
-
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
|
|
801
|
-
if (smAwa) ui.awa.setAttribute('transform', `rotate(${curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val))}, 200, 200)`);
|
|
802
|
-
if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
|
|
803
|
-
|
|
804
|
-
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
805
|
-
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
806
|
-
smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
807
|
-
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
808
|
-
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
809
|
-
ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#ff9800" : "";
|
|
810
|
-
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
|
|
814
|
-
if (lastAvgUIUpdate++ % 10 === 0) {
|
|
815
|
-
saveDashboardState();
|
|
816
|
-
}
|
|
817
673
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
674
|
+
if (lastAvgUIUpdate % 3 === 0) {
|
|
675
|
+
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
|
|
676
|
+
let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
|
|
677
|
+
let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true);
|
|
678
|
+
let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
|
|
679
|
+
let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
|
|
680
|
+
|
|
681
|
+
upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
|
|
682
|
+
upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
|
|
683
|
+
upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
|
|
684
|
+
upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
|
|
685
|
+
upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
|
|
686
|
+
|
|
687
|
+
if (hObj && twdObj) {
|
|
688
|
+
const reflectAngle = (targetRad, axisRad) => {
|
|
689
|
+
const dS = Math.sin(axisRad - targetRad);
|
|
690
|
+
const dC = Math.cos(axisRad - targetRad);
|
|
691
|
+
return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
|
|
692
|
+
};
|
|
693
|
+
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
694
|
+
if (!isNavigating) ui.tackHdg.innerHTML = "---°";
|
|
695
|
+
else if (unstableH) { ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data'); }
|
|
696
|
+
else {
|
|
697
|
+
const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
|
|
698
|
+
ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}°`;
|
|
699
|
+
ui.tackHdg.classList.remove('unstable-data');
|
|
700
|
+
}
|
|
701
|
+
if (cObj) {
|
|
702
|
+
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
703
|
+
if (!isNavigating) ui.tackCog.innerHTML = "---°";
|
|
704
|
+
else if (unstableC) { ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data'); }
|
|
705
|
+
else {
|
|
706
|
+
const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
|
|
707
|
+
ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}°`;
|
|
708
|
+
ui.tackCog.classList.remove('unstable-data');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
853
711
|
}
|
|
854
712
|
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
|
|
858
|
-
const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
859
|
-
if (smHdgIcons && smTwdIcons) {
|
|
860
|
-
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
|
|
861
|
-
ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
862
|
-
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val));
|
|
863
|
-
ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
713
|
}, RENDER_INTERVAL_MS);
|
|
867
714
|
}
|
|
868
715
|
|
|
@@ -1085,321 +932,6 @@ function manageHistory(type, value) {
|
|
|
1085
932
|
store.lastUpdates[type] = Math.floor(now / bucketIntervalMs) * bucketIntervalMs;
|
|
1086
933
|
}
|
|
1087
934
|
|
|
1088
|
-
/**
|
|
1089
|
-
* Gestione dinamica delle scale dei grafici (Involucro Elastico Snapped e Safety Zoom)
|
|
1090
|
-
* Implementa lo "Snap a Griglia" basato sull'hercSpan senza padding per tutti i sensori.
|
|
1091
|
-
*/
|
|
1092
|
-
function calculateScale(type, data, mode) {
|
|
1093
|
-
const s = CONFIG.scales[type];
|
|
1094
|
-
const currentVal = data[data.length - 1];
|
|
1095
|
-
|
|
1096
|
-
// Fallback di emergenza se il buffer è momentaneamente vuoto
|
|
1097
|
-
if (currentVal === undefined || currentVal === null) {
|
|
1098
|
-
return { min: 0, max: s ? s.stdMax : 10 };
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Inizializzazione della memoria delle scale nello store se non esiste
|
|
1102
|
-
if (!store.herculesScales) store.herculesScales = {};
|
|
1103
|
-
if (!store.herculesScales[type]) {
|
|
1104
|
-
store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
|
|
1105
|
-
}
|
|
1106
|
-
let currentScale = store.herculesScales[type];
|
|
1107
|
-
|
|
1108
|
-
// ==========================================================================
|
|
1109
|
-
// 1. SEZIONE PROFONDITÀ (DEDICATA A 2 MINUTI DI SICUREZZA, MINIMO FISSO A 0)
|
|
1110
|
-
// ==========================================================================
|
|
1111
|
-
if (type === 'depth') {
|
|
1112
|
-
const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
|
|
1113
|
-
if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
|
|
1114
|
-
|
|
1115
|
-
const now = Date.now();
|
|
1116
|
-
const depthSafetyWindowMs = 120000; // 2 minuti fissi per la sicurezza
|
|
1117
|
-
|
|
1118
|
-
// Estrazione dati reali degli ultimi 2 minuti con timestamp
|
|
1119
|
-
const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
|
|
1120
|
-
const recentVals = recentPoints.map(p => p.val);
|
|
1121
|
-
|
|
1122
|
-
const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
|
|
1123
|
-
const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
|
|
1124
|
-
|
|
1125
|
-
// --- 1.1 NORMALE PROFONDITÀ (CON SOGLIA STANDARD E FILTRO 2 MINUTI) ---
|
|
1126
|
-
if (mode !== 'hercules') {
|
|
1127
|
-
if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
|
|
1128
|
-
store.depthProtectedActive = true;
|
|
1129
|
-
}
|
|
1130
|
-
if (store.depthProtectedActive) {
|
|
1131
|
-
if (localMin > shallowThreshold) {
|
|
1132
|
-
store.depthProtectedActive = false;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
if (store.depthProtectedActive) {
|
|
1136
|
-
return { min: 0, max: shallowThreshold };
|
|
1137
|
-
}
|
|
1138
|
-
const maxHistorico = Math.max(...data);
|
|
1139
|
-
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// --- 1.2 HERCULES PROFONDITÀ (BASE 0 IN ACQUE BASSE, FLUTTUANTE AL LARGO) ---
|
|
1143
|
-
if (mode === 'hercules') {
|
|
1144
|
-
const roundStep = s.hercSpan; // Passo di griglia selezionato dall'utente
|
|
1145
|
-
const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
|
|
1146
|
-
|
|
1147
|
-
let targetMin = 0;
|
|
1148
|
-
// Se siamo in acque profonde, lasciamo che il minimo fluttui per mostrare i micro-dettagli
|
|
1149
|
-
if (localMin > shallowThreshold) {
|
|
1150
|
-
targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
1154
|
-
|
|
1155
|
-
// Impediamo uno span inferiore a 4 metri per evitare grafici piatti
|
|
1156
|
-
if (targetMax - targetMin < 4) {
|
|
1157
|
-
targetMax = targetMin + 4;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// Regola asimmetrica per il MIN e il MAX (Espansione istantanea, contrazione a 2 minuti)
|
|
1161
|
-
if (currentVal < currentScale.min || currentVal > currentScale.max) {
|
|
1162
|
-
currentScale.min = Math.min(currentScale.min, targetMin);
|
|
1163
|
-
currentScale.max = Math.max(currentScale.max, targetMax);
|
|
1164
|
-
} else {
|
|
1165
|
-
const allStableInTarget = recentVals.every(val => val >= targetMin && val <= targetMax);
|
|
1166
|
-
if (allStableInTarget) {
|
|
1167
|
-
currentScale.min = targetMin;
|
|
1168
|
-
currentScale.max = targetMax;
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
return { min: currentScale.min, max: currentScale.max };
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// ==========================================================================
|
|
1177
|
-
// 2. ALTRI GRAFICI (STW, SOG, TWS): COMPORTAMENTO ADATTIVO SU TERZO DEL GRAFICO
|
|
1178
|
-
// ==========================================================================
|
|
1179
|
-
|
|
1180
|
-
// --- 2.1 MODALITÀ NORMALE ---
|
|
1181
|
-
if (mode !== 'hercules') {
|
|
1182
|
-
const maxHistorico = Math.max(...data);
|
|
1183
|
-
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// --- 2.2 MODALITÀ HERCULES AD ALTO CONTRASTO (SNAP SENZA PADDING) ---
|
|
1187
|
-
if (mode === 'hercules') {
|
|
1188
|
-
const roundStep = s.hercSpan; // Passo di griglia (ex hercSpan)
|
|
1189
|
-
|
|
1190
|
-
const oneThirdCount = Math.max(1, Math.floor(data.length / 3));
|
|
1191
|
-
const recentThirdData = data.slice(-oneThirdCount);
|
|
1192
|
-
|
|
1193
|
-
const localMin = Math.min(...recentThirdData);
|
|
1194
|
-
const localMax = Math.max(...recentThirdData);
|
|
1195
|
-
|
|
1196
|
-
// Applichiamo lo snap diretto ai multipli della griglia
|
|
1197
|
-
let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
|
|
1198
|
-
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
1199
|
-
|
|
1200
|
-
// Se lo span calcolato è nullo (es. velocità costante), forziamo lo span minimo
|
|
1201
|
-
if (targetMax - targetMin === 0) {
|
|
1202
|
-
targetMax = targetMin + roundStep;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// APPLICAZIONE REGOLE ASIMMETRICHE ANTI-CLIPPING
|
|
1206
|
-
// A. Espansione ISTANTANEA in alto o in basso (sicurezza)
|
|
1207
|
-
if (currentVal < currentScale.min || currentVal > currentScale.max) {
|
|
1208
|
-
currentScale.min = Math.min(currentScale.min, targetMin);
|
|
1209
|
-
currentScale.max = Math.max(currentScale.max, targetMax);
|
|
1210
|
-
}
|
|
1211
|
-
// B. Contrazione RITARDATA (solo se tutto il terzo recente si è assestato nel target)
|
|
1212
|
-
else {
|
|
1213
|
-
const allStableInTarget = recentThirdData.every(val => val >= targetMin && val <= targetMax);
|
|
1214
|
-
if (allStableInTarget) {
|
|
1215
|
-
currentScale.min = targetMin;
|
|
1216
|
-
currentScale.max = targetMax;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
return { min: currentScale.min, max: currentScale.max };
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
function updateScaleLabels(t, min, max) {
|
|
1225
|
-
const el = document.getElementById(t + '-scale');
|
|
1226
|
-
if (el) el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
/**
|
|
1230
|
-
* refreshGraph: Recupero dati, switch AWS/TWS/VMG e passaggio a motore grafico.
|
|
1231
|
-
* Redirige e protegge i canali secondari (AWS/VMG) evitando ridisegni inutili.
|
|
1232
|
-
*/
|
|
1233
|
-
function refreshGraph(t) {
|
|
1234
|
-
// Redirezione di sicurezza dei canali secondari (AWS / VMG) verso i contenitori principali
|
|
1235
|
-
if (t === 'aws') {
|
|
1236
|
-
if (displayModeTws !== 'AWS') return; // Se non stiamo visualizzando AWS a schermo, ignora il rinfresco
|
|
1237
|
-
t = 'tws';
|
|
1238
|
-
}
|
|
1239
|
-
if (t === 'vmg') {
|
|
1240
|
-
if (displayModeSog !== 'VMG') return; // Se non stiamo visualizzando VMG a schermo, ignora il rinfresco
|
|
1241
|
-
t = 'sog';
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
const boxType = t;
|
|
1245
|
-
let rawData;
|
|
1246
|
-
|
|
1247
|
-
if (t === 'tws' && displayModeTws === 'AWS') {
|
|
1248
|
-
rawData = store.histories['aws'];
|
|
1249
|
-
} else {
|
|
1250
|
-
rawData = store.histories[t];
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
if (!rawData || rawData.length < 2) return;
|
|
1254
|
-
|
|
1255
|
-
const values = rawData.map(p => p.val);
|
|
1256
|
-
const mode = graphModes[boxType];
|
|
1257
|
-
const cfg = calculateScale(boxType, values, mode);
|
|
1258
|
-
|
|
1259
|
-
const box = document.querySelector(`.box-${boxType}`);
|
|
1260
|
-
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
1261
|
-
|
|
1262
|
-
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
1263
|
-
drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
/**
|
|
1267
|
-
* drawGraph: Motore SVG con Timeline Reale e Gestione GAP
|
|
1268
|
-
* Risolve i conflitti di orologio (Clock Drift) tra Cerbo GX e Tablet.
|
|
1269
|
-
* Integra la reattività dello spessore della curva in modalità Dual Screen (Focus).
|
|
1270
|
-
*/
|
|
1271
|
-
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
1272
|
-
const svg = document.getElementById(id);
|
|
1273
|
-
if (!svg || d.length < 2) return;
|
|
1274
|
-
|
|
1275
|
-
const w = 200, h = 40;
|
|
1276
|
-
const range = max - min || 1;
|
|
1277
|
-
const isDepth = (id === 'depth-graph');
|
|
1278
|
-
|
|
1279
|
-
// --- RISOLUZIONE DISALLINEAMENTO ORARIO ---
|
|
1280
|
-
// Usiamo il tempo del sensore (l'ultimo dato ricevuto) invece dell'orologio del tablet
|
|
1281
|
-
const latestPoint = d[d.length - 1];
|
|
1282
|
-
const now = latestPoint ? latestPoint.time : Date.now();
|
|
1283
|
-
|
|
1284
|
-
// --- RILEVAMENTO DINAMICO DUAL SCREEN BLINDATO ---
|
|
1285
|
-
const box = svg.closest('.data-box');
|
|
1286
|
-
const isFocused = isFocusActive && box && box.classList.contains('is-focused');
|
|
1287
|
-
|
|
1288
|
-
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
1289
|
-
const viewportMs = visibleMinutes * 60000;
|
|
1290
|
-
const viewportStart = now - viewportMs;
|
|
1291
|
-
|
|
1292
|
-
const visibleData = d.filter(p => p.time >= viewportStart);
|
|
1293
|
-
if (visibleData.length < 2) return;
|
|
1294
|
-
|
|
1295
|
-
const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
|
|
1296
|
-
const colDepth = "#0088cc", colStw = "#00C851", colStwBorder = "#007a3d", colSog = "#ffbb33", colVmg = "#00b8d4";
|
|
1297
|
-
|
|
1298
|
-
const getColorProps = (val) => {
|
|
1299
|
-
// Se siamo in Dual Screen (focus), aumentiamo lo spessore base della curva di un filino (da 1.6 a 2.2)
|
|
1300
|
-
const baseStroke = isFocused ? "4.2" : "1.6";
|
|
1301
|
-
const alertStroke = isFocused ? "4.8" : "2.2";
|
|
1302
|
-
const warnStroke = isFocused ? "4.4" : "1.8";
|
|
1303
|
-
|
|
1304
|
-
let color = colTws, opacity = "0.15", stroke = baseStroke;
|
|
1305
|
-
if (isTws) {
|
|
1306
|
-
const baseWind = (displayModeTws === 'AWS') ? colAws : colTws;
|
|
1307
|
-
if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
|
|
1308
|
-
else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
|
|
1309
|
-
else color = baseWind;
|
|
1310
|
-
} else if (isDepth) {
|
|
1311
|
-
if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
|
|
1312
|
-
else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
|
|
1313
|
-
else color = colDepth;
|
|
1314
|
-
} else {
|
|
1315
|
-
if (id === 'stw-graph') color = colStw;
|
|
1316
|
-
else if (id === 'sog-graph') color = (displayModeSog === 'VMG') ? colVmg : colSog;
|
|
1317
|
-
}
|
|
1318
|
-
return { color, opacity, stroke };
|
|
1319
|
-
};
|
|
1320
|
-
|
|
1321
|
-
let grids = "";
|
|
1322
|
-
// --- GRIGLIE ORIZZONTALI (Spessore fisso a 0.5px, non deformabile) ---
|
|
1323
|
-
[0.25, 0.5, 0.75].forEach(p => grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(0,0,0,0.12)" stroke-width="0.5" vector-effect="non-scaling-stroke" />`);
|
|
1324
|
-
|
|
1325
|
-
// --- GRIGLIE VERTICALI (Spessore fisso a 0.4px, non deformabile) ---
|
|
1326
|
-
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
1327
|
-
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
1328
|
-
const x = w - ((m / visibleMinutes) * w);
|
|
1329
|
-
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.4" vector-effect="non-scaling-stroke" />`;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
// --- DISEGNO LINEE DI SICUREZZA ALLARME PROFONDITÀ (DANGER & WARNING) ---
|
|
1333
|
-
if (isDepth) {
|
|
1334
|
-
const dangerVal = CONFIG.alarms.depthDanger; // Es. 2.5m
|
|
1335
|
-
const warningVal = CONFIG.alarms.depthWarning; // Es. 3.5m
|
|
1336
|
-
const marginX = 4; // Margine per non toccare i bordi esterni
|
|
1337
|
-
|
|
1338
|
-
// Spessore dinamico per gli allarmi: 4.8px in Dual Screen, 1.8px nella griglia normale
|
|
1339
|
-
const alarmStrokeWidth = isFocused ? "4.8" : "1.8";
|
|
1340
|
-
|
|
1341
|
-
// Linea Rossa Viva (Spessore dinamico, con il tuo tratteggio alternato personalizzato)
|
|
1342
|
-
if (dangerVal >= min && dangerVal <= max) {
|
|
1343
|
-
const p = (dangerVal - min) / range;
|
|
1344
|
-
const y = h - (p * h);
|
|
1345
|
-
grids += `<line x1="${marginX}" y1="${y}" x2="${w - marginX}" y2="${y}" stroke="rgba(255, 59, 48, 0.95)" stroke-width="${alarmStrokeWidth}" stroke-dasharray="12, 6, 2, 6" vector-effect="non-scaling-stroke" />`;
|
|
1346
|
-
}
|
|
1347
|
-
// Linea Giallo Oro Viva (Spessore dinamico, con il tuo tratteggio alternato personalizzato)
|
|
1348
|
-
if (warningVal >= min && warningVal <= max) {
|
|
1349
|
-
const p = (warningVal - min) / range;
|
|
1350
|
-
const y = h - (p * h);
|
|
1351
|
-
grids += `<line x1="${marginX}" y1="${y}" x2="${w - marginX}" y2="${y}" stroke="rgba(255, 204, 0, 0.95)" stroke-width="${alarmStrokeWidth}" stroke-dasharray="6 , 2, 6, 12" vector-effect="non-scaling-stroke" />`;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
let gradientStops = "", lines = "", areaPath = "";
|
|
1356
|
-
let started = false;
|
|
1357
|
-
|
|
1358
|
-
for (let i = 1; i < visibleData.length; i++) {
|
|
1359
|
-
const pA = visibleData[i - 1];
|
|
1360
|
-
const pB = visibleData[i];
|
|
1361
|
-
|
|
1362
|
-
const x1 = ((pA.time - viewportStart) / viewportMs) * w;
|
|
1363
|
-
const x2 = ((pB.time - viewportStart) / viewportMs) * w;
|
|
1364
|
-
const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
|
|
1365
|
-
const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
|
|
1366
|
-
|
|
1367
|
-
const props = getColorProps(pB.val);
|
|
1368
|
-
const deltaTime = pB.time - pA.time;
|
|
1369
|
-
const expectedInterval = viewportMs / CONFIG.graphs.samples;
|
|
1370
|
-
const isGap = deltaTime > (expectedInterval * 2.5);
|
|
1371
|
-
|
|
1372
|
-
const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
|
|
1373
|
-
gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
1374
|
-
gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
1375
|
-
|
|
1376
|
-
if (isGap) {
|
|
1377
|
-
if (started) {
|
|
1378
|
-
areaPath += `L ${x1} ${h} `;
|
|
1379
|
-
started = false;
|
|
1380
|
-
}
|
|
1381
|
-
} else {
|
|
1382
|
-
// Curva del grafico: spessore reattivo al focus e protezione da deformazione (non-scaling)
|
|
1383
|
-
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" vector-effect="non-scaling-stroke" />`;
|
|
1384
|
-
if (!started) {
|
|
1385
|
-
areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
|
|
1386
|
-
started = true;
|
|
1387
|
-
}
|
|
1388
|
-
areaPath += `L ${x2} ${y2} `;
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
if (started) {
|
|
1393
|
-
const last = visibleData[visibleData.length - 1];
|
|
1394
|
-
const lastX = ((last.time - viewportStart) / viewportMs) * w;
|
|
1395
|
-
areaPath += `L ${lastX} ${h} Z`;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
const gradId = `grad-${id}`;
|
|
1399
|
-
const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
|
|
1400
|
-
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
935
|
// ==========================================================================
|
|
1404
936
|
// 9. INTERAZIONI E GESTI
|
|
1405
937
|
// ==========================================================================
|
|
@@ -1565,21 +1097,10 @@ function connect() {
|
|
|
1565
1097
|
// 11. INIT E CICLO DI VITA
|
|
1566
1098
|
// ==========================================================================
|
|
1567
1099
|
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
1568
|
-
(function genTicks() {
|
|
1569
|
-
const c = document.getElementById('ticks');
|
|
1570
|
-
if (c) {
|
|
1571
|
-
for (let i = 0; i < 360; i += 10) {
|
|
1572
|
-
const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
1573
|
-
const m = i % 30 === 0;
|
|
1574
|
-
l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50));
|
|
1575
|
-
l.setAttribute("stroke", m ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1");
|
|
1576
|
-
l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l);
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
})();
|
|
1580
1100
|
|
|
1581
1101
|
async function init() {
|
|
1582
1102
|
loadDashboardState();
|
|
1103
|
+
initCompassTicks(); // Genera i ticks sul quadrante usando il modulo gauge.js
|
|
1583
1104
|
|
|
1584
1105
|
// Rileviamo se siamo sul Mac tramite file:// (Ambiente di sviluppo locale)
|
|
1585
1106
|
const isLocalFile = (window.location.protocol === 'file:');
|