Skip to content

Commit 339de0a

Browse files
committed
Add METAR support
1 parent f2faae3 commit 339de0a

File tree

17 files changed

+553
-43
lines changed

17 files changed

+553
-43
lines changed

src/features/rap/extra/reportMetadata/Legend.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export default function Legend({ showTaf, showNws }: LegendProps) {
7272
{showTaf && (
7373
<LegendItem>
7474
<StyledPlaneSvg />
75-
Terminal Aerodrome Forecast (TAF) location
75+
METAR & TAF location
7676
</LegendItem>
7777
)}
7878
</Container>

src/features/rap/extra/reportMetadata/ReportMetadata.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const MapController = () => {
115115
const rapPosition: LatLngExpression = [rap[0].lat, -rap[0].lon];
116116
const airportPosition: LatLngExpression | undefined =
117117
aviationWeather && typeof aviationWeather === "object"
118-
? [aviationWeather.lat, aviationWeather.lon]
118+
? [aviationWeather.taf.lat, aviationWeather.taf.lon]
119119
: undefined;
120120

121121
useEffect(() => {

src/features/weather/aviation/DetailedAviationReport.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import Forecast, {
1111
getTimeFormatString,
1212
} from "./Forecast";
1313
import { TemperatureUnit } from "../../user/userSlice";
14+
import { metarReport } from "../weatherSliceLazy";
15+
import MetarDetail from "./MetarDetail";
1416

1517
const Container = styled.div`
1618
overflow: hidden;
@@ -29,7 +31,7 @@ const Description = styled.div`
2931
margin: 0 1rem 1rem;
3032
`;
3133

32-
const Forecasts = styled.div`
34+
export const Forecasts = styled.div`
3335
display: flex;
3436
flex-direction: column;
3537
gap: 1.5rem;
@@ -60,20 +62,23 @@ export default function DetailedAviationReport({
6062
const timeZone = useAppSelector(timeZoneSelector);
6163
const temperatureUnit = useAppSelector((state) => state.user.temperatureUnit);
6264
const timeFormat = useAppSelector((state) => state.user.timeFormat);
65+
const metar = useAppSelector(metarReport);
6366

6467
function formatTemperature(temperatureInC: number): string {
6568
switch (temperatureUnit) {
6669
case TemperatureUnit.Celsius:
6770
return `${temperatureInC}℃`;
6871
case TemperatureUnit.Fahrenheit:
69-
return `${cToF(temperatureInC)}℉`;
72+
return `${Math.round(cToF(temperatureInC))}℉`;
7073
}
7174
}
7275

7376
if (!timeZone) throw new Error("timezone undefined");
7477

7578
return (
7679
<Container>
80+
{metar ? <MetarDetail metar={metar} /> : ""}
81+
7782
<Title>Forecast</Title>
7883

7984
<Description>
@@ -134,6 +139,6 @@ export default function DetailedAviationReport({
134139
);
135140
}
136141

137-
function cToF(celsius: number): number {
138-
return Math.round((celsius * 9) / 5 + 32);
142+
export function cToF(celsius: number): number {
143+
return (celsius * 9) / 5 + 32;
139144
}

src/features/weather/aviation/Forecast.tsx

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import React from "react";
1212
import { notEmpty } from "../../../helpers/array";
1313
import { capitalizeFirstLetter } from "../../../helpers/string";
1414
import {
15-
determineCeilingFromClouds,
1615
FlightCategory,
1716
formatDescriptive,
1817
formatHeight,
@@ -26,12 +25,15 @@ import {
2625
} from "../../../helpers/taf";
2726
import { useAppSelector } from "../../../hooks";
2827
import { timeZoneSelector } from "../weatherSlice";
29-
import Cloud from "./Cloud";
3028
import Wind from "./cells/Wind";
3129
import WindShear from "./cells/WindShear";
3230
import { TimeFormat } from "../../user/userSlice";
31+
import Clouds from "./cells/Clouds";
32+
import Ceiling from "./cells/Ceiling";
3333

34-
const Container = styled.div<{ type: WeatherChangeType | undefined }>`
34+
export const Container = styled.div<{
35+
type: WeatherChangeType | undefined | "METAR";
36+
}>`
3537
padding: 1rem;
3638
display: flex;
3739
flex-direction: column;
@@ -54,22 +56,26 @@ const Container = styled.div<{ type: WeatherChangeType | undefined }>`
5456
return css`
5557
border-left-color: #0095ff5d;
5658
`;
59+
case "METAR":
60+
return css`
61+
border-left-color: #6d0050;
62+
`;
5763
}
5864
}}
5965
`;
6066

61-
const Header = styled.div`
67+
export const Header = styled.div`
6268
display: flex;
6369
justify-content: space-between;
6470
align-items: flex-start;
6571
margin-bottom: -0.25rem;
6672
`;
6773

68-
const Text = styled.p`
74+
export const Text = styled.p`
6975
margin: 0;
7076
`;
7177

72-
const Category = styled.div<{ category: FlightCategory }>`
78+
export const Category = styled.div<{ category: FlightCategory }>`
7379
display: inline-block;
7480
padding: 2px 8px;
7581
@@ -79,7 +85,7 @@ const Category = styled.div<{ category: FlightCategory }>`
7985
${({ category }) => getFlightCategoryCssColor(category)}
8086
`;
8187

82-
const Table = styled.table`
88+
export const Table = styled.table`
8389
width: 100%;
8490
table-layout: fixed;
8591
@@ -95,7 +101,7 @@ const Table = styled.table`
95101
}
96102
`;
97103

98-
const Raw = styled.div`
104+
export const Raw = styled.div`
99105
padding: 0.5rem;
100106
101107
background: rgba(0, 0, 0, 0.5);
@@ -114,7 +120,6 @@ export default function Forecast({ data }: ForecastProps) {
114120

115121
if (!timeZone) throw new Error("timezone undefined");
116122

117-
const ceiling = determineCeilingFromClouds(data.clouds);
118123
const category = getFlightCategory(
119124
data.visibility,
120125
data.clouds,
@@ -201,15 +206,10 @@ export default function Forecast({ data }: ForecastProps) {
201206
<tr>
202207
<td>Clouds</td>
203208
<td>
204-
{data.clouds.map((cloud, index) => (
205-
<React.Fragment key={index}>
206-
<Cloud data={cloud} />
207-
<br />
208-
</React.Fragment>
209-
))}
210-
{data.verticalVisibility != null ? (
211-
<>Obscured sky</>
212-
) : undefined}
209+
<Clouds
210+
clouds={data.clouds}
211+
verticalVisibility={data.verticalVisibility}
212+
/>
213213
</td>
214214
</tr>
215215
) : (
@@ -230,14 +230,10 @@ export default function Forecast({ data }: ForecastProps) {
230230
<tr>
231231
<td>Ceiling</td>
232232
<td>
233-
{ceiling?.height != null
234-
? `${formatHeight(ceiling.height, heightUnit)} AGL`
235-
: data.verticalVisibility
236-
? `Vertical visibility ${formatHeight(
237-
data.verticalVisibility,
238-
heightUnit
239-
)} AGL`
240-
: `At least ${formatHeight(12_000, heightUnit)} AGL`}
233+
<Ceiling
234+
clouds={data.clouds}
235+
verticalVisibility={data.verticalVisibility}
236+
/>
241237
</td>
242238
</tr>
243239
) : (
@@ -306,15 +302,17 @@ export default function Forecast({ data }: ForecastProps) {
306302
))}
307303
</td>
308304
</tr>
309-
) : undefined}
305+
) : (
306+
""
307+
)}
310308
</tbody>
311309
</Table>
312310
<Raw>{data.raw}</Raw>
313311
</Container>
314312
);
315313
}
316314

317-
function formatWeather(weather: IWeatherCondition[]): React.ReactNode {
315+
export function formatWeather(weather: IWeatherCondition[]): React.ReactNode {
318316
return (
319317
<>
320318
{capitalizeFirstLetter(
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { IMetarDated, RemarkType } from "metar-taf-parser";
2+
import {
3+
Category,
4+
Container,
5+
Header,
6+
Raw,
7+
Table,
8+
formatWeather,
9+
} from "./Forecast";
10+
import { formatVisibility, getFlightCategory } from "../../../helpers/taf";
11+
import Wind from "./cells/Wind";
12+
import WindShear from "./cells/WindShear";
13+
import { useAppSelector } from "../../../hooks";
14+
import { Forecasts } from "./DetailedAviationReport";
15+
import Temperature from "./cells/Temperature";
16+
import Pressure from "./cells/Pressure";
17+
import RelativeTime from "../../../shared/RelativeTime";
18+
import Remarks from "./cells/Remarks";
19+
import Clouds from "./cells/Clouds";
20+
import Ceiling from "./cells/Ceiling";
21+
import Humidity from "./cells/Humidity";
22+
23+
interface MetarDetailProps {
24+
metar: IMetarDated;
25+
}
26+
27+
export default function MetarDetail({ metar }: MetarDetailProps) {
28+
const distanceUnit = useAppSelector((state) => state.user.distanceUnit);
29+
const aviationWeather = useAppSelector(
30+
(state) => state.weather.aviationWeather
31+
);
32+
33+
if (
34+
!aviationWeather ||
35+
aviationWeather === "failed" ||
36+
aviationWeather === "not-available" ||
37+
aviationWeather === "pending" ||
38+
!aviationWeather.metar
39+
)
40+
return <></>;
41+
42+
const category = getFlightCategory(
43+
metar.visibility,
44+
metar.clouds,
45+
metar.verticalVisibility
46+
);
47+
48+
const highPrecisionTemperatureDewPoint = (() => {
49+
for (const remark of metar.remarks) {
50+
if (remark.type === RemarkType.HourlyTemperatureDewPoint) return remark;
51+
}
52+
})();
53+
const temperature =
54+
highPrecisionTemperatureDewPoint?.temperature ?? metar.temperature;
55+
const dewPoint = highPrecisionTemperatureDewPoint?.dewPoint ?? metar.dewPoint;
56+
57+
return (
58+
<Forecasts>
59+
<Container type="METAR">
60+
<Header>
61+
<span>
62+
Observed conditions <RelativeTime date={metar.issued} />
63+
</span>
64+
<Category category={category}>{category}</Category>
65+
</Header>
66+
67+
<Table>
68+
<tbody>
69+
{temperature != null && (
70+
<tr>
71+
<td>Temperature</td>
72+
<td>
73+
<Temperature temperatureInC={temperature} />
74+
</td>
75+
</tr>
76+
)}
77+
{dewPoint != null && (
78+
<tr>
79+
<td>Dew Point</td>
80+
<td>
81+
<Temperature temperatureInC={dewPoint} />{" "}
82+
{temperature != null ? (
83+
<>
84+
{" "}
85+
[{" "}
86+
<Humidity
87+
temperature={temperature}
88+
dewPoint={dewPoint}
89+
/>{" "}
90+
]
91+
</>
92+
) : (
93+
""
94+
)}
95+
</td>
96+
</tr>
97+
)}
98+
{metar.altimeter && (
99+
<tr>
100+
<td>Pressure</td>
101+
<td>
102+
<Pressure altimeter={metar.altimeter} />
103+
</td>
104+
</tr>
105+
)}
106+
{metar.wind && (
107+
<tr>
108+
<td>Wind</td>
109+
<td>
110+
<Wind wind={metar.wind} />
111+
</td>
112+
</tr>
113+
)}
114+
{metar.windShear && (
115+
<tr>
116+
<td>Wind Shear</td>
117+
<td>
118+
<WindShear windShear={metar.windShear} />
119+
</td>
120+
</tr>
121+
)}
122+
{metar.clouds.length || metar.verticalVisibility != null ? (
123+
<tr>
124+
<td>Clouds</td>
125+
<td>
126+
<Clouds
127+
clouds={metar.clouds}
128+
verticalVisibility={metar.verticalVisibility}
129+
/>
130+
</td>
131+
</tr>
132+
) : (
133+
""
134+
)}
135+
{metar.visibility && (
136+
<tr>
137+
<td>Visibility</td>
138+
<td>
139+
{formatVisibility(metar.visibility, distanceUnit)}{" "}
140+
{metar.visibility.ndv && "No directional visibility"}{" "}
141+
</td>
142+
</tr>
143+
)}
144+
{metar.visibility &&
145+
(metar.clouds.length || metar.verticalVisibility != null) ? (
146+
<tr>
147+
<td>Ceiling</td>
148+
<td>
149+
<Ceiling
150+
clouds={metar.clouds}
151+
verticalVisibility={metar.verticalVisibility}
152+
/>
153+
</td>
154+
</tr>
155+
) : (
156+
""
157+
)}
158+
{metar.weatherConditions.length ? (
159+
<tr>
160+
<td>Weather</td>
161+
<td>{formatWeather(metar.weatherConditions)}</td>
162+
</tr>
163+
) : undefined}
164+
{metar.remarks.length ? (
165+
<tr>
166+
<td>Remarks</td>
167+
<td>
168+
<Remarks remarks={metar.remarks} />
169+
</td>
170+
</tr>
171+
) : (
172+
""
173+
)}
174+
</tbody>
175+
</Table>
176+
<Raw>{aviationWeather.metar.raw}</Raw>
177+
</Container>
178+
</Forecasts>
179+
);
180+
}

0 commit comments

Comments
 (0)