@sailingrotevista/rotevista-dash 6.2.8 → 7.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.
- package/app.js +195 -592
- package/charts.js +275 -0
- package/gauge.js +82 -0
- package/index.html +142 -99
- package/index.js +8 -4
- package/package.json +1 -1
- package/{radar.html → sample_radar.html} +183 -74
- package/style.css +37 -0
- package/utils.js +140 -0
- package/weather-radar.js +281 -0
- /package/{test.html → debug_signalk_connection.html} +0 -0
- /package/{radaar debug.html → debug_weather_radar.html} +0 -0
package/weather-radar.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
const twdRecent = (store.twdMinuteBuffer || []).filter(p => p.time >= start30m);
|
|
63
|
+
const twsRecent = (store.twsMinuteBuffer || []).filter(p => p.time >= start30m);
|
|
64
|
+
|
|
65
|
+
if (twdRecent.length === 0) return null;
|
|
66
|
+
|
|
67
|
+
const twsVals = twsRecent.map(p => p.val).filter(v => isFinite(v));
|
|
68
|
+
const maxTws = twsVals.length > 0 ? Math.max(...twsVals) : 0;
|
|
69
|
+
const minTws = twsVals.length > 0 ? Math.min(...twsVals) : 0;
|
|
70
|
+
|
|
71
|
+
if (maxTws < CALM_THRESHOLD_KTS) {
|
|
72
|
+
return { twsPeak: maxTws, twsMin: minTws, twdMin: 0, twdMax: 360, isCalm: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let allAngles = [];
|
|
76
|
+
twdRecent.forEach(p => {
|
|
77
|
+
allAngles.push(p.val);
|
|
78
|
+
allAngles.push(p.min);
|
|
79
|
+
allAngles.push(p.max);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let sumSin = 0; let sumCos = 0;
|
|
83
|
+
allAngles.forEach(a => { sumSin += Math.sin(a); sumCos += Math.cos(a); });
|
|
84
|
+
const avgAngle = Math.atan2(sumSin, sumCos);
|
|
85
|
+
const finalAvg = (avgAngle + Math.PI * 2) % (Math.PI * 2);
|
|
86
|
+
|
|
87
|
+
let diffs = allAngles.map(a => {
|
|
88
|
+
let diff = a - finalAvg;
|
|
89
|
+
return Math.atan2(Math.sin(diff), Math.cos(diff));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
diffs.sort((a, b) => a - b);
|
|
93
|
+
const trimCount = Math.floor(diffs.length * 0.05);
|
|
94
|
+
const activeDiffs = diffs.slice(trimCount, diffs.length - trimCount);
|
|
95
|
+
const finalDiffs = activeDiffs.length > 0 ? activeDiffs : diffs;
|
|
96
|
+
|
|
97
|
+
const minDiff = Math.min(...finalDiffs);
|
|
98
|
+
const maxDiff = Math.max(...finalDiffs);
|
|
99
|
+
|
|
100
|
+
const finalMinDeg = Math.round(radToDeg((finalAvg + minDiff + Math.PI * 2) % (Math.PI * 2)));
|
|
101
|
+
const finalMaxDeg = Math.round(radToDeg((finalAvg + maxDiff + Math.PI * 2) % (Math.PI * 2)));
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
twdMin: finalMinDeg,
|
|
105
|
+
twdMax: finalMaxDeg,
|
|
106
|
+
twsPeak: maxTws,
|
|
107
|
+
twsMin: minTws,
|
|
108
|
+
isCalm: false
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- 4. MOTORE GRAFICO DI DISEGNO DEL RADAR ---
|
|
113
|
+
function renderRadar() {
|
|
114
|
+
const ringsContainer = document.getElementById('radar-rings');
|
|
115
|
+
const defsContainer = document.getElementById('radar-gradients');
|
|
116
|
+
|
|
117
|
+
if (!ringsContainer || !defsContainer) return; // Protezione se l'SVG del radar non è presente nel DOM
|
|
118
|
+
|
|
119
|
+
defsContainer.innerHTML = `
|
|
120
|
+
<clipPath id="radar-boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
121
|
+
<filter id="radar-center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
122
|
+
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
123
|
+
</filter>
|
|
124
|
+
`;
|
|
125
|
+
ringsContainer.innerHTML = '';
|
|
126
|
+
|
|
127
|
+
const oraOrbit = document.getElementById('ora-orbit');
|
|
128
|
+
if (oraOrbit) oraOrbit.setAttribute("r", ringRadii[1]);
|
|
129
|
+
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const current30mSlot = Math.floor(now / 1800000) * 1800000;
|
|
132
|
+
|
|
133
|
+
const radarDataList = [];
|
|
134
|
+
|
|
135
|
+
// 1. ANELLO 0: Previsione Futura Open-Meteo
|
|
136
|
+
if (store.futureForecast) {
|
|
137
|
+
const twdDeg = Math.round(radToDeg(store.futureForecast.twd));
|
|
138
|
+
radarDataList.push({
|
|
139
|
+
twdMin: (twdDeg - 20 + 360) % 360,
|
|
140
|
+
twdMax: (twdDeg + 20 + 360) % 360,
|
|
141
|
+
twsPeak: store.futureForecast.tws,
|
|
142
|
+
isFuture: true
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
radarDataList.push(null);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2. ANELLO 1: Presente Mobile (Real-Time Client-Side)
|
|
149
|
+
const activeRing = calculateActive30mRing();
|
|
150
|
+
radarDataList.push(activeRing);
|
|
151
|
+
|
|
152
|
+
// 3. ANELLI 2-7: Storico consolidato dal server
|
|
153
|
+
const slots = store.windRadarSlots || [];
|
|
154
|
+
for (let i = 1; i <= 6; i++) {
|
|
155
|
+
const targetTimestamp = current30mSlot - (i * 1800000);
|
|
156
|
+
const matchedSlot = slots.find(s => s.timestamp === targetTimestamp);
|
|
157
|
+
|
|
158
|
+
if (matchedSlot) {
|
|
159
|
+
radarDataList.push({
|
|
160
|
+
twdMin: Math.round(radToDeg(matchedSlot.twdMin)),
|
|
161
|
+
twdMax: Math.round(radToDeg(matchedSlot.twdMax)),
|
|
162
|
+
twsPeak: matchedSlot.twsPeak,
|
|
163
|
+
twsMin: matchedSlot.twsMin !== undefined ? matchedSlot.twsMin : matchedSlot.twsPeak,
|
|
164
|
+
isCalm: matchedSlot.twsPeak < CALM_THRESHOLD_KTS
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
radarDataList.push(null);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Disegno degli archi
|
|
172
|
+
radarDataList.forEach((data, index) => {
|
|
173
|
+
if (!data) return;
|
|
174
|
+
|
|
175
|
+
const radius = ringRadii[index];
|
|
176
|
+
const gradId = `chord-gradient-${index}`;
|
|
177
|
+
const opacityValue = 1;
|
|
178
|
+
|
|
179
|
+
if (data.isCalm || data.twsPeak < CALM_THRESHOLD_KTS) {
|
|
180
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
181
|
+
circle.setAttribute("cx", "200");
|
|
182
|
+
circle.setAttribute("cy", "200");
|
|
183
|
+
circle.setAttribute("r", radius);
|
|
184
|
+
circle.setAttribute("fill", "none");
|
|
185
|
+
circle.setAttribute("stroke", document.body.classList.contains('night-mode') ? "#440000" : "#b0bec5");
|
|
186
|
+
circle.setAttribute("stroke-width", "1.2");
|
|
187
|
+
circle.setAttribute("stroke-dasharray", "4, 4");
|
|
188
|
+
ringsContainer.appendChild(circle);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let strokeColor = '';
|
|
193
|
+
|
|
194
|
+
const getColorForSpeed = (tws) => {
|
|
195
|
+
const R1 = CONFIG.graphs.reef1 || 15;
|
|
196
|
+
const R2 = CONFIG.graphs.reef2 || 20;
|
|
197
|
+
const R3 = R2 + (R2 - R1);
|
|
198
|
+
if (tws < R1 * 0.4) return '#ffffff';
|
|
199
|
+
if (tws < R1 * 0.75) return '#00C851';
|
|
200
|
+
if (tws < R1) return '#ff9800';
|
|
201
|
+
if (tws < R2) return '#ffaa00';
|
|
202
|
+
if (tws < R2 + (R3 - R2) * 0.5) return '#ff3b30';
|
|
203
|
+
return '#9c27b0';
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const baseTws = data.isFuture && store.futureForecast ? store.futureForecast.tws : (data.twsMin !== undefined ? data.twsMin : data.twsPeak);
|
|
207
|
+
const peakTws = data.isFuture && store.futureForecast ? store.futureForecast.gust : data.twsPeak;
|
|
208
|
+
|
|
209
|
+
const baseColor = getColorForSpeed(baseTws);
|
|
210
|
+
const peakColor = getColorForSpeed(peakTws);
|
|
211
|
+
|
|
212
|
+
if (baseColor !== peakColor) {
|
|
213
|
+
const startPt = polarToCartesian(200, 200, radius, data.twdMax);
|
|
214
|
+
const endPt = polarToCartesian(200, 200, radius, data.twdMin);
|
|
215
|
+
|
|
216
|
+
const xml = `
|
|
217
|
+
<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">
|
|
218
|
+
<stop offset="0%" stop-color="${baseColor}" />
|
|
219
|
+
<stop offset="50%" stop-color="${peakColor}" />
|
|
220
|
+
<stop offset="100%" stop-color="${baseColor}" />
|
|
221
|
+
</linearGradient>
|
|
222
|
+
`;
|
|
223
|
+
defsContainer.innerHTML += xml;
|
|
224
|
+
strokeColor = `url(#${gradId})`;
|
|
225
|
+
} else {
|
|
226
|
+
strokeColor = baseColor;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
|
|
230
|
+
|
|
231
|
+
if (!data.isFuture) {
|
|
232
|
+
const borderPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
233
|
+
borderPath.setAttribute("d", pathData);
|
|
234
|
+
borderPath.setAttribute("fill", "none");
|
|
235
|
+
borderPath.setAttribute("stroke", "#000000");
|
|
236
|
+
borderPath.setAttribute("stroke-width", BORDER_STROKE_WIDTH);
|
|
237
|
+
borderPath.setAttribute("stroke-linecap", "round");
|
|
238
|
+
borderPath.setAttribute("opacity", opacityValue);
|
|
239
|
+
ringsContainer.appendChild(borderPath);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const mainPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
243
|
+
mainPath.setAttribute("d", pathData);
|
|
244
|
+
mainPath.setAttribute("fill", "none");
|
|
245
|
+
mainPath.setAttribute("stroke", strokeColor);
|
|
246
|
+
mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
|
|
247
|
+
mainPath.setAttribute("stroke-linecap", "round");
|
|
248
|
+
mainPath.setAttribute("opacity", data.isFuture ? "0.5" : opacityValue);
|
|
249
|
+
mainPath.id = data.isFuture ? "" : (index === 1 ? "active-present-arc" : "");
|
|
250
|
+
ringsContainer.appendChild(mainPath);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Disegno del LED lampeggiante del meteo-trend
|
|
254
|
+
if (activeRing && !activeRing.isCalm) {
|
|
255
|
+
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
256
|
+
const strategicWindowMs = (isNavigating ? 15 : 60) * 60000;
|
|
257
|
+
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
258
|
+
|
|
259
|
+
if (twdNow && twdRef) {
|
|
260
|
+
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
261
|
+
|
|
262
|
+
if (Math.abs(deltaMeteo) > 6.0) {
|
|
263
|
+
const isSouth = store.raw["navigation.position"] && store.raw["navigation.position"].latitude < 0;
|
|
264
|
+
let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#00C851" : "#ff3b30") : (deltaMeteo > 0 ? "#00C851" : "#ff3b30");
|
|
265
|
+
|
|
266
|
+
const radiusAnello1 = ringRadii[1];
|
|
267
|
+
const angleTarget = deltaMeteo > 0 ? activeRing.twdMax : activeRing.twdMin;
|
|
268
|
+
const pt = polarToCartesian(200, 200, radiusAnello1, angleTarget);
|
|
269
|
+
|
|
270
|
+
const led = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
271
|
+
led.setAttribute("cx", pt.x.toFixed(1));
|
|
272
|
+
led.setAttribute("cy", pt.y.toFixed(1));
|
|
273
|
+
led.setAttribute("r", "5.5");
|
|
274
|
+
led.setAttribute("fill", meteoColor);
|
|
275
|
+
led.setAttribute("class", "is-trending");
|
|
276
|
+
led.setAttribute("filter", "url(#radar-center-glow)");
|
|
277
|
+
ringsContainer.appendChild(led);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
File without changes
|
|
File without changes
|