@sailingrotevista/rotevista-dash 8.0.1 → 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.

@@ -1,280 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import { HistoryPoint, WindRadarSlot, FutureForecast, AppConfig } from '../types';
3
- import {
4
- CALM_THRESHOLD_KTS,
5
- ringRadii,
6
- ARC_STROKE_WIDTH,
7
- BORDER_STROKE_WIDTH,
8
- polarToCartesian,
9
- describeArc,
10
- calculateActive30mRing,
11
- } from '../utils/radarMath';
12
- import { radToDeg } from '../utils/math';
13
-
14
- interface WeatherRadarProps {
15
- twdHistory: HistoryPoint[];
16
- twsHistory: HistoryPoint[];
17
- windRadarSlots: WindRadarSlot[];
18
- futureForecast: FutureForecast | null;
19
- headingTrueDeg: number;
20
- config: AppConfig;
21
- meteoLed: { active: boolean; color: string; angleTarget: number } | null;
22
- onSwitchToGauge: () => void;
23
- }
24
-
25
- export const WeatherRadar: React.FC<WeatherRadarProps> = ({
26
- twdHistory,
27
- twsHistory,
28
- windRadarSlots,
29
- futureForecast,
30
- headingTrueDeg,
31
- config,
32
- meteoLed,
33
- onSwitchToGauge,
34
- }) => {
35
- // Generate radar ticks
36
- const radarTicks = useMemo(() => {
37
- const list: React.ReactNode[] = [];
38
- for (let i = 0; i < 360; i += 10) {
39
- const isMajor = i % 30 === 0;
40
- list.push(
41
- <line
42
- key={`rtick-${i}`}
43
- x1="200"
44
- y1="40"
45
- x2="200"
46
- y2={isMajor ? 60 : 50}
47
- stroke={isMajor ? '#000' : '#bbb'}
48
- strokeWidth={isMajor ? 2 : 1}
49
- transform={`rotate(${i}, 200, 200)`}
50
- />
51
- );
52
- }
53
- return list;
54
- }, []);
55
-
56
- const getColorForSpeed = (tws: number) => {
57
- const r1 = config.graphs.reef1 || 15;
58
- const r2 = config.graphs.reef2 || 20;
59
- const r3 = r2 + (r2 - r1);
60
- if (tws < r1 * 0.4) return '#ffffff';
61
- if (tws < r1 * 0.75) return '#00C851';
62
- if (tws < r1) return '#ff9800';
63
- if (tws < r2) return '#ffaa00';
64
- if (tws < r2 + (r3 - r2) * 0.5) return '#ff3b30';
65
- return '#9c27b0';
66
- };
67
-
68
- const radarRings = useMemo(() => {
69
- const now = Date.now();
70
- const current30mSlot = Math.floor(now / 1800000) * 1800000;
71
- const radarDataList: Array<{
72
- twdMin: number;
73
- twdMax: number;
74
- twsPeak: number;
75
- twsMin?: number;
76
- isCalm?: boolean;
77
- isFuture?: boolean;
78
- } | null> = [];
79
-
80
- // Ring 0: Future Forecast
81
- if (futureForecast) {
82
- const twdDeg = Math.round(radToDeg(futureForecast.twd));
83
- radarDataList.push({
84
- twdMin: (twdDeg - 20 + 360) % 360,
85
- twdMax: (twdDeg + 20 + 360) % 360,
86
- twsPeak: futureForecast.gust || futureForecast.tws,
87
- twsMin: futureForecast.tws,
88
- isFuture: true,
89
- isCalm: false,
90
- });
91
- } else {
92
- radarDataList.push(null);
93
- }
94
-
95
- // Ring 1: Active Present 30m mobile ring
96
- const activeRing = calculateActive30mRing(twdHistory, twsHistory);
97
- radarDataList.push(activeRing);
98
-
99
- // Rings 2-7: Historical consolidated slots
100
- const slots = windRadarSlots || [];
101
- for (let i = 1; i <= 6; i++) {
102
- const targetTimestamp = current30mSlot - i * 1800000;
103
- const matched = slots.find(s => s.timestamp === targetTimestamp);
104
- if (matched) {
105
- radarDataList.push({
106
- twdMin: Math.round(radToDeg(matched.twdMin)),
107
- twdMax: Math.round(radToDeg(matched.twdMax)),
108
- twsPeak: matched.twsPeak,
109
- twsMin: matched.twsMin !== undefined ? matched.twsMin : matched.twsPeak,
110
- isCalm: matched.twsPeak < CALM_THRESHOLD_KTS,
111
- isFuture: false,
112
- });
113
- } else {
114
- radarDataList.push(null);
115
- }
116
- }
117
-
118
- const elements: React.ReactNode[] = [];
119
- const defGradients: React.ReactNode[] = [];
120
-
121
- radarDataList.forEach((data, index) => {
122
- if (!data) return;
123
- const radius = ringRadii[index];
124
- const gradId = `chord-gradient-${index}`;
125
-
126
- if (data.isCalm || data.twsPeak < CALM_THRESHOLD_KTS) {
127
- elements.push(
128
- <circle
129
- key={`calm-ring-${index}`}
130
- cx="200"
131
- cy="200"
132
- r={radius}
133
- fill="none"
134
- stroke="#b0bec5"
135
- strokeWidth="1.2"
136
- strokeDasharray="4, 4"
137
- />
138
- );
139
- return;
140
- }
141
-
142
- const baseTws = data.isFuture && futureForecast ? futureForecast.tws : (data.twsMin ?? data.twsPeak);
143
- const peakTws = data.isFuture && futureForecast ? futureForecast.gust ?? data.twsPeak : data.twsPeak;
144
-
145
- const baseColor = getColorForSpeed(baseTws);
146
- const peakColor = getColorForSpeed(peakTws);
147
-
148
- let strokeColor = baseColor;
149
-
150
- if (baseColor !== peakColor) {
151
- const startPt = polarToCartesian(200, 200, radius, data.twdMax);
152
- const endPt = polarToCartesian(200, 200, radius, data.twdMin);
153
-
154
- defGradients.push(
155
- <linearGradient
156
- key={`grad-${index}`}
157
- id={gradId}
158
- x1={startPt.x.toFixed(1)}
159
- y1={startPt.y.toFixed(1)}
160
- x2={endPt.x.toFixed(1)}
161
- y2={endPt.y.toFixed(1)}
162
- gradientUnits="userSpaceOnUse"
163
- >
164
- <stop offset="0%" stopColor={baseColor} />
165
- <stop offset="50%" stopColor={peakColor} />
166
- <stop offset="100%" stopColor={baseColor} />
167
- </linearGradient>
168
- );
169
- strokeColor = `url(#${gradId})`;
170
- }
171
-
172
- const pathData = describeArc(200, 200, radius, data.twdMin, data.twdMax);
173
-
174
- // Border path
175
- elements.push(
176
- <path
177
- key={`border-${index}`}
178
- d={pathData}
179
- fill="none"
180
- stroke={data.isFuture ? '#90a4ae' : '#000000'}
181
- strokeWidth={BORDER_STROKE_WIDTH}
182
- strokeLinecap="round"
183
- opacity={data.isFuture ? 0.6 : 1}
184
- />
185
- );
186
-
187
- // Main colored arc
188
- elements.push(
189
- <path
190
- key={`arc-${index}`}
191
- id={data.isFuture ? undefined : index === 1 ? 'active-present-arc' : undefined}
192
- d={pathData}
193
- fill="none"
194
- stroke={strokeColor}
195
- strokeWidth={ARC_STROKE_WIDTH}
196
- strokeLinecap="round"
197
- opacity={data.isFuture ? 0.7 : 1}
198
- />
199
- );
200
- });
201
-
202
- return { elements, defGradients };
203
- }, [futureForecast, twdHistory, twsHistory, windRadarSlots, config.graphs]);
204
-
205
- const ledPoint = useMemo(() => {
206
- if (!meteoLed) return null;
207
- return polarToCartesian(200, 200, ringRadii[1], meteoLed.angleTarget);
208
- }, [meteoLed]);
209
-
210
- return (
211
- <svg
212
- id="wind-radar"
213
- viewBox="35 38 330 395"
214
- preserveAspectRatio="xMidYMid meet"
215
- onClick={onSwitchToGauge}
216
- style={{ cursor: 'pointer' }}
217
- >
218
- <defs id="radar-gradients">
219
- <clipPath id="radar-boat-clip">
220
- <circle cx="200" cy="200" r="50" />
221
- </clipPath>
222
- <filter id="radar-center-glow" x="-20%" y="-20%" width="140%" height="140%">
223
- <feDropShadow dx="0" dy="0" stdDeviation="8" floodColor="#aaaaaa" floodOpacity={0.5} />
224
- </filter>
225
- {radarRings.defGradients}
226
- </defs>
227
-
228
- <circle cx="200" cy="200" r="160" fill="rgb(252, 252, 252)" />
229
- <circle cx="200" cy="200" r="125" fill="rgb(240, 240, 240)" />
230
-
231
- <g id="radar-ticks">{radarTicks}</g>
232
-
233
- <g id="radar-tick-labels" fill="#000000" textAnchor="middle" dominantBaseline="middle" fontFamily="Arial" fontWeight="bold">
234
- <text fontSize="16" transform="translate(200, 74)">N</text>
235
- <text fontSize="16" transform="translate(326, 200) rotate(90)">E</text>
236
- <text fontSize="16" transform="translate(74, 200) rotate(-90)">W</text>
237
- <text fontSize="16" transform="translate(200, 326) rotate(180)">S</text>
238
- <text fontSize="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
239
- <text fontSize="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
240
- <text fontSize="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
241
- <text fontSize="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
242
- <text fontSize="11" transform="translate(137.5, 91.7) rotate(-30)">330</text>
243
- <text fontSize="11" transform="translate(91.7, 137.5) rotate(-60)">300</text>
244
- <text fontSize="11" transform="translate(91.7, 262.5) rotate(-120)">240</text>
245
- <text fontSize="11" transform="translate(137.5, 308.3) rotate(-150)">210</text>
246
- </g>
247
-
248
- {/* Orbit ring */}
249
- <circle id="ora-orbit" cx="200" cy="200" r={ringRadii[1]} fill="none" stroke="#cfd8dc" strokeWidth="1.2" strokeDasharray="4, 4" />
250
-
251
- {/* Rendered historical arcs */}
252
- <g id="radar-rings">{radarRings.elements}</g>
253
-
254
- {/* Meteo trend pulsing LED on present arc */}
255
- {ledPoint && meteoLed && (
256
- <circle
257
- cx={ledPoint.x.toFixed(1)}
258
- cy={ledPoint.y.toFixed(1)}
259
- r="5.5"
260
- fill={meteoLed.color}
261
- className="is-trending"
262
- filter="url(#radar-center-glow)"
263
- />
264
- )}
265
-
266
- {/* Center hotspot */}
267
- <circle id="radar-hotspot" cx="200" cy="200" r="55" fill="rgb(238, 238, 238)" stroke="#e0e0e0" strokeWidth="1" filter="url(#radar-center-glow)" cursor="pointer" />
268
-
269
- {/* Tactical boat silhouette oriented North-Up by True Heading */}
270
- <path
271
- id="radar-boat-icon"
272
- d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
273
- fill="#000000"
274
- transform={`rotate(${headingTrueDeg}, 200, 200) translate(0, 5)`}
275
- clipPath="url(#radar-boat-clip)"
276
- style={{ pointerEvents: 'none' }}
277
- />
278
- </svg>
279
- );
280
- };