@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.
@@ -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