Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/features/rap/extra/reportMetadata/Legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default function Legend({ showTaf, showNws, showOp40 }: LegendProps) {
{showTaf && (
<LegendItem>
<StyledPlaneSvg />
Terminal Aerodrome Forecast (TAF) location
METAR & TAF location
</LegendItem>
)}
</Container>
Expand Down
2 changes: 1 addition & 1 deletion src/features/rap/extra/reportMetadata/ReportMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const MapController = () => {
];
const airportPosition: LatLngExpression | undefined =
aviationWeather && typeof aviationWeather === "object"
? [aviationWeather.lat, aviationWeather.lon]
? [aviationWeather.taf.lat, aviationWeather.taf.lon]
: undefined;

useEffect(() => {
Expand Down
11 changes: 8 additions & 3 deletions src/features/weather/aviation/DetailedAviationReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Forecast, {
getTimeFormatString,
} from "./Forecast";
import { TemperatureUnit } from "../../rap/extra/settings/settingEnums";
import { metarReport } from "../weatherSliceLazy";
import MetarDetail from "./MetarDetail";

const Container = styled.div`
overflow: hidden;
Expand All @@ -29,7 +31,7 @@ const Description = styled.div`
margin: 0 1rem 1rem;
`;

const Forecasts = styled.div`
export const Forecasts = styled.div`
display: flex;
flex-direction: column;
gap: 1.5rem;
Expand Down Expand Up @@ -60,20 +62,23 @@ export default function DetailedAviationReport({
const timeZone = useAppSelector(timeZoneSelector);
const temperatureUnit = useAppSelector((state) => state.user.temperatureUnit);
const timeFormat = useAppSelector((state) => state.user.timeFormat);
const metar = useAppSelector(metarReport);

function formatTemperature(temperatureInC: number): string {
switch (temperatureUnit) {
case TemperatureUnit.Celsius:
return `${temperatureInC}℃`;
case TemperatureUnit.Fahrenheit:
return `${cToF(temperatureInC)}℉`;
return `${Math.round(cToF(temperatureInC))}℉`;
}
}

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

return (
<Container>
{metar ? <MetarDetail metar={metar} /> : ""}

<Title>Forecast</Title>

<Description>
Expand Down Expand Up @@ -135,5 +140,5 @@ export default function DetailedAviationReport({
}

export function cToF(celsius: number): number {
return Math.round((celsius * 9) / 5 + 32);
return (celsius * 9) / 5 + 32;
}
54 changes: 26 additions & 28 deletions src/features/weather/aviation/Forecast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import React from "react";
import { notEmpty } from "../../../helpers/array";
import { capitalizeFirstLetter } from "../../../helpers/string";
import {
determineCeilingFromClouds,
FlightCategory,
formatDescriptive,
formatHeight,
Expand All @@ -26,12 +25,15 @@ import {
} from "../../../helpers/taf";
import { useAppSelector } from "../../../hooks";
import { timeZoneSelector } from "../weatherSlice";
import Cloud from "./Cloud";
import Wind from "./cells/Wind";
import WindShear from "./cells/WindShear";
import { TimeFormat } from "../../rap/extra/settings/settingEnums";
import Clouds from "./cells/Clouds";
import Ceiling from "./cells/Ceiling";

const Container = styled.div<{ type: WeatherChangeType | undefined }>`
export const Container = styled.div<{
type: WeatherChangeType | undefined | "METAR";
}>`
padding: 1rem;
display: flex;
flex-direction: column;
Expand All @@ -54,22 +56,26 @@ const Container = styled.div<{ type: WeatherChangeType | undefined }>`
return css`
border-left-color: #0095ff5d;
`;
case "METAR":
return css`
border-left-color: #6d0050;
`;
}
}}
`;

const Header = styled.div`
export const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: -0.25rem;
`;

const Text = styled.p`
export const Text = styled.p`
margin: 0;
`;

const Category = styled.div<{ category: FlightCategory }>`
export const Category = styled.div<{ category: FlightCategory }>`
display: inline-block;
padding: 2px 8px;

Expand All @@ -79,7 +85,7 @@ const Category = styled.div<{ category: FlightCategory }>`
${({ category }) => getFlightCategoryCssColor(category)}
`;

const Table = styled.table`
export const Table = styled.table`
width: 100%;
table-layout: fixed;

Expand All @@ -95,7 +101,7 @@ const Table = styled.table`
}
`;

const Raw = styled.div`
export const Raw = styled.div`
padding: 0.5rem;

background: rgba(0, 0, 0, 0.5);
Expand All @@ -114,7 +120,6 @@ export default function Forecast({ data }: ForecastProps) {

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

const ceiling = determineCeilingFromClouds(data.clouds);
const category = getFlightCategory(
data.visibility,
data.clouds,
Expand Down Expand Up @@ -200,15 +205,10 @@ export default function Forecast({ data }: ForecastProps) {
<tr>
<td>Clouds</td>
<td>
{data.clouds.map((cloud, index) => (
<React.Fragment key={index}>
<Cloud data={cloud} />
<br />
</React.Fragment>
))}
{data.verticalVisibility != null ? (
<>Obscured sky</>
) : undefined}
<Clouds
clouds={data.clouds}
verticalVisibility={data.verticalVisibility}
/>
</td>
</tr>
) : (
Expand All @@ -229,14 +229,10 @@ export default function Forecast({ data }: ForecastProps) {
<tr>
<td>Ceiling</td>
<td>
{ceiling?.height != null
? `${formatHeight(ceiling.height, heightUnit)} AGL`
: data.verticalVisibility
? `Vertical visibility ${formatHeight(
data.verticalVisibility,
heightUnit
)} AGL`
: `At least ${formatHeight(12_000, heightUnit)} AGL`}
<Ceiling
clouds={data.clouds}
verticalVisibility={data.verticalVisibility}
/>
</td>
</tr>
) : (
Expand Down Expand Up @@ -305,15 +301,17 @@ export default function Forecast({ data }: ForecastProps) {
))}
</td>
</tr>
) : undefined}
) : (
""
)}
</tbody>
</Table>
<Raw>{data.raw}</Raw>
</Container>
);
}

function formatWeather(weather: IWeatherCondition[]): React.ReactNode {
export function formatWeather(weather: IWeatherCondition[]): React.ReactNode {
return (
<>
{capitalizeFirstLetter(
Expand Down
180 changes: 180 additions & 0 deletions src/features/weather/aviation/MetarDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { IMetarDated, RemarkType } from "metar-taf-parser";
import {
Category,
Container,
Header,
Raw,
Table,
formatWeather,
} from "./Forecast";
import { formatVisibility, getFlightCategory } from "../../../helpers/taf";
import Wind from "./cells/Wind";
import WindShear from "./cells/WindShear";
import { useAppSelector } from "../../../hooks";
import { Forecasts } from "./DetailedAviationReport";
import Temperature from "./cells/Temperature";
import Pressure from "./cells/Pressure";
import RelativeTime from "../../../shared/RelativeTime";
import Remarks from "./cells/Remarks";
import Clouds from "./cells/Clouds";
import Ceiling from "./cells/Ceiling";
import Humidity from "./cells/Humidity";

interface MetarDetailProps {
metar: IMetarDated;
}

export default function MetarDetail({ metar }: MetarDetailProps) {
const distanceUnit = useAppSelector((state) => state.user.distanceUnit);
const aviationWeather = useAppSelector(
(state) => state.weather.aviationWeather
);

if (
!aviationWeather ||
aviationWeather === "failed" ||
aviationWeather === "not-available" ||
aviationWeather === "pending" ||
!aviationWeather.metar
)
return <></>;

const category = getFlightCategory(
metar.visibility,
metar.clouds,
metar.verticalVisibility
);

const highPrecisionTemperatureDewPoint = (() => {
for (const remark of metar.remarks) {
if (remark.type === RemarkType.HourlyTemperatureDewPoint) return remark;
}
})();
const temperature =
highPrecisionTemperatureDewPoint?.temperature ?? metar.temperature;
const dewPoint = highPrecisionTemperatureDewPoint?.dewPoint ?? metar.dewPoint;

return (
<Forecasts>
<Container type="METAR">
<Header>
<span>
Observed conditions <RelativeTime date={metar.issued} />
</span>
<Category category={category}>{category}</Category>
</Header>

<Table>
<tbody>
{temperature != null && (
<tr>
<td>Temperature</td>
<td>
<Temperature temperatureInC={temperature} />
</td>
</tr>
)}
{dewPoint != null && (
<tr>
<td>Dew Point</td>
<td>
<Temperature temperatureInC={dewPoint} />{" "}
{temperature != null ? (
<>
{" "}
[{" "}
<Humidity
temperature={temperature}
dewPoint={dewPoint}
/>{" "}
]
</>
) : (
""
)}
</td>
</tr>
)}
{metar.altimeter && (
<tr>
<td>Pressure</td>
<td>
<Pressure altimeter={metar.altimeter} />
</td>
</tr>
)}
{metar.wind && (
<tr>
<td>Wind</td>
<td>
<Wind wind={metar.wind} />
</td>
</tr>
)}
{metar.windShear && (
<tr>
<td>Wind Shear</td>
<td>
<WindShear windShear={metar.windShear} />
</td>
</tr>
)}
{metar.clouds.length || metar.verticalVisibility != null ? (
<tr>
<td>Clouds</td>
<td>
<Clouds
clouds={metar.clouds}
verticalVisibility={metar.verticalVisibility}
/>
</td>
</tr>
) : (
""
)}
{metar.visibility && (
<tr>
<td>Visibility</td>
<td>
{formatVisibility(metar.visibility, distanceUnit)}{" "}
{metar.visibility.ndv && "No directional visibility"}{" "}
</td>
</tr>
)}
{metar.visibility &&
(metar.clouds.length || metar.verticalVisibility != null) ? (
<tr>
<td>Ceiling</td>
<td>
<Ceiling
clouds={metar.clouds}
verticalVisibility={metar.verticalVisibility}
/>
</td>
</tr>
) : (
""
)}
{metar.weatherConditions.length ? (
<tr>
<td>Weather</td>
<td>{formatWeather(metar.weatherConditions)}</td>
</tr>
) : undefined}
{metar.remarks.length ? (
<tr>
<td>Remarks</td>
<td>
<Remarks remarks={metar.remarks} />
</td>
</tr>
) : (
""
)}
</tbody>
</Table>
<Raw>{aviationWeather.metar.raw}</Raw>
</Container>
</Forecasts>
);
}
Loading