@sailingrotevista/rotevista-dash 7.0.45 → 8.0.2
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.
Potentially problematic release.
This version of @sailingrotevista/rotevista-dash might be problematic. Click here for more details.
- package/README.md +28 -70
- package/index.js +14 -0
- package/package.json +26 -1
- package/public/assets/dash-DmyxO1aO.png +0 -0
- package/public/assets/index-BFSy7dOW.css +1 -0
- package/public/assets/index-BHhAwNqq.js +9 -0
- package/public/assets/manifest-Ea_0WKQ8.json +17 -0
- package/public/icons/dash.png +0 -0
- package/public/index.html +23 -0
- package/public/manifest.json +17 -0
- package/app.js +0 -1505
- package/charts.js +0 -305
- package/debug.html +0 -1037
- package/gauge.js +0 -90
- package/index.html +0 -264
- package/style.css +0 -640
- package/utils.js +0 -148
- package/weather-radar.js +0 -305
- package/weather_debug.html +0 -181
package/utils.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ==========================================================================
|
|
3
|
-
* Sailing Dashboard Pro - Math, Conversions & Audio Utilities
|
|
4
|
-
* ==========================================================================
|
|
5
|
-
* Raccoglie le funzioni pure di calcolo vettoriale e sintesi sonora.
|
|
6
|
-
*file utils.js
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// --- 1. CONVERSIONI STANDARD ---
|
|
10
|
-
const radToDeg = (rad) => rad * (180 / Math.PI);
|
|
11
|
-
const degToRad = (deg) => deg * (Math.PI / 180);
|
|
12
|
-
const msToKts = (ms) => ms * 1.94384;
|
|
13
|
-
const ktsToMs = (kts) => kts / 1.94384;
|
|
14
|
-
|
|
15
|
-
// Calcola la rotta di rotazione più breve tra due angoli (evita scatti a 360°)
|
|
16
|
-
function getShortestRotation(curr, target) {
|
|
17
|
-
let diff = (target - curr) % 360;
|
|
18
|
-
if (diff > 180) diff -= 360;
|
|
19
|
-
else if (diff < -180) diff += 360;
|
|
20
|
-
return curr + diff;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Scrive testo in sicurezza evitando reflow inutili nel DOM/SVG
|
|
24
|
-
function safeSetText(el, text) {
|
|
25
|
-
if (!el) return;
|
|
26
|
-
const isSVG = el instanceof SVGElement;
|
|
27
|
-
if (isSVG) {
|
|
28
|
-
if (el.textContent !== text) el.textContent = text;
|
|
29
|
-
} else {
|
|
30
|
-
if (el.innerHTML !== text) el.innerHTML = text;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// --- 2. MOTORE MATEMATICO: MEDIA CIRCOLARE VETTORIALE ---
|
|
35
|
-
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now, stabilityThreshold = 0.95, stabilityBreakout = 15) {
|
|
36
|
-
const len = bufferArray.length;
|
|
37
|
-
if (len === 0) return null;
|
|
38
|
-
|
|
39
|
-
const currentTime = now || Date.now();
|
|
40
|
-
const newestSampleTime = bufferArray[len - 1].time;
|
|
41
|
-
|
|
42
|
-
// Se l'ultimo pacchetto registrato nel buffer ha più di 15 secondi di silenzio, il sensore è offline
|
|
43
|
-
if ((currentTime - newestSampleTime) > 15000) return null;
|
|
44
|
-
|
|
45
|
-
let sSin = 0, sCos = 0, count = 0;
|
|
46
|
-
let newestTime = 0, oldestTime = 0;
|
|
47
|
-
|
|
48
|
-
let pilotSin = 0, pilotCos = 0;
|
|
49
|
-
const pilotSamples = Math.min(len, 15);
|
|
50
|
-
for (let i = len - 1; i >= len - pilotSamples; i--) {
|
|
51
|
-
pilotSin += bufferArray[i].sin;
|
|
52
|
-
pilotCos += bufferArray[i].cos;
|
|
53
|
-
}
|
|
54
|
-
const pilotRad = Math.atan2(pilotSin, pilotCos);
|
|
55
|
-
const limitRad = stabilityBreakout * (Math.PI / 180);
|
|
56
|
-
|
|
57
|
-
for (let i = len - 1; i >= 0; i--) {
|
|
58
|
-
const item = bufferArray[i];
|
|
59
|
-
// Decadimento temporale reale ancorato sull'orologio corrente
|
|
60
|
-
if ((currentTime - item.time) > windowMs) break;
|
|
61
|
-
|
|
62
|
-
let diffRad = Math.atan2(Math.sin(item.val - pilotRad), Math.cos(item.val - pilotRad));
|
|
63
|
-
let finalSin, finalCos;
|
|
64
|
-
|
|
65
|
-
if (Math.abs(diffRad) > limitRad) {
|
|
66
|
-
const clampedDiff = Math.sign(diffRad) * limitRad;
|
|
67
|
-
const clampedRad = pilotRad + clampedDiff;
|
|
68
|
-
finalSin = Math.sin(clampedRad);
|
|
69
|
-
finalCos = Math.cos(clampedRad);
|
|
70
|
-
} else {
|
|
71
|
-
finalSin = item.sin;
|
|
72
|
-
finalCos = item.cos;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
sSin += finalSin;
|
|
76
|
-
sCos += finalCos;
|
|
77
|
-
|
|
78
|
-
if (count === 0) newestTime = item.time;
|
|
79
|
-
oldestTime = item.time;
|
|
80
|
-
count++;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (count === 0) return null;
|
|
84
|
-
|
|
85
|
-
const R = Math.hypot(sSin, sCos) / count;
|
|
86
|
-
const avgRad = Math.atan2(sSin, sCos);
|
|
87
|
-
const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
|
|
88
|
-
const safeR = Math.max(R, 1e-9);
|
|
89
|
-
|
|
90
|
-
// Stabilità valida se ci sono almeno 2 campioni nel buffer e la varianza radiale supera la soglia
|
|
91
|
-
const isStable = (count >= 2) && (R > stabilityThreshold);
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
val: finalVal,
|
|
95
|
-
stable: isStable,
|
|
96
|
-
dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
|
|
97
|
-
samples: count
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// --- 3. SINTESI AUDIO WEB AUDIO API (ALLARMI ACUSTICI) ---
|
|
102
|
-
let audioCtx = null;
|
|
103
|
-
let lastAlarmTime = 0;
|
|
104
|
-
|
|
105
|
-
function playBingBing() {
|
|
106
|
-
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
107
|
-
const n = Date.now();
|
|
108
|
-
if (n - lastAlarmTime < 3000) return;
|
|
109
|
-
lastAlarmTime = n;
|
|
110
|
-
|
|
111
|
-
function b(f, s) {
|
|
112
|
-
const o = audioCtx.createOscillator();
|
|
113
|
-
const g = audioCtx.createGain();
|
|
114
|
-
o.connect(g);
|
|
115
|
-
g.connect(audioCtx.destination);
|
|
116
|
-
o.frequency.value = f;
|
|
117
|
-
g.gain.setValueAtTime(0.1, s);
|
|
118
|
-
g.gain.exponentialRampToValueAtTime(0.01, s + 0.4);
|
|
119
|
-
o.start(s);
|
|
120
|
-
o.stop(s + 0.5);
|
|
121
|
-
}
|
|
122
|
-
b(880, audioCtx.currentTime);
|
|
123
|
-
b(880, audioCtx.currentTime + 0.6);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function playGybeAlarm() {
|
|
127
|
-
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
128
|
-
const n = Date.now();
|
|
129
|
-
if (n - lastAlarmTime < 2000) return;
|
|
130
|
-
lastAlarmTime = n;
|
|
131
|
-
|
|
132
|
-
function note(f, s, d) {
|
|
133
|
-
const o = audioCtx.createOscillator();
|
|
134
|
-
const g = audioCtx.createGain();
|
|
135
|
-
o.connect(g);
|
|
136
|
-
g.connect(audioCtx.destination);
|
|
137
|
-
o.type = 'square';
|
|
138
|
-
o.frequency.value = f;
|
|
139
|
-
g.gain.setValueAtTime(0.05, s);
|
|
140
|
-
g.gain.exponentialRampToValueAtTime(0.001, s + d);
|
|
141
|
-
o.start(s);
|
|
142
|
-
o.stop(s + d);
|
|
143
|
-
}
|
|
144
|
-
for (let i = 0; i < 4; i++) {
|
|
145
|
-
note(1800, audioCtx.currentTime + (i * 0.15), 0.1);
|
|
146
|
-
note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1);
|
|
147
|
-
}
|
|
148
|
-
}
|
package/weather-radar.js
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ==========================================================================
|
|
3
|
-
* Signal K Wind Radar - Vector Core and Compass Rendering Engine (Modular v6.0)
|
|
4
|
-
* ==========================================================================
|
|
5
|
-
* Autore: Sailing Rotevista
|
|
6
|
-
* Libreria grafica pura per il disegno della bussola radar storica (TWD).
|
|
7
|
-
* Condivide le risorse, lo stato e i flussi di dati generati da app.js.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
// --- 1. PARAMETRI DI CALCOLO INTERNI ---
|
|
11
|
-
const CALM_THRESHOLD_KTS = 1.5;
|
|
12
|
-
const PRESSURE_FILTER_RATIO = 0.40;
|
|
13
|
-
const ringRadii = [59.0, 67.2, 75.4, 83.6, 91.8, 100.0, 108.2, 116.4];
|
|
14
|
-
const ARC_STROKE_WIDTH = 5.0;
|
|
15
|
-
const BORDER_STROKE_WIDTH = ARC_STROKE_WIDTH + 2; // 7px
|
|
16
|
-
|
|
17
|
-
// --- 2. FUNZIONI GEOMETRICHE DI SUPPORTO ---
|
|
18
|
-
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
|
|
19
|
-
const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
|
|
20
|
-
return {
|
|
21
|
-
x: centerX + (radius * Math.cos(angleInRadians)),
|
|
22
|
-
y: centerY + (radius * Math.sin(angleInRadians))
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function describeArc(centerX, centerY, radius, startAngle, endAngle) {
|
|
27
|
-
const start = polarToCartesian(centerX, centerY, radius, endAngle);
|
|
28
|
-
const end = polarToCartesian(centerX, centerY, radius, startAngle);
|
|
29
|
-
|
|
30
|
-
let arcSweep = endAngle - startAngle;
|
|
31
|
-
if (arcSweep < 0) arcSweep += 360;
|
|
32
|
-
|
|
33
|
-
const largeArcFlag = arcSweep <= 180 ? "0" : "1";
|
|
34
|
-
|
|
35
|
-
return [
|
|
36
|
-
"M", start.x, start.y,
|
|
37
|
-
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
|
|
38
|
-
].join(" ");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Genera dinamicamente le tacche dei gradi bussola per l'SVG del radar
|
|
42
|
-
function initRadarTicks() {
|
|
43
|
-
const c = document.getElementById('radar-ticks');
|
|
44
|
-
if (c) {
|
|
45
|
-
c.innerHTML = "";
|
|
46
|
-
for (let i = 0; i < 360; i += 10) {
|
|
47
|
-
const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
48
|
-
const m = i % 30 === 0;
|
|
49
|
-
l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50));
|
|
50
|
-
l.setAttribute("stroke", m ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1");
|
|
51
|
-
l.setAttribute("transform", `rotate(${i}, 200, 200)`);
|
|
52
|
-
c.appendChild(l);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// --- 3. MOTORE DI CALCOLO DELL'ANELLO 1 (Presente Mobile) ---
|
|
58
|
-
function calculateActive30mRing() {
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
const start30m = now - 1800000;
|
|
61
|
-
|
|
62
|
-
// Lettura unificata dai buffer storici dello store
|
|
63
|
-
const twdRecent = (store.histories.twd || []).filter(p => p.time >= start30m);
|
|
64
|
-
const twsRecent = (store.histories.tws || []).filter(p => p.time >= start30m);
|
|
65
|
-
|
|
66
|
-
if (twdRecent.length === 0) return null;
|
|
67
|
-
|
|
68
|
-
const twsVals = twsRecent.map(p => p.val).filter(v => isFinite(v));
|
|
69
|
-
const maxTws = twsVals.length > 0 ? Math.max(...twsVals) : 0;
|
|
70
|
-
const minTws = twsVals.length > 0 ? Math.min(...twsVals) : 0;
|
|
71
|
-
|
|
72
|
-
if (maxTws < CALM_THRESHOLD_KTS) {
|
|
73
|
-
return { twsPeak: maxTws, twsMin: minTws, twdMin: 0, twdMax: 360, isCalm: true };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let allAngles = [];
|
|
77
|
-
twdRecent.forEach(p => {
|
|
78
|
-
allAngles.push(p.val);
|
|
79
|
-
allAngles.push(p.min);
|
|
80
|
-
allAngles.push(p.max);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
let sumSin = 0; let sumCos = 0;
|
|
84
|
-
allAngles.forEach(a => { sumSin += Math.sin(a); sumCos += Math.cos(a); });
|
|
85
|
-
const avgAngle = Math.atan2(sumSin, sumCos);
|
|
86
|
-
const finalAvg = (avgAngle + Math.PI * 2) % (Math.PI * 2);
|
|
87
|
-
|
|
88
|
-
let diffs = allAngles.map(a => {
|
|
89
|
-
let diff = a - finalAvg;
|
|
90
|
-
return Math.atan2(Math.sin(diff), Math.cos(diff));
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
diffs.sort((a, b) => a - b);
|
|
94
|
-
const trimCount = Math.floor(diffs.length * 0.05);
|
|
95
|
-
const activeDiffs = diffs.slice(trimCount, diffs.length - trimCount);
|
|
96
|
-
const finalDiffs = activeDiffs.length > 0 ? activeDiffs : diffs;
|
|
97
|
-
|
|
98
|
-
const minDiff = Math.min(...finalDiffs);
|
|
99
|
-
const maxDiff = Math.max(...finalDiffs);
|
|
100
|
-
|
|
101
|
-
const finalMinDeg = Math.round(radToDeg((finalAvg + minDiff + Math.PI * 2) % (Math.PI * 2)));
|
|
102
|
-
const finalMaxDeg = Math.round(radToDeg((finalAvg + maxDiff + Math.PI * 2) % (Math.PI * 2)));
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
twdMin: finalMinDeg,
|
|
106
|
-
twdMax: finalMaxDeg,
|
|
107
|
-
twsPeak: maxTws,
|
|
108
|
-
twsMin: minTws,
|
|
109
|
-
isCalm: false
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// --- 4. MOTORE GRAFICO DI DISEGNO DEL RADAR ---
|
|
114
|
-
function renderRadar() {
|
|
115
|
-
const ringsContainer = document.getElementById('radar-rings');
|
|
116
|
-
const defsContainer = document.getElementById('radar-gradients');
|
|
117
|
-
|
|
118
|
-
if (!ringsContainer || !defsContainer) return; // Protezione se l'SVG del radar non è presente nel DOM
|
|
119
|
-
|
|
120
|
-
defsContainer.innerHTML = `
|
|
121
|
-
<clipPath id="radar-boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
122
|
-
<filter id="radar-center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
123
|
-
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
124
|
-
</filter>
|
|
125
|
-
`;
|
|
126
|
-
ringsContainer.innerHTML = '';
|
|
127
|
-
|
|
128
|
-
const oraOrbit = document.getElementById('ora-orbit');
|
|
129
|
-
if (oraOrbit) oraOrbit.setAttribute("r", ringRadii[1]);
|
|
130
|
-
|
|
131
|
-
const now = Date.now();
|
|
132
|
-
const current30mSlot = Math.floor(now / 1800000) * 1800000;
|
|
133
|
-
|
|
134
|
-
const radarDataList = [];
|
|
135
|
-
|
|
136
|
-
// 1. ANELLO 0: Previsione Futura Open-Meteo
|
|
137
|
-
if (store.futureForecast) {
|
|
138
|
-
const twdDeg = Math.round(radToDeg(store.futureForecast.twd));
|
|
139
|
-
radarDataList.push({
|
|
140
|
-
twdMin: (twdDeg - 20 + 360) % 360,
|
|
141
|
-
twdMax: (twdDeg + 20 + 360) % 360,
|
|
142
|
-
twsPeak: store.futureForecast.gust, // Chirurgico: Usiamo il Gust come picco per visualizzare l'arco delle raffiche future
|
|
143
|
-
isFuture: true
|
|
144
|
-
});
|
|
145
|
-
} else {
|
|
146
|
-
radarDataList.push(null);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// 2. ANELLO 1: Presente Mobile (Real-Time Client-Side)
|
|
151
|
-
const activeRing = calculateActive30mRing();
|
|
152
|
-
radarDataList.push(activeRing);
|
|
153
|
-
|
|
154
|
-
// 3. ANELLI 2-7: Storico consolidato dal server
|
|
155
|
-
const slots = store.windRadarSlots || [];
|
|
156
|
-
for (let i = 1; i <= 6; i++) {
|
|
157
|
-
const targetTimestamp = current30mSlot - (i * 1800000);
|
|
158
|
-
const matchedSlot = slots.find(s => s.timestamp === targetTimestamp);
|
|
159
|
-
|
|
160
|
-
if (matchedSlot) {
|
|
161
|
-
radarDataList.push({
|
|
162
|
-
twdMin: Math.round(radToDeg(matchedSlot.twdMin)),
|
|
163
|
-
twdMax: Math.round(radToDeg(matchedSlot.twdMax)),
|
|
164
|
-
twsPeak: matchedSlot.twsPeak,
|
|
165
|
-
twsMin: matchedSlot.twsMin !== undefined ? matchedSlot.twsMin : matchedSlot.twsPeak,
|
|
166
|
-
isCalm: matchedSlot.twsPeak < CALM_THRESHOLD_KTS
|
|
167
|
-
});
|
|
168
|
-
} else {
|
|
169
|
-
radarDataList.push(null);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Disegno degli archi
|
|
174
|
-
radarDataList.forEach((data, index) => {
|
|
175
|
-
if (!data) return;
|
|
176
|
-
|
|
177
|
-
const radius = ringRadii[index];
|
|
178
|
-
const gradId = `chord-gradient-${index}`;
|
|
179
|
-
const opacityValue = 1;
|
|
180
|
-
|
|
181
|
-
if (data.isCalm || data.twsPeak < CALM_THRESHOLD_KTS) {
|
|
182
|
-
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
183
|
-
circle.setAttribute("cx", "200");
|
|
184
|
-
circle.setAttribute("cy", "200");
|
|
185
|
-
circle.setAttribute("r", radius);
|
|
186
|
-
circle.setAttribute("fill", "none");
|
|
187
|
-
circle.setAttribute("stroke", document.body.classList.contains('night-mode') ? "#440000" : "#b0bec5");
|
|
188
|
-
circle.setAttribute("stroke-width", "1.2");
|
|
189
|
-
circle.setAttribute("stroke-dasharray", "4, 4");
|
|
190
|
-
ringsContainer.appendChild(circle);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
let strokeColor = '';
|
|
195
|
-
|
|
196
|
-
const getColorForSpeed = (tws) => {
|
|
197
|
-
const R1 = CONFIG.graphs.reef1 || 15;
|
|
198
|
-
const R2 = CONFIG.graphs.reef2 || 20;
|
|
199
|
-
const R3 = R2 + (R2 - R1);
|
|
200
|
-
if (tws < R1 * 0.4) return '#ffffff';
|
|
201
|
-
if (tws < R1 * 0.75) return '#00C851';
|
|
202
|
-
if (tws < R1) return '#ff9800';
|
|
203
|
-
if (tws < R2) return '#ffaa00';
|
|
204
|
-
if (tws < R2 + (R3 - R2) * 0.5) return '#ff3b30';
|
|
205
|
-
return '#9c27b0';
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const baseTws = data.isFuture && store.futureForecast ? store.futureForecast.tws : (data.twsMin !== undefined ? data.twsMin : data.twsPeak);
|
|
209
|
-
const peakTws = data.isFuture && store.futureForecast ? store.futureForecast.gust : data.twsPeak;
|
|
210
|
-
|
|
211
|
-
const baseColor = getColorForSpeed(baseTws);
|
|
212
|
-
const peakColor = getColorForSpeed(peakTws);
|
|
213
|
-
|
|
214
|
-
if (baseColor !== peakColor) {
|
|
215
|
-
const startPt = polarToCartesian(200, 200, radius, data.twdMax);
|
|
216
|
-
const endPt = polarToCartesian(200, 200, radius, data.twdMin);
|
|
217
|
-
|
|
218
|
-
const xml = `
|
|
219
|
-
<linearGradient id="${gradId}" x1="${startPt.x.toFixed(1)}" y1="${startPt.y.toFixed(1)}" x2="${endPt.x.toFixed(1)}" y2="${endPt.y.toFixed(1)}" gradientUnits="userSpaceOnUse">
|
|
220
|
-
<stop offset="0%" stop-color="${baseColor}" />
|
|
221
|
-
<stop offset="50%" stop-color="${peakColor}" />
|
|
222
|
-
<stop offset="100%" stop-color="${baseColor}" />
|
|
223
|
-
</linearGradient>
|
|
224
|
-
`;
|
|
225
|
-
defsContainer.innerHTML += xml;
|
|
226
|
-
strokeColor = `url(#${gradId})`;
|
|
227
|
-
} else {
|
|
228
|
-
strokeColor = baseColor;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
|
|
232
|
-
|
|
233
|
-
if (!data.isFuture) {
|
|
234
|
-
const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
235
|
-
borderPath.setAttribute("d", pathData);
|
|
236
|
-
borderPath.setAttribute("fill", "none");
|
|
237
|
-
borderPath.setAttribute("stroke", "#000000");
|
|
238
|
-
borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
|
|
239
|
-
borderPath.setAttribute("stroke-linecap", "round");
|
|
240
|
-
borderPath.setAttribute("opacity", opacityValue);
|
|
241
|
-
ringsContainer.appendChild(borderPath);
|
|
242
|
-
} else {
|
|
243
|
-
const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
244
|
-
borderPath.setAttribute("d", pathData);
|
|
245
|
-
borderPath.setAttribute("fill", "none");
|
|
246
|
-
const isNight = document.body.classList.contains('night-mode');
|
|
247
|
-
borderPath.setAttribute("stroke", isNight ? "#220000" : "#90a4ae");
|
|
248
|
-
borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
|
|
249
|
-
borderPath.setAttribute("stroke-linecap", "round");
|
|
250
|
-
borderPath.setAttribute("opacity", "0.6");
|
|
251
|
-
ringsContainer.appendChild(borderPath);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
255
|
-
mainPath.setAttribute("d", pathData);
|
|
256
|
-
mainPath.setAttribute("fill", "none");
|
|
257
|
-
mainPath.setAttribute("stroke", strokeColor);
|
|
258
|
-
mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
|
|
259
|
-
mainPath.setAttribute("stroke-linecap", "round");
|
|
260
|
-
mainPath.setAttribute("opacity", data.isFuture ? "0.7" : opacityValue);
|
|
261
|
-
mainPath.id = data.isFuture ? "" : (index === 1 ? "active-present-arc" : "");
|
|
262
|
-
ringsContainer.appendChild(mainPath);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// Disegno del LED lampeggiante del meteo-trend
|
|
266
|
-
if (activeRing && !activeRing.isCalm) {
|
|
267
|
-
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
268
|
-
const isNav = (typeof store !== 'undefined' && store.isNavigating);
|
|
269
|
-
const strategicWindowMs = (isNav ? 15 : 60) * 60000;
|
|
270
|
-
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
271
|
-
|
|
272
|
-
if (twdNow && twdRef) {
|
|
273
|
-
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
274
|
-
|
|
275
|
-
if (Math.abs(deltaMeteo) > 6.0) {
|
|
276
|
-
const isSouth = store.raw["navigation.position"] && store.raw["navigation.position"].latitude < 0;
|
|
277
|
-
let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#00C851" : "#ff3b30") : (deltaMeteo > 0 ? "#00C851" : "#ff3b30");
|
|
278
|
-
|
|
279
|
-
const radiusAnello1 = ringRadii[1];
|
|
280
|
-
const angleTarget = deltaMeteo > 0 ? activeRing.twdMax : activeRing.twdMin;
|
|
281
|
-
const pt = polarToCartesian(200, 200, radiusAnello1, angleTarget);
|
|
282
|
-
|
|
283
|
-
const led = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
284
|
-
led.setAttribute("cx", pt.x.toFixed(1));
|
|
285
|
-
led.setAttribute("cy", pt.y.toFixed(1));
|
|
286
|
-
led.setAttribute("r", "5.5");
|
|
287
|
-
led.setAttribute("fill", meteoColor);
|
|
288
|
-
led.setAttribute("class", "is-trending");
|
|
289
|
-
led.setAttribute("filter", "url(#radar-center-glow)");
|
|
290
|
-
ringsContainer.appendChild(led);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ROTAZIONE TATTICA BARCA (North-Up Alignment)
|
|
296
|
-
// Orientiamo l'icona della barca in base alla Prua Vera (HDG)
|
|
297
|
-
const boatIcon = document.getElementById('radar-boat-icon');
|
|
298
|
-
if (boatIcon && typeof store !== 'undefined' && store.raw) {
|
|
299
|
-
const hdgRad = store.raw["navigation.headingTrue"] !== undefined ? store.raw["navigation.headingTrue"] : 0;
|
|
300
|
-
const hdgDeg = Math.round(radToDeg(hdgRad));
|
|
301
|
-
|
|
302
|
-
// Applicazione della rotazione centrata sull'asse (200, 200) dell'SVG
|
|
303
|
-
boatIcon.setAttribute("transform", `rotate(${hdgDeg}, 200, 200) translate(0, 5)`);
|
|
304
|
-
}
|
|
305
|
-
}
|
package/weather_debug.html
DELETED
|
@@ -1,181 +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>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>
|