@sailingrotevista/rotevista-dash 6.2.4 → 6.2.6
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 +1 -1
- package/index.js +7 -7
- package/package.json +1 -1
- package/radar.html +10 -28
- package/weather_debug.html +181 -0
- package/radar copia.html +0 -490
- /package/{rarad_test.html → radaar debug.html} +0 -0
package/app.js
CHANGED
|
@@ -22,7 +22,7 @@ let CONFIG = {
|
|
|
22
22
|
minSpeed: 0.5,
|
|
23
23
|
stabilityBreakout: 15
|
|
24
24
|
},
|
|
25
|
-
graphs: { reef1:
|
|
25
|
+
graphs: { reef1: 15, reef2: 20, historyMinutes: 10, samples: 60 },
|
|
26
26
|
scales: {
|
|
27
27
|
stw: { stdMax: 4, hercSpan: 2, step: 1 },
|
|
28
28
|
sog: { stdMax: 4, hercSpan: 2, step: 1 },
|
package/index.js
CHANGED
|
@@ -74,7 +74,8 @@ module.exports = function (app) {
|
|
|
74
74
|
const responseData = {
|
|
75
75
|
...histories,
|
|
76
76
|
windRadarSlots: windRadarSlots,
|
|
77
|
-
futureForecast: futureForecast
|
|
77
|
+
futureForecast: futureForecast,
|
|
78
|
+
'navigation.position': raw['navigation.position'] // Chirurgico: Espone le coordinate GPS correnti per la diagnostica e il radar
|
|
78
79
|
};
|
|
79
80
|
res.json(responseData);
|
|
80
81
|
});
|
|
@@ -86,6 +87,7 @@ module.exports = function (app) {
|
|
|
86
87
|
const localSubscription = {
|
|
87
88
|
context: 'vessels.self',
|
|
88
89
|
subscribe: [
|
|
90
|
+
{ path: 'navigation.position' }, // Chirurgico: Aggiunto l'ascolto della posizione GPS per abilitare le previsioni
|
|
89
91
|
{ path: 'navigation.speedThroughWater' },
|
|
90
92
|
{ path: 'navigation.speedOverGround' },
|
|
91
93
|
{ path: 'environment.depth.belowTransducer' },
|
|
@@ -191,7 +193,7 @@ module.exports = function (app) {
|
|
|
191
193
|
const awa = raw["environment.wind.angleApparent"];
|
|
192
194
|
const stw = raw["navigation.speedThroughWater"] || 0;
|
|
193
195
|
const sog = raw["navigation.speedOverGround"] || 0;
|
|
194
|
-
const hdg = raw["navigation.headingTrue"]
|
|
196
|
+
const hdg = raw["navigation.headingTrue"];
|
|
195
197
|
const cog = raw["navigation.courseOverGroundTrue"] || 0;
|
|
196
198
|
|
|
197
199
|
if (aws !== undefined && awa !== undefined) {
|
|
@@ -358,11 +360,8 @@ module.exports = function (app) {
|
|
|
358
360
|
|
|
359
361
|
// EMISSIONE DEL DELTA: Se abbiamo calcolato il TWD di fallback, lo trasmettiamo a Signal K
|
|
360
362
|
if (now - lastNativeTwdTime > 5000) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
min: finalValue.min,
|
|
364
|
-
max: finalValue.max
|
|
365
|
-
});
|
|
363
|
+
// Standard Signal K: trasmettiamo solo il valore medio (float numerico in radianti)
|
|
364
|
+
emitDelta('environment.wind.directionTrue', finalValue.val);
|
|
366
365
|
}
|
|
367
366
|
|
|
368
367
|
// --- TRIGGER DI CONGELAMENTO ARCO (Ogni :00 e :30 dell'orologio) ---
|
|
@@ -646,6 +645,7 @@ module.exports = function (app) {
|
|
|
646
645
|
https.get(url, (res) => {
|
|
647
646
|
if (res.statusCode !== 200) {
|
|
648
647
|
app.error(`[Open-Meteo] HTTP Error: ${res.statusCode}`);
|
|
648
|
+
res.resume();
|
|
649
649
|
lastForecast30mSlot = 0; // Reset in caso di errore per permettere un tentativo al prossimo pacchetto GPS
|
|
650
650
|
return;
|
|
651
651
|
}
|
package/package.json
CHANGED
package/radar.html
CHANGED
|
@@ -396,32 +396,8 @@
|
|
|
396
396
|
let sourceLabel = u.$source || (u.source ? (u.source.label || "N2K") : "NMEA");
|
|
397
397
|
if (u.values) {
|
|
398
398
|
u.values.forEach(v => {
|
|
399
|
+
// Mantiene l'aggiornamento dei testi di debug istantanei a 1Hz
|
|
399
400
|
processIncomingDelta(v.path, v.value, sourceLabel, timeMs);
|
|
400
|
-
|
|
401
|
-
// Ricezione live delle emissioni delta del nostro plugin server
|
|
402
|
-
if (v.path === 'environment.wind.directionTrue') {
|
|
403
|
-
let val, min, max;
|
|
404
|
-
if (v.value && typeof v.value === 'object' && v.value.val !== undefined) {
|
|
405
|
-
val = v.value.val;
|
|
406
|
-
min = v.value.min;
|
|
407
|
-
max = v.value.max;
|
|
408
|
-
} else {
|
|
409
|
-
val = v.value; min = v.value; max = v.value;
|
|
410
|
-
}
|
|
411
|
-
store.twdMinuteBuffer.push({ val: val, min: min, max: max, time: timeMs });
|
|
412
|
-
while(store.twdMinuteBuffer.length > 120) store.twdMinuteBuffer.shift();
|
|
413
|
-
renderRadar();
|
|
414
|
-
}
|
|
415
|
-
if (v.path === 'environment.wind.speedTrue') {
|
|
416
|
-
let val;
|
|
417
|
-
if (v.value && typeof v.value === 'object' && v.value.val !== undefined) {
|
|
418
|
-
val = v.value.val;
|
|
419
|
-
} else {
|
|
420
|
-
val = v.value;
|
|
421
|
-
}
|
|
422
|
-
store.twsMinuteBuffer.push({ val: val, time: timeMs });
|
|
423
|
-
while(store.twsMinuteBuffer.length > 120) store.twsMinuteBuffer.shift();
|
|
424
|
-
}
|
|
425
401
|
});
|
|
426
402
|
}
|
|
427
403
|
});
|
|
@@ -747,11 +723,17 @@
|
|
|
747
723
|
function init() {
|
|
748
724
|
drawCompassTicks();
|
|
749
725
|
connect();
|
|
750
|
-
|
|
751
|
-
|
|
726
|
+
|
|
727
|
+
// Sincronizzazione strategica: scarica lo storico consolidato dal server ogni 30 secondi e ridisegna
|
|
728
|
+
setInterval(async () => {
|
|
729
|
+
await fetchConfigAndHistory();
|
|
730
|
+
renderRadar();
|
|
731
|
+
}, 30000);
|
|
752
732
|
}
|
|
753
733
|
|
|
754
|
-
|
|
734
|
+
window.onload = init;
|
|
735
|
+
|
|
736
|
+
window.onload = init;
|
|
755
737
|
</script>
|
|
756
738
|
</body>
|
|
757
739
|
</html>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="it">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Sailing Dashboard - Open-Meteo Diagnostic Tool</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
background-color: #0f172a;
|
|
10
|
+
color: #e2e8f0;
|
|
11
|
+
font-family: monospace;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
margin: 0;
|
|
14
|
+
}
|
|
15
|
+
h1 {
|
|
16
|
+
color: #38bdf8;
|
|
17
|
+
border-bottom: 2px solid #38bdf8;
|
|
18
|
+
padding-bottom: 10px;
|
|
19
|
+
}
|
|
20
|
+
.card {
|
|
21
|
+
background: #1e293b;
|
|
22
|
+
border: 1px solid #334155;
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
padding: 15px;
|
|
25
|
+
margin-bottom: 20px;
|
|
26
|
+
}
|
|
27
|
+
button {
|
|
28
|
+
background: #0ea5e9;
|
|
29
|
+
color: #fff;
|
|
30
|
+
border: none;
|
|
31
|
+
padding: 10px 20px;
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
font-weight: bold;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
}
|
|
37
|
+
button:hover { background: #0284c7; }
|
|
38
|
+
pre {
|
|
39
|
+
background: #020617;
|
|
40
|
+
padding: 15px;
|
|
41
|
+
border-radius: 6px;
|
|
42
|
+
overflow: auto;
|
|
43
|
+
max-height: 250px;
|
|
44
|
+
font-size: 12px;
|
|
45
|
+
color: #34d399;
|
|
46
|
+
border: 1px solid #1e293b;
|
|
47
|
+
}
|
|
48
|
+
.success { color: #4ade80; font-weight: bold; }
|
|
49
|
+
.error { color: #f87171; font-weight: bold; }
|
|
50
|
+
.pending { color: #fbbf24; font-weight: bold; }
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
|
|
55
|
+
<h1>Diagnostic Tool: Open-Meteo & GPS</h1>
|
|
56
|
+
<p>Usa questo pannello per verificare se la barca riesce a scaricare le previsioni meteo.</p>
|
|
57
|
+
|
|
58
|
+
<!-- STEP 1: VERIFICA GPS INTERNO DI BORDO -->
|
|
59
|
+
<div class="card">
|
|
60
|
+
<h3>1. Coordinate GPS correnti dal Cerbo GX (Signal K)</h3>
|
|
61
|
+
<button onclick="checkBoatGPS()">Leggi GPS Barca</button>
|
|
62
|
+
<p>Stato: <span id="gps-status" class="pending">In attesa...</span></p>
|
|
63
|
+
<pre id="gps-data">Nessun dato letto.</pre>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- STEP 2: TEST DI CONNESSIONE DIRETTO DAL TABLET -->
|
|
67
|
+
<div class="card">
|
|
68
|
+
<h3>2. Test connessione Open-Meteo diretto dal Tablet (Questo Browser)</h3>
|
|
69
|
+
<p>Verifica se il tuo tablet ha accesso a internet in questo momento.</p>
|
|
70
|
+
<button onclick="testTabletConnection()">Esegui Test Tablet</button>
|
|
71
|
+
<p>Risultato: <span id="tablet-status" class="pending">In attesa...</span></p>
|
|
72
|
+
<pre id="tablet-data">Nessun tentativo eseguito.</pre>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- STEP 3: ANALISI STORICO DEL SERVER -->
|
|
76
|
+
<div class="card">
|
|
77
|
+
<h3>3. Verifica Previsione salvata sul Server (/rotevista-history)</h3>
|
|
78
|
+
<p>Verifica se il Cerbo ha memorizzato la previsione oraria.</p>
|
|
79
|
+
<button onclick="checkServerHistory()">Interroga Server</button>
|
|
80
|
+
<p>Stato Previsione: <span id="server-status" class="pending">In attesa...</span></p>
|
|
81
|
+
<pre id="server-data">Nessun tentativo eseguito.</pre>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<script>
|
|
85
|
+
const fallbackIp = "192.168.111.240:3000";
|
|
86
|
+
|
|
87
|
+
function getApiUrl(path) {
|
|
88
|
+
let addr = window.location.host || fallbackIp;
|
|
89
|
+
if (window.location.protocol === 'file:') {
|
|
90
|
+
return `http://${fallbackIp}${path}`;
|
|
91
|
+
}
|
|
92
|
+
return `${window.location.protocol}//${addr}${path}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 1. Legge il GPS dal server di bordo
|
|
96
|
+
async function checkBoatGPS() {
|
|
97
|
+
const status = document.getElementById('gps-status');
|
|
98
|
+
const dataPre = document.getElementById('gps-data');
|
|
99
|
+
status.innerHTML = "Interrogazione in corso...";
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(getApiUrl('/rotevista-history'));
|
|
103
|
+
if (res.ok) {
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
if (data['navigation.position']) {
|
|
106
|
+
const pos = data['navigation.position'];
|
|
107
|
+
status.className = "success";
|
|
108
|
+
status.innerText = "GPS ATTIVO DI BORDO";
|
|
109
|
+
dataPre.innerText = JSON.stringify(pos, null, 2);
|
|
110
|
+
} else {
|
|
111
|
+
status.className = "error";
|
|
112
|
+
status.innerText = "GPS RAGGIUNGIBILE MA SENZA COORDINATE";
|
|
113
|
+
dataPre.innerText = "La chiave 'navigation.position' non è presente nello storico del server.";
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
throw new Error(`Risposta server non OK: ${res.status}`);
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
status.className = "error";
|
|
120
|
+
status.innerText = "ERRORE DI RETE";
|
|
121
|
+
dataPre.innerText = err.message;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Test connessione Open-Meteo dal Tablet
|
|
126
|
+
async function testTabletConnection() {
|
|
127
|
+
const status = document.getElementById('tablet-status');
|
|
128
|
+
const dataPre = document.getElementById('tablet-data');
|
|
129
|
+
status.innerHTML = "Tentativo di connessione a Open-Meteo...";
|
|
130
|
+
|
|
131
|
+
// Coordinate di test (La Spezia)
|
|
132
|
+
const url = "https://api.open-meteo.com/v1/forecast?latitude=44.11&longitude=9.83&hourly=wind_speed_10m&wind_speed_unit=kn&forecast_days=1";
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetch(url);
|
|
136
|
+
if (res.ok) {
|
|
137
|
+
const json = await res.json();
|
|
138
|
+
status.className = "success";
|
|
139
|
+
status.innerText = "CONNESSIONE OK - Il Tablet ha internet";
|
|
140
|
+
dataPre.innerText = JSON.stringify(json.hourly.time.slice(0, 3).map((t, i) => `${t}: ${json.hourly.wind_speed_10m[i]} nodi`), null, 2);
|
|
141
|
+
} else {
|
|
142
|
+
throw new Error(`Errore API: ${res.status}`);
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
status.className = "error";
|
|
146
|
+
status.innerText = "ERRORE - Il tablet NON ha internet o Open-Meteo è offline";
|
|
147
|
+
dataPre.innerText = err.message + "\n\nNota: Se sei connesso al Wi-Fi locale della barca privo di SIM, è normale che il test fallisca.";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 3. Verifica se il server ha la previsione
|
|
152
|
+
async function checkServerHistory() {
|
|
153
|
+
const status = document.getElementById('server-status');
|
|
154
|
+
const dataPre = document.getElementById('server-data');
|
|
155
|
+
status.innerHTML = "Lettura storico dal Cerbo GX...";
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch(getApiUrl('/rotevista-history'));
|
|
159
|
+
if (res.ok) {
|
|
160
|
+
const data = await res.json();
|
|
161
|
+
if (data.futureForecast) {
|
|
162
|
+
status.className = "success";
|
|
163
|
+
status.innerText = "METEO PRESENTE NEL SERVER";
|
|
164
|
+
dataPre.innerText = JSON.stringify(data.futureForecast, null, 2);
|
|
165
|
+
} else {
|
|
166
|
+
status.className = "error";
|
|
167
|
+
status.innerText = "PREVISIONE ASSENTE (futureForecast è null)";
|
|
168
|
+
dataPre.innerText = "Il server è attivo ma non ha ancora scaricato o elaborato dati di previsione da Open-Meteo.";
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
throw new Error(`Risposta server non OK: ${res.status}`);
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
status.className = "error";
|
|
175
|
+
status.innerText = "ERRORE LETTURA STORICO";
|
|
176
|
+
dataPre.innerText = err.message;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
</script>
|
|
180
|
+
</body>
|
|
181
|
+
</html>
|
package/radar copia.html
DELETED
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="it">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Wind Radar - Twin Light Compass Calibrata (v6.0)</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root {
|
|
9
|
-
/* Dimensione dinamica reattiva del widget quadrato */
|
|
10
|
-
--box-size: 400px;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
body {
|
|
14
|
-
background-color: #ffffff;
|
|
15
|
-
color: #000000;
|
|
16
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
17
|
-
display: flex;
|
|
18
|
-
flex-direction: column;
|
|
19
|
-
align-items: center;
|
|
20
|
-
justify-content: center;
|
|
21
|
-
min-height: 100vh;
|
|
22
|
-
margin: 0;
|
|
23
|
-
padding: 20px;
|
|
24
|
-
box-sizing: border-box;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
h1 {
|
|
28
|
-
color: #000;
|
|
29
|
-
font-size: 1.3rem;
|
|
30
|
-
margin-bottom: 5px;
|
|
31
|
-
letter-spacing: 1px;
|
|
32
|
-
text-transform: uppercase;
|
|
33
|
-
font-weight: 700;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
p {
|
|
37
|
-
color: #666;
|
|
38
|
-
font-size: 0.85rem;
|
|
39
|
-
margin-top: 0;
|
|
40
|
-
margin-bottom: 25px;
|
|
41
|
-
text-align: center;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.container {
|
|
45
|
-
display: flex;
|
|
46
|
-
flex-wrap: wrap;
|
|
47
|
-
gap: 30px;
|
|
48
|
-
align-items: flex-start;
|
|
49
|
-
justify-content: center;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.controls-panel {
|
|
53
|
-
background: #fdfdfd;
|
|
54
|
-
border: 1px solid #e5e5e5;
|
|
55
|
-
padding: 20px;
|
|
56
|
-
border-radius: 12px;
|
|
57
|
-
width: 320px;
|
|
58
|
-
box-sizing: border-box;
|
|
59
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.control-group {
|
|
63
|
-
margin-bottom: 15px;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.control-group label {
|
|
67
|
-
display: block;
|
|
68
|
-
font-size: 0.75rem;
|
|
69
|
-
font-weight: bold;
|
|
70
|
-
color: #666;
|
|
71
|
-
text-transform: uppercase;
|
|
72
|
-
margin-bottom: 8px;
|
|
73
|
-
letter-spacing: 0.5px;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.control-group input[type="range"] {
|
|
77
|
-
width: 100%;
|
|
78
|
-
accent-color: #00C851;
|
|
79
|
-
background: #ddd;
|
|
80
|
-
height: 4px;
|
|
81
|
-
border-radius: 2px;
|
|
82
|
-
outline: none;
|
|
83
|
-
cursor: pointer;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.val-display {
|
|
87
|
-
float: right;
|
|
88
|
-
color: #000;
|
|
89
|
-
font-weight: bold;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/* BOX STRUTTURALE SCALABILE */
|
|
93
|
-
.data-box-square {
|
|
94
|
-
position: relative;
|
|
95
|
-
width: var(--box-size);
|
|
96
|
-
height: var(--box-size);
|
|
97
|
-
background: rgb(255, 255, 255);
|
|
98
|
-
border: 1px solid #eee;
|
|
99
|
-
border-radius: 12px;
|
|
100
|
-
box-sizing: border-box;
|
|
101
|
-
display: flex;
|
|
102
|
-
align-items: center;
|
|
103
|
-
justify-content: center;
|
|
104
|
-
overflow: hidden;
|
|
105
|
-
transition: width 0.1s ease, height 0.1s ease;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.label-row {
|
|
109
|
-
position: absolute;
|
|
110
|
-
top: 10px;
|
|
111
|
-
left: 12px;
|
|
112
|
-
right: 12px;
|
|
113
|
-
display: flex;
|
|
114
|
-
justify-content: space-between;
|
|
115
|
-
align-items: baseline;
|
|
116
|
-
pointer-events: none;
|
|
117
|
-
z-index: 10;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.label {
|
|
121
|
-
color: #888;
|
|
122
|
-
font-size: 0.65rem;
|
|
123
|
-
font-weight: bold;
|
|
124
|
-
text-transform: uppercase;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.unit {
|
|
128
|
-
color: #aaa;
|
|
129
|
-
font-size: 0.6rem;
|
|
130
|
-
font-weight: bold;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/* Forza l'SVG a riempire lo spazio a disposizione mantenendo le proporzioni */
|
|
134
|
-
svg {
|
|
135
|
-
display: block;
|
|
136
|
-
width: 100%;
|
|
137
|
-
height: 100%;
|
|
138
|
-
max-height: 100%;
|
|
139
|
-
object-fit: contain;
|
|
140
|
-
}
|
|
141
|
-
</style>
|
|
142
|
-
</head>
|
|
143
|
-
<body>
|
|
144
|
-
|
|
145
|
-
<h1>Tactical Wind Radar (v6.0)</h1>
|
|
146
|
-
<p>Calibrazione Spaziatura: Compressione proporzionale degli anelli con luce di 5px dal dial</p>
|
|
147
|
-
|
|
148
|
-
<div class="container">
|
|
149
|
-
|
|
150
|
-
<div class="controls-panel">
|
|
151
|
-
|
|
152
|
-
<!-- SLIDER DI ZOOM SCALABILITÀ -->
|
|
153
|
-
<div class="control-group" style="padding-bottom: 15px; border-bottom: 1px solid #eee; margin-bottom: 20px;">
|
|
154
|
-
<label style="color: #0088cc;">Zoom Box / Scalabilità <span class="val-display" id="scale-val" style="color: #0088cc;">400 px</span></label>
|
|
155
|
-
<input type="range" id="slider-scale" min="200" max="800" value="400" step="10" oninput="resizeBox()">
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
<div class="control-group">
|
|
159
|
-
<label>Reef 1 (Limite 1) <span class="val-display" id="r1-val">18 kts</span></label>
|
|
160
|
-
<input type="range" id="slider-r1" min="10" max="22" value="18" step="1" oninput="updateReefs()">
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
|
-
<div class="control-group">
|
|
164
|
-
<label>Reef 2 (Limite 2) <span class="val-display" id="r2-val">24 kts</span></label>
|
|
165
|
-
<input type="range" id="slider-r2" min="16" max="32" value="24" step="1" oninput="updateReefs()">
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
<div class="control-group" style="border-top: 1px solid #eee; padding-top: 15px; margin-bottom: 0;">
|
|
169
|
-
<label>Reef 3 (Storm) <span class="val-display" id="r3-val" style="color: #9c27b0;">30 kts</span></label>
|
|
170
|
-
<span style="font-size: 0.7rem; color: #888; display: block; margin-top: -3px; line-height: 1.2;">
|
|
171
|
-
Calcolato dinamicamente: R2 + (R2 - R1)
|
|
172
|
-
</span>
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
<div class="data-box-square">
|
|
177
|
-
<div class="label-row">
|
|
178
|
-
<span class="label">TWD (HISTORICAL)</span>
|
|
179
|
-
<span class="unit">ECMWF + N2K</span>
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
<!-- BUSSOLA RADAR SVG (Esatto ViewBox dell'originale, nativamente scalabile) -->
|
|
183
|
-
<svg id="wind-radar" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
|
|
184
|
-
<defs id="radar-gradients">
|
|
185
|
-
<clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
186
|
-
<linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
187
|
-
<stop offset="0%" style="stop-color:#333333;stop-opacity:1" />
|
|
188
|
-
<stop offset="100%" style="stop-color:#999999;stop-opacity:1" />
|
|
189
|
-
</linearGradient>
|
|
190
|
-
|
|
191
|
-
<filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
192
|
-
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
193
|
-
</filter>
|
|
194
|
-
</defs>
|
|
195
|
-
|
|
196
|
-
<!-- I tre sfondi speculari originali diurni con coordinate RGB esatte -->
|
|
197
|
-
<circle cx="200" cy="200" r="160" fill="rgb(252, 252, 252)" />
|
|
198
|
-
<circle cx="200" cy="200" r="125" fill="rgb(240, 240, 240)" />
|
|
199
|
-
|
|
200
|
-
<!-- Ticks originali generati dinamicamente -->
|
|
201
|
-
<g id="ticks"></g>
|
|
202
|
-
|
|
203
|
-
<!-- Etichette cardinali N/S/E/W -->
|
|
204
|
-
<g id="tick-labels" fill="#000000" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
|
|
205
|
-
<text font-size="16" transform="translate(200, 74)">N</text>
|
|
206
|
-
<text font-size="16" transform="translate(326, 200) rotate(90)">E</text>
|
|
207
|
-
<text font-size="16" transform="translate(74, 200) rotate(-90)">W</text>
|
|
208
|
-
<text font-size="16" transform="translate(200, 326) rotate(180)">S</text>
|
|
209
|
-
|
|
210
|
-
<text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
|
|
211
|
-
<text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
|
|
212
|
-
<text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
|
|
213
|
-
<text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
|
|
214
|
-
<text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
|
|
215
|
-
<text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
|
|
216
|
-
<text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
|
|
217
|
-
<text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
|
|
218
|
-
</g>
|
|
219
|
-
|
|
220
|
-
<!-- 1. ORBITA DEL PRESENTE "ORA" SUL LIVELLO DI SFONDO (Sotto gli archi) -->
|
|
221
|
-
<!-- Posizionata esattamente sulla mezzeria del raggio dell'Anello 1 (67.2px) -->
|
|
222
|
-
<circle id="ora-orbit" cx="200" cy="200" r="67.2" fill="none" stroke="#cfd8dc" stroke-width="1.2" stroke-dasharray="4, 4" />
|
|
223
|
-
|
|
224
|
-
<!-- Gli anelli radar degli archi verranno stampati qui -->
|
|
225
|
-
<g id="radar-rings"></g>
|
|
226
|
-
|
|
227
|
-
<!-- Cerchio centrale interattivo originale e Barca NERA SOLIDA -->
|
|
228
|
-
<circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="rgb(238, 238, 238)" stroke="#e0e0e0" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
|
|
229
|
-
<path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
|
|
230
|
-
fill="#000000" transform="translate(0, 5)" clip-path="url(#boat-clip)" style="pointer-events: none;" />
|
|
231
|
-
</svg>
|
|
232
|
-
</div>
|
|
233
|
-
</div>
|
|
234
|
-
|
|
235
|
-
<script>
|
|
236
|
-
// Funzione per testare dinamicamente il ridimensionamento del div
|
|
237
|
-
function resizeBox() {
|
|
238
|
-
const size = document.getElementById('slider-scale').value;
|
|
239
|
-
// Modifichiamo la variabile CSS, l'SVG seguirà nativamente
|
|
240
|
-
document.documentElement.style.setProperty('--box-size', size + 'px');
|
|
241
|
-
document.getElementById('scale-val').innerText = size + " px";
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const CALM_THRESHOLD_KTS = 1.5;
|
|
245
|
-
|
|
246
|
-
// Default Reef settings
|
|
247
|
-
let REEF1 = 18;
|
|
248
|
-
let REEF2 = 24;
|
|
249
|
-
let REEF3 = 30;
|
|
250
|
-
|
|
251
|
-
// Dati di simulazione calibrati su 8 anelli reali con la correzione ripristinata dei diametri
|
|
252
|
-
const mockData = [
|
|
253
|
-
{ isFuture: true, twdMin: 235, twdMax: 245, twsPeak: 5.0 }, // Anello 0 (r:59.0px): Previsione futura (Bianco Solido - 100% OPACITÀ CON TRATTEGGIO)
|
|
254
|
-
{ isActive: true, twdMin: 220, twdMax: 260, twsPeak: 11.5 }, // Anello 1 (r:67.2px): Presente Mobile (Sfumatura Bianco -> Verde -> Bianco)
|
|
255
|
-
{ twdMin: 225, twdMax: 255, twsPeak: 13.5 }, // Anello 2 (r:75.4px): Ora -0.5 (Verde Solido)
|
|
256
|
-
{ twdMin: 230, twdMax: 250, twsPeak: 16.0 }, // Anello 3 (r:83.6px): Ora -1.0 (Sfumatura Verde -> Arancio -> Verde)
|
|
257
|
-
{ twdMin: 235, twdMax: 245, twsPeak: 20.0 }, // Anello 4 (r:91.8px): Ora -1.5 (Arancio Solido)
|
|
258
|
-
{ twdMin: 220, twdMax: 260, twsPeak: 22.5 }, // Anello 5 (r:100.0px): Ora -2.0 (Sfumatura Arancio -> Rosso -> Arancio)
|
|
259
|
-
{ twdMin: 230, twdMax: 250, twsPeak: 25.5 }, // Anello 6 (r:108.2px): Ora -2.5 (Rosso Solido)
|
|
260
|
-
{ twdMin: 215, twdMax: 265, twsPeak: 28.5 } // Anello 7 (r:116.4px): Ora -3.0 (Sfumatura Rosso -> Viola -> Rosso)
|
|
261
|
-
];
|
|
262
|
-
|
|
263
|
-
// --- MAPPA RADIALE SPECULARE COMPRESSA AL MILLIMETRO SU 8 ANELLI ---
|
|
264
|
-
// r_i = 59.0px + 8.2px * i (Incastonati in sicurezza, massimo raggio a 116.4px)
|
|
265
|
-
const ringRadii = [59.0, 67.2, 75.4, 83.6, 91.8, 100.0, 108.2, 116.4];
|
|
266
|
-
const chronologicalMapping = [0, 1, 2, 3, 4, 5, 6, 7];
|
|
267
|
-
|
|
268
|
-
const ARC_STROKE_WIDTH = 5.0; // Spessore arco ridotto a 5.0px per una visualizzazione affilatissima
|
|
269
|
-
const BORDER_STROKE_WIDTH = ARC_STROKE_WIDTH + 2; // Spessore 7.0px (bordo da 1px per lato)
|
|
270
|
-
|
|
271
|
-
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
|
|
272
|
-
const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
|
|
273
|
-
return {
|
|
274
|
-
x: centerX + (radius * Math.cos(angleInRadians)),
|
|
275
|
-
y: centerY + (radius * Math.sin(angleInRadians))
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function describeArc(centerX, centerY, radius, startAngle, endAngle) {
|
|
280
|
-
const start = polarToCartesian(centerX, centerY, radius, endAngle);
|
|
281
|
-
const end = polarToCartesian(centerX, centerY, radius, startAngle);
|
|
282
|
-
|
|
283
|
-
let arcSweep = endAngle - startAngle;
|
|
284
|
-
if (arcSweep < 0) arcSweep += 360;
|
|
285
|
-
|
|
286
|
-
const largeArcFlag = arcSweep <= 180 ? "0" : "1";
|
|
287
|
-
|
|
288
|
-
return [
|
|
289
|
-
"M", start.x, start.y,
|
|
290
|
-
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
|
|
291
|
-
].join(" ");
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function drawCompassTicks() {
|
|
295
|
-
const ticksGroup = document.getElementById('ticks');
|
|
296
|
-
ticksGroup.innerHTML = '';
|
|
297
|
-
for (let i = 0; i < 360; i += 10) {
|
|
298
|
-
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
299
|
-
const isMajor = (i % 30 === 0);
|
|
300
|
-
|
|
301
|
-
line.setAttribute("x1", "200");
|
|
302
|
-
line.setAttribute("y1", "40");
|
|
303
|
-
line.setAttribute("x2", "200");
|
|
304
|
-
line.setAttribute("y2", isMajor ? "60" : "50");
|
|
305
|
-
|
|
306
|
-
line.setAttribute("stroke", isMajor ? "#000000" : "#bbbbbb");
|
|
307
|
-
line.setAttribute("stroke-width", isMajor ? "2" : "1");
|
|
308
|
-
line.setAttribute("transform", `rotate(${i}, 200, 200)`);
|
|
309
|
-
ticksGroup.appendChild(line);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function updateReefs() {
|
|
314
|
-
REEF1 = parseInt(document.getElementById('slider-r1').value);
|
|
315
|
-
REEF2 = parseInt(document.getElementById('slider-r2').value);
|
|
316
|
-
REEF3 = REEF2 + (REEF2 - REEF1);
|
|
317
|
-
|
|
318
|
-
document.getElementById('r1-val').innerText = REEF1 + " kts";
|
|
319
|
-
document.getElementById('r2-val').innerText = REEF2 + " kts";
|
|
320
|
-
document.getElementById('r3-val').innerText = REEF3 + " kts";
|
|
321
|
-
|
|
322
|
-
renderRadar();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// --- LA NUOVA SCALA A COLORE DI PROSSIMITÀ CON CONTRASTO RICALIBRATO ---
|
|
326
|
-
function getChordAlignedGradient(id, radius, tws, startAngle, endAngle) {
|
|
327
|
-
const startPt = polarToCartesian(200, 200, radius, endAngle);
|
|
328
|
-
const endPt = polarToCartesian(200, 200, radius, startAngle);
|
|
329
|
-
|
|
330
|
-
let stops = '';
|
|
331
|
-
const R1 = REEF1;
|
|
332
|
-
const R2 = REEF2;
|
|
333
|
-
const R3 = REEF3;
|
|
334
|
-
const colorEdge = 'rgb(240, 240, 240)';
|
|
335
|
-
|
|
336
|
-
// 1. BIANCO SOLIDO (0 - 40% di R1)
|
|
337
|
-
if (tws < R1 * 0.4) {
|
|
338
|
-
return { type: 'solid', color: '#ffffff' };
|
|
339
|
-
}
|
|
340
|
-
// 2. TRANSIZIONE BIANCO -> VERDE (40% - 60% di R1)
|
|
341
|
-
else if (tws >= R1 * 0.4 && tws < R1 * 0.6) {
|
|
342
|
-
stops = `
|
|
343
|
-
<stop offset="0%" stop-color="#ffffff" />
|
|
344
|
-
<stop offset="50%" stop-color="#00C851" />
|
|
345
|
-
<stop offset="100%" stop-color="#ffffff" />
|
|
346
|
-
`;
|
|
347
|
-
}
|
|
348
|
-
// 3. VERDE SOLIDO (60% - 75% di R1) -> Brezza stabile
|
|
349
|
-
else if (tws >= R1 * 0.6 && tws < R1 * 0.75) {
|
|
350
|
-
return { type: 'solid', color: '#00C851' };
|
|
351
|
-
}
|
|
352
|
-
// 4. TRANSIZIONE VERDE -> ARANCIONE (75% R1 - R1) -> Raffiche in aumento
|
|
353
|
-
else if (tws >= R1 * 0.75 && tws < R1) {
|
|
354
|
-
stops = `
|
|
355
|
-
<stop offset="0%" stop-color="#00C851" />
|
|
356
|
-
<stop offset="50%" stop-color="#ff9800" />
|
|
357
|
-
<stop offset="100%" stop-color="#00C851" />
|
|
358
|
-
`;
|
|
359
|
-
}
|
|
360
|
-
// 5. ARANCIONE SOLIDO (R1 -> Metà di R2) -> Vento forte stabile
|
|
361
|
-
else if (tws >= R1 && tws < R1 + (R2 - R1) * 0.5) {
|
|
362
|
-
return { type: 'solid', color: '#ff9800' };
|
|
363
|
-
}
|
|
364
|
-
// 6. TRANSIZIONE ARANCIONE -> ROSSO (Metà R2 -> R2) -> RICALIBRATO AD ALTO CONTRASTO (Dorato -> Cremisi)
|
|
365
|
-
else if (tws >= R1 + (R2 - R1) * 0.5 && tws < R2) {
|
|
366
|
-
stops = `
|
|
367
|
-
<stop offset="0%" stop-color="#ffaa00" /> <!-- Arancio Dorato Brillante sui bordi -->
|
|
368
|
-
<stop offset="50%" stop-color="#d50000" /> <!-- Rosso Cremisi Intenso e saturo al centro -->
|
|
369
|
-
<stop offset="100%" stop-color="#ffaa00" />
|
|
370
|
-
`;
|
|
371
|
-
}
|
|
372
|
-
// 7. ROSSO SOLIDO (R2 -> Metà di R3) -> Burrasca stabile
|
|
373
|
-
else if (tws >= R2 && tws < R2 + (R3 - R2) * 0.5) {
|
|
374
|
-
return { type: 'solid', color: '#ff3b30' };
|
|
375
|
-
}
|
|
376
|
-
// 8. TRANSIZIONE ROSSO -> VIOLA (Metà R3 -> R3) -> Transizione Spettrale (Rosso vivo -> Viola)
|
|
377
|
-
else if (tws >= R2 + (R3 - R2) * 0.5 && tws < R3) {
|
|
378
|
-
stops = `
|
|
379
|
-
<stop offset="0%" stop-color="#ff3b30" />
|
|
380
|
-
<stop offset="50%" stop-color="#9c27b0" />
|
|
381
|
-
<stop offset="100%" stop-color="#ff3b30" />
|
|
382
|
-
`;
|
|
383
|
-
}
|
|
384
|
-
// 9. VIOLA SOLIDO (Oltre R3) -> Tempesta stabile
|
|
385
|
-
else {
|
|
386
|
-
return { type: 'solid', color: '#9c27b0' };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const xml = `
|
|
390
|
-
<linearGradient id="${id}" x1="${startPt.x.toFixed(1)}" y1="${startPt.y.toFixed(1)}" x2="${endPt.x.toFixed(1)}" y2="${endPt.y.toFixed(1)}" gradientUnits="userSpaceOnUse">
|
|
391
|
-
${stops}
|
|
392
|
-
</linearGradient>
|
|
393
|
-
`;
|
|
394
|
-
|
|
395
|
-
return { type: 'gradient', xml: xml, url: `url(#${id})` };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function renderRadar() {
|
|
399
|
-
const ringsContainer = document.getElementById('radar-rings');
|
|
400
|
-
const defsContainer = document.getElementById('radar-gradients');
|
|
401
|
-
|
|
402
|
-
defsContainer.innerHTML = `
|
|
403
|
-
<clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
404
|
-
<linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
405
|
-
<stop offset="0%" style="stop-color:#333333;stop-opacity:1" />
|
|
406
|
-
<stop offset="100%" style="stop-color:#999999;stop-opacity:1" />
|
|
407
|
-
</linearGradient>
|
|
408
|
-
<filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
409
|
-
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
410
|
-
</filter>
|
|
411
|
-
`;
|
|
412
|
-
ringsContainer.innerHTML = '';
|
|
413
|
-
|
|
414
|
-
// Imposta dinamicamente i raggi in base a X per renderli scalabili
|
|
415
|
-
const oraOrbit = document.getElementById('ora-orbit');
|
|
416
|
-
if (oraOrbit) oraOrbit.setAttribute("r", 67.2);
|
|
417
|
-
|
|
418
|
-
mockData.forEach((data, index) => {
|
|
419
|
-
const radius = ringRadii[index];
|
|
420
|
-
const gradId = `chord-gradient-${index}`;
|
|
421
|
-
|
|
422
|
-
// IMPORTANTE: Rimosso il dimezzamento dell'opacità per la previsione.
|
|
423
|
-
// Ora l'opacità è sempre 1 (100% solida) per tutti gli anelli,
|
|
424
|
-
// garantendo che il Bianco sia puro e brillante, senza sbiadire in grigio!
|
|
425
|
-
const opacityValue = 1;
|
|
426
|
-
|
|
427
|
-
// --- 1. CASO CALMA PIATTA (Cerchio grigio-azzurro discreto sottile) ---
|
|
428
|
-
if (data.twsPeak < CALM_THRESHOLD_KTS) {
|
|
429
|
-
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
430
|
-
circle.setAttribute("cx", "200");
|
|
431
|
-
circle.setAttribute("cy", "200");
|
|
432
|
-
circle.setAttribute("r", radius);
|
|
433
|
-
circle.setAttribute("fill", "none");
|
|
434
|
-
circle.setAttribute("stroke", "#b0bec5");
|
|
435
|
-
circle.setAttribute("stroke-width", "1.2");
|
|
436
|
-
circle.setAttribute("stroke-dasharray", "4, 4");
|
|
437
|
-
ringsContainer.appendChild(circle);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// --- 2. CASO ARCO DIREZIONALE ---
|
|
442
|
-
const grad = getChordAlignedGradient(gradId, radius, data.twsPeak, data.twdMin, data.twdMax);
|
|
443
|
-
let strokeColor = '';
|
|
444
|
-
|
|
445
|
-
if (grad.type === 'gradient') {
|
|
446
|
-
defsContainer.innerHTML += grad.xml;
|
|
447
|
-
strokeColor = grad.url;
|
|
448
|
-
} else {
|
|
449
|
-
strokeColor = grad.color;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
|
|
453
|
-
|
|
454
|
-
const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
455
|
-
borderPath.setAttribute("d", pathData);
|
|
456
|
-
borderPath.setAttribute("fill", "none");
|
|
457
|
-
borderPath.setAttribute("stroke", "#000000");
|
|
458
|
-
borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
|
|
459
|
-
borderPath.setAttribute("stroke-linecap", "round");
|
|
460
|
-
borderPath.setAttribute("opacity", opacityValue);
|
|
461
|
-
|
|
462
|
-
// Tratteggio per l'Anello Previsione (Anello 0)
|
|
463
|
-
if (data.isFuture) {
|
|
464
|
-
borderPath.setAttribute("stroke-dasharray", "10, 6");
|
|
465
|
-
}
|
|
466
|
-
ringsContainer.appendChild(borderPath);
|
|
467
|
-
|
|
468
|
-
const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
469
|
-
mainPath.setAttribute("d", pathData);
|
|
470
|
-
mainPath.setAttribute("fill", "none");
|
|
471
|
-
mainPath.setAttribute("stroke", strokeColor);
|
|
472
|
-
mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
|
|
473
|
-
mainPath.setAttribute("stroke-linecap", "round");
|
|
474
|
-
mainPath.setAttribute("opacity", opacityValue);
|
|
475
|
-
|
|
476
|
-
// Tratteggio per l'Anello Previsione (Anello 0)
|
|
477
|
-
if (data.isFuture) {
|
|
478
|
-
mainPath.setAttribute("stroke-dasharray", "10, 6");
|
|
479
|
-
}
|
|
480
|
-
ringsContainer.appendChild(mainPath);
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
window.onload = function() {
|
|
485
|
-
drawCompassTicks();
|
|
486
|
-
renderRadar();
|
|
487
|
-
};
|
|
488
|
-
</script>
|
|
489
|
-
</body>
|
|
490
|
-
</html>
|
|
File without changes
|