@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.
- package/package.json +9 -1
- package/public/assets/{index-C4co33rh.css → index-BFSy7dOW.css} +1 -1
- package/public/assets/index-BHhAwNqq.js +9 -0
- package/public/index.html +2 -2
- package/bun.lock +0 -157
- package/index.html +0 -22
- package/metadata.json +0 -7
- package/public/assets/index-DvzZHuhg.js +0 -9
- package/src/App.tsx +0 -346
- package/src/components/AnalogGauge.tsx +0 -243
- package/src/components/DataBox.tsx +0 -116
- package/src/components/MiniCompass.tsx +0 -120
- package/src/components/Sparkline.tsx +0 -343
- package/src/components/WeatherRadar.tsx +0 -280
- package/src/hooks/useDashboard.ts +0 -1161
- package/src/main.tsx +0 -10
- package/src/style.css +0 -705
- package/src/types.ts +0 -88
- package/src/utils/math.ts +0 -153
- package/src/utils/radarMath.ts +0 -89
- package/src/vite-env.d.ts +0 -1
- package/static/icons/dash.png +0 -0
- package/static/manifest.json +0 -17
- package/tsconfig.json +0 -20
- package/vite.config.ts +0 -22
|
@@ -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
|
-
};
|