diff --git a/src/features/outlook/Day.tsx b/src/features/outlook/Day.tsx
new file mode 100644
index 0000000..dc24155
--- /dev/null
+++ b/src/features/outlook/Day.tsx
@@ -0,0 +1,50 @@
+import styled from "@emotion/styled";
+import { formatInTimeZone } from "date-fns-tz";
+import { useAppSelector } from "../../hooks";
+import { timeZoneSelector } from "../weather/weatherSlice";
+
+const Table = styled.table`
+ width: 100%;
+
+ padding: 0;
+ border-collapse: collapse;
+ border: none;
+
+ text-align: center;
+`;
+
+const THead = styled.thead`
+ position: sticky;
+ top: 0;
+ transform: translateY(-0.5px);
+ z-index: 1;
+ background: var(--bg-bottom-sheet);
+`;
+
+const DayLabelCell = styled.th`
+ text-align: start;
+ padding: 8px 16px;
+`;
+
+interface DayProps {
+ date: Date;
+ hours: React.ReactNode[];
+}
+
+export default function Day({ hours, date }: DayProps) {
+ const timeZone = useAppSelector(timeZoneSelector);
+ if (!timeZone) throw new Error("timeZone needed");
+
+ return (
+
+
+
+
+ {formatInTimeZone(date, timeZone, "eeee, LLL d")}
+
+
+
+ {hours}
+
+ );
+}
diff --git a/src/features/outlook/Outlook.tsx b/src/features/outlook/Outlook.tsx
new file mode 100644
index 0000000..f759a2e
--- /dev/null
+++ b/src/features/outlook/Outlook.tsx
@@ -0,0 +1,11 @@
+import { useAppSelector } from "../../hooks";
+import OutlookTable from "./OutlookTable";
+
+export default function Outlook() {
+ const weather = useAppSelector((state) => state.weather.weather);
+
+ if (weather === "failed") return;
+ if (!weather || weather === "pending") return;
+
+ return ;
+}
diff --git a/src/features/outlook/OutlookRow.tsx b/src/features/outlook/OutlookRow.tsx
new file mode 100644
index 0000000..4a6633c
--- /dev/null
+++ b/src/features/outlook/OutlookRow.tsx
@@ -0,0 +1,158 @@
+import { formatInTimeZone } from "date-fns-tz";
+import { timeZoneSelector } from "../weather/weatherSlice";
+import { useAppSelector } from "../../hooks";
+import styled from "@emotion/styled";
+import WindIndicator from "../rap/WindIndicator";
+import WindSpeed from "./WindSpeed";
+import {
+ faCloudMoon,
+ faClouds,
+ faCloudsMoon,
+ faCloudsSun,
+ faCloudSun,
+ faMoon,
+ faSun,
+} from "@fortawesome/pro-duotone-svg-icons";
+import SunCalc from "suncalc";
+import { TemperatureText } from "../rap/cells/Temperature";
+import { Aside } from "../rap/cells/Altitude";
+import {
+ TemperatureUnit,
+ TimeFormat,
+} from "../rap/extra/settings/settingEnums";
+import { cToF } from "../weather/aviation/DetailedAviationReport";
+import { Observations } from "../weather/header/Weather";
+import { NWSWeatherObservation } from "../../services/nwsWeather";
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
+
+const Row = styled.tr<{ day: boolean }>`
+ display: flex;
+
+ > * {
+ flex: 1;
+ }
+
+ border-bottom: 1px solid #77777715;
+
+ background: ${({ day }) => (day ? "#ffffff07" : "transparent")};
+`;
+
+const TimeCell = styled.td`
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
+const StyledObservations = styled(Observations)`
+ font-size: 1em;
+ margin-right: 0;
+`;
+
+interface OutlookRowProps {
+ hour: Date;
+ windDirection: number;
+ windSpeed: number;
+ windGust: number;
+ temperature: number;
+ observations: NWSWeatherObservation[] | number;
+ skyCover: number;
+}
+
+export default function OutlookRow({
+ hour,
+ windDirection,
+ windSpeed,
+ windGust,
+ temperature: inCelsius,
+ observations,
+ skyCover,
+}: OutlookRowProps) {
+ const timeFormat = useAppSelector((state) => state.user.timeFormat);
+
+ const timeZone = useAppSelector(timeZoneSelector);
+ if (!timeZone) throw new Error("timeZone needed");
+
+ const coordinates = useAppSelector((state) => state.weather.coordinates);
+ if (!coordinates) throw new Error("coordinates not found");
+
+ const temperatureUnit = useAppSelector((state) => state.user.temperatureUnit);
+
+ const time = formatInTimeZone(hour, timeZone, timeFormatString(timeFormat));
+
+ const isDay =
+ SunCalc.getPosition(hour, coordinates.lat, coordinates.lon).altitude > 0;
+
+ const temperatureUnitLabel = (() => {
+ switch (temperatureUnit) {
+ case TemperatureUnit.Celsius:
+ return "C";
+ case TemperatureUnit.Fahrenheit:
+ return "F";
+ }
+ })();
+
+ const temperature = (() => {
+ switch (temperatureUnit) {
+ case TemperatureUnit.Celsius:
+ return inCelsius;
+ case TemperatureUnit.Fahrenheit:
+ return cToF(inCelsius);
+ }
+ })();
+
+ console.log(observations);
+
+ return (
+
+ {time}
+
+
+ |
+
+
+ {Math.round(temperature)} {" "}
+
+ |
+ |
+
+
+ |
+
+
+ |
+
+ );
+}
+
+function timeFormatString(timeFormat: TimeFormat): string {
+ switch (timeFormat) {
+ case TimeFormat.Twelve:
+ return "hha";
+ case TimeFormat.TwentyFour:
+ return "HHmm";
+ }
+}
+
+function getDefaultIcon(skyCover: number, isDay: boolean): IconProp {
+ switch (true) {
+ case skyCover < 20:
+ return isDay ? faSun : faMoon;
+ case skyCover < 60:
+ return isDay ? faCloudSun : faCloudMoon;
+ case skyCover < 80:
+ return isDay ? faCloudsSun : faCloudsMoon;
+ default:
+ return faClouds;
+ }
+}
diff --git a/src/features/outlook/OutlookTable.tsx b/src/features/outlook/OutlookTable.tsx
new file mode 100644
index 0000000..6d65bad
--- /dev/null
+++ b/src/features/outlook/OutlookTable.tsx
@@ -0,0 +1,117 @@
+import { addDays, eachHourOfInterval, startOfDay } from "date-fns";
+import { findValue, NWSWeather } from "../../services/nwsWeather";
+import { Weather } from "../weather/weatherSlice";
+import { OpenMeteoWeather } from "../../services/openMeteo";
+import { useMemo } from "react";
+import OutlookRow from "./OutlookRow";
+import compact from "lodash/fp/compact";
+import styled from "@emotion/styled";
+import Day from "./Day";
+
+const Rows = styled.div``;
+
+interface OutlookTableProps {
+ weather: Weather;
+}
+
+function getOutlook(
+ mapFn: (hour: Date, index: number) => React.ReactNode | undefined,
+) {
+ const hours = eachHourOfInterval({
+ start: new Date(),
+ end: addDays(new Date(), 7),
+ });
+
+ const data = compact(
+ hours.map((hour, index) => ({ node: mapFn(hour, index), hour })),
+ );
+
+ return Object.entries(
+ Object.groupBy(data, ({ hour }) => startOfDay(hour).getTime()),
+ ).map(([timeStr, hours]) => ({
+ date: new Date(+timeStr),
+ hours: hours!.map(({ node }) => node),
+ }));
+}
+
+export default function OutlookTable({ weather }: OutlookTableProps) {
+ const rows = (() => {
+ if ("properties" in weather) return ;
+ return ;
+ })();
+
+ return {rows};
+}
+
+function NWSOutlookRows({ weather }: { weather: NWSWeather }) {
+ const days = useMemo(
+ () =>
+ getOutlook((hour, index) => {
+ const windDirection = findValue(
+ hour,
+ weather.properties.windDirection,
+ )?.value;
+ const windSpeed = findValue(hour, weather.properties.windSpeed)?.value;
+ const windGust = findValue(hour, weather.properties.windGust)?.value;
+ const temperature = findValue(
+ hour,
+ weather.properties.temperature,
+ )?.value;
+ const observations = findValue(hour, weather.properties.weather)?.value;
+ const skyCover = findValue(hour, weather.properties.skyCover)?.value;
+
+ if (windDirection == null) return;
+ if (windSpeed == null) return;
+ if (windGust == null) return;
+ if (temperature == null) return;
+ if (observations == null) return;
+ if (skyCover == null) return;
+
+ return (
+
+ );
+ }),
+ [weather],
+ );
+
+ return days.map(({ date, hours }, index) => (
+
+ ));
+}
+function OpenMeteoOutlookRows({ weather }: { weather: OpenMeteoWeather }) {
+ const days = useMemo(
+ () =>
+ getOutlook((hour, index) => {
+ const data = weather.byUnixTimestamp[hour.getTime() / 1_000];
+
+ if (!data) return;
+
+ return (
+
+ );
+ }),
+ [weather],
+ );
+
+ return days.map(({ date, hours }, index) => (
+
+ ));
+}
diff --git a/src/features/outlook/WindSpeed.tsx b/src/features/outlook/WindSpeed.tsx
new file mode 100644
index 0000000..ca9522b
--- /dev/null
+++ b/src/features/outlook/WindSpeed.tsx
@@ -0,0 +1,49 @@
+import styled from "@emotion/styled";
+import { formatWind } from "../../helpers/taf";
+import { useAppSelector } from "../../hooks";
+import { SpeedUnit } from "metar-taf-parser";
+import { toMph, WindIcon } from "../weather/header/Wind";
+import { HeaderType } from "../weather/WeatherHeader";
+import { faWindsock } from "@fortawesome/pro-duotone-svg-icons";
+
+const Speed = styled.div`
+ word-spacing: -2px;
+`;
+
+const StyledWindIcon = styled(WindIcon)`
+ margin-right: 12px;
+`;
+interface WindSpeedProps {
+ speed: number;
+ gust: number;
+}
+
+export default function WindSpeed({ speed, gust }: WindSpeedProps) {
+ const speedUnit = useAppSelector((state) => state.user.speedUnit);
+ const speedFormatted = formatWind(
+ speed,
+ SpeedUnit.KilometersPerHour,
+ speedUnit,
+ false,
+ );
+ const gustFormatted = formatWind(
+ gust,
+ SpeedUnit.KilometersPerHour,
+ speedUnit,
+ false,
+ );
+
+ return (
+ <>
+
+
+ {speedFormatted}G{gustFormatted}
+
+ >
+ );
+}
diff --git a/src/features/rap/extra/Extra.tsx b/src/features/rap/extra/Extra.tsx
index 2c3c37f..41368cd 100644
--- a/src/features/rap/extra/Extra.tsx
+++ b/src/features/rap/extra/Extra.tsx
@@ -2,6 +2,7 @@ import React, { lazy, Suspense } from "react";
import styled from "@emotion/styled";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
+ faCalendarAlt,
faCog,
faFileAlt,
faLongArrowRight,
@@ -19,6 +20,7 @@ import InstallPrompt from "../../install/InstallPrompt";
import { isAfter } from "date-fns";
import Spinner from "../../../shared/Spinner";
import { useTranslation } from "react-i18next";
+import Outlook from "../../outlook/Outlook";
const ReportMetadata = lazy(() => import("./reportMetadata/ReportMetadata"));
@@ -37,6 +39,16 @@ const Container = styled.div`
margin: 0 auto;
`;
+const Line = styled.div`
+ display: flex;
+ gap: 1rem;
+
+ > * {
+ flex: 1;
+ min-width: 0;
+ }
+`;
+
export default function Extra() {
const { t } = useTranslation();
const weather = useAppSelector((state) => state.weather.weather);
@@ -63,6 +75,17 @@ export default function Extra() {
+
+ {t("Extended Forecast")}
+
+ }
+ title={t("Extended Forecast")}
+ >
+
+
+
{discussion !== "not-available" ? (
) : undefined}
-
- {t("Report Metadata")}
-
- }
- title={t("Report Metadata")}
- >
- }>
-
-
-
+
+
+ {t("Metadata")}
+
+ }
+ title={t("Report Metadata")}
+ >
+ }>
+
+
+
-
- {t("Settings")}
-
- }
- title={t("Settings")}
- >
-
-
+
+ {t("Settings")}
+
+ }
+ title={t("Settings")}
+ >
+
+
+
);
}
@@ -125,6 +150,8 @@ const ItemContainer = styled("div", {
transition: 100ms linear;
transition-property: opacity, filter;
+ container-type: inline-size;
+
${({ loading }) =>
loading &&
css`
@@ -147,6 +174,7 @@ const IconContainer = styled.div<{
height: 1.85rem;
position: relative;
margin-right: 1rem;
+ flex-shrink: 0;
&::before {
content: "";
@@ -193,6 +221,15 @@ const IconContainer = styled.div<{
const RightArrow = styled(FontAwesomeIcon)`
margin-left: auto;
opacity: 0.5;
+
+ @container (max-width: 140px) {
+ display: none;
+ }
+`;
+
+const Text = styled.span`
+ text-overflow: ellipsis;
+ overflow: hidden;
`;
interface IconProps {
@@ -221,7 +258,7 @@ function Item({
{!loading ? : }
- {children}
+ {children}
);
diff --git a/src/features/weather/header/NWSWeather.tsx b/src/features/weather/header/NWSWeather.tsx
index 37c4e3f..d3afc32 100644
--- a/src/features/weather/header/NWSWeather.tsx
+++ b/src/features/weather/header/NWSWeather.tsx
@@ -24,16 +24,24 @@ const Flex = styled.div`
interface NWSWeatherProps {
observations: NWSWeatherObservation[];
+ defaultIcon?: IconProp;
+ className?: string;
}
-export default function NWSWeather({ observations }: NWSWeatherProps) {
+export default function NWSWeather({
+ observations,
+ defaultIcon,
+ ...rest
+}: NWSWeatherProps) {
const observation: NWSWeatherObservation | undefined =
observations.find(({ weather }) => weather === "thunderstorms") ||
observations[0];
- if (!observation) return <>>;
+ if (!observation) return renderWithIcon(defaultIcon);
function renderTooltip() {
+ if (observations.every(({ weather }) => !weather)) return;
+
return capitalize(
observations
.map((observation) =>
@@ -45,21 +53,29 @@ export default function NWSWeather({ observations }: NWSWeatherProps) {
const icon = findIconFor(observation);
- if (!icon) return <>>;
+ if (!icon) return renderWithIcon(defaultIcon);
+
+ return renderWithIcon(icon);
+
+ function renderWithIcon(icon: IconProp | undefined) {
+ if (!icon) return <>>;
- return (
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+ );
+ }
}
function findIconFor(observation: NWSWeatherObservation): IconProp | undefined {
diff --git a/src/features/weather/header/WMOWeather.tsx b/src/features/weather/header/WMOWeather.tsx
index 1a50241..31447aa 100644
--- a/src/features/weather/header/WMOWeather.tsx
+++ b/src/features/weather/header/WMOWeather.tsx
@@ -8,6 +8,7 @@ import {
import { faSnowflake } from "@fortawesome/pro-light-svg-icons";
import { convertTitleCaseToSpaces } from "../../../helpers/string";
import Tooltip from "../../../shared/Tooltip";
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
const Flex = styled.div`
display: flex;
@@ -47,9 +48,15 @@ enum WMOWeatherCode {
interface WMOWeatherCodeProps {
wmoCode: WMOWeatherCode;
+ defaultIcon?: IconProp;
+ className?: string;
}
-export default function WMOWeather({ wmoCode }: WMOWeatherCodeProps) {
+export default function WMOWeather({
+ wmoCode,
+ defaultIcon,
+ ...rest
+}: WMOWeatherCodeProps) {
const icon = (() => {
switch (wmoCode) {
case WMOWeatherCode.LightRainShowers:
@@ -82,13 +89,21 @@ export default function WMOWeather({ wmoCode }: WMOWeatherCodeProps) {
}
})();
- if (!icon) return <>>;
+ if (!icon) return renderWithIcon(defaultIcon);
- return (
- convertTitleCaseToSpaces(WMOWeatherCode[wmoCode])}>
-
-
-
-
- );
+ return renderWithIcon(icon);
+
+ function renderWithIcon(icon: IconProp | undefined) {
+ if (!icon) return <>>;
+
+ return (
+ convertTitleCaseToSpaces(WMOWeatherCode[wmoCode])}
+ >
+
+
+
+
+ );
+ }
}
diff --git a/src/features/weather/header/Weather.tsx b/src/features/weather/header/Weather.tsx
index 5f83ad6..a14dd32 100644
--- a/src/features/weather/header/Weather.tsx
+++ b/src/features/weather/header/Weather.tsx
@@ -4,12 +4,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useMemo } from "react";
import { outputP3ColorFromRGB } from "../../../helpers/colors";
-import { findValue } from "../../../services/nwsWeather";
-import { WeatherResult as NWSWeatherResult } from "../weatherSlice";
+import { findValue, NWSWeatherObservation } from "../../../services/nwsWeather";
+import { WeatherResult } from "../weatherSlice";
import { keyframes } from "@emotion/css";
import { css } from "@emotion/react";
import NWSWeather from "./NWSWeather";
import WMOWeather from "./WMOWeather";
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
const thunderAnimate = keyframes`
0% {
@@ -84,7 +85,7 @@ export const WeatherIcon = styled(FontAwesomeIcon, {
interface WeatherProps {
date: string;
- weather: NWSWeatherResult | undefined;
+ weather: WeatherResult | undefined;
}
export default function Weather({ date, weather }: WeatherProps) {
@@ -103,8 +104,17 @@ export default function Weather({ date, weather }: WeatherProps) {
if (!observations) return <>>;
- if (Array.isArray(observations))
- return ;
+ return ;
+}
+
+interface ObservationsProps {
+ data: number | NWSWeatherObservation[];
+ defaultIcon?: IconProp | undefined;
+ className?: string;
+}
+
+export function Observations({ data, ...rest }: ObservationsProps) {
+ if (Array.isArray(data)) return ;
- return ;
+ return ;
}
diff --git a/src/features/weather/header/Wind.tsx b/src/features/weather/header/Wind.tsx
index 5f8e077..61d95a2 100644
--- a/src/features/weather/header/Wind.tsx
+++ b/src/features/weather/header/Wind.tsx
@@ -21,7 +21,7 @@ const colorScale = chroma
.scale(["#ffffff66", "#ffffff", "#fffb00", "#ff0000"])
.domain([10, 14, 16, 18]);
-const WindIcon = styled(FontAwesomeIcon, {
+export const WindIcon = styled(FontAwesomeIcon, {
shouldForwardProp: (prop) => prop !== "headerType",
})<{
headerType: HeaderType;
@@ -130,13 +130,13 @@ export default function Wind({ headerType, date, weather }: WindProps) {
);
}
-function toMph(speed: number): number {
+export function toMph(speed: number): number {
return speed * 0.621371;
}
/**
* @returns A "composite" wind value for gusts+sustained, similar to temperature "real feel"
*/
-function getCompositeWindValue(speed: number, gust: number): number {
+export function getCompositeWindValue(speed: number, gust: number): number {
return Math.max((gust - speed) * 2.5, speed, gust);
}
diff --git a/src/features/weather/weatherSlice.ts b/src/features/weather/weatherSlice.ts
index aa38e55..4a1f070 100644
--- a/src/features/weather/weatherSlice.ts
+++ b/src/features/weather/weatherSlice.ts
@@ -18,7 +18,7 @@ import { AxiosError } from "axios";
const UPDATE_INTERVAL_MINUTES = 30;
-type Weather = nwsWeather.NWSWeather | openMeteo.OpenMeteoWeather;
+export type Weather = nwsWeather.NWSWeather | openMeteo.OpenMeteoWeather;
export type WeatherResult =
// component has requested a weather, to be batched in next bulk request
@@ -585,10 +585,10 @@ export const getWeather =
let windsAloft, weather, elevationInM;
try {
- ({ windsAloft, weather, elevationInM } = await openMeteo.getWindsAloft(
- lat,
- lon,
- ));
+ [{ windsAloft, elevationInM }, weather] = await Promise.all([
+ openMeteo.getWindsAloft(lat, lon),
+ openMeteo.getWeather(lat, lon),
+ ]);
} catch (error) {
if (!isStale()) {
dispatch(windsAloftFailed());
@@ -614,14 +614,14 @@ export const getWeather =
if (isStale()) return;
if (!windsAloft) return; // pending
- const { elevation, weather } = windsAloft;
+ const { elevation } = windsAloft;
if (elevation == null) loadElevation();
else dispatch(elevationReceived(elevation));
await Promise.all([
loadNWSAlerts(),
- loadWeatherAndDiscussion(weather),
+ loadWeatherAndDiscussion(),
loadAviationWeather(),
loadAviationAlerts(),
]);
@@ -629,7 +629,6 @@ export const getWeather =
async function loadWindsAloft(): Promise<
| {
elevation?: number;
- weather?: Weather;
}
| undefined
> {
@@ -661,16 +660,13 @@ export const getWeather =
try {
// It would be nice in the future to intelligently choose an API
// instead of trial and error (and, it would be faster)
- const { windsAloft, weather } = await openMeteo.getWindsAloft(
- lat,
- lon,
- );
+ const { windsAloft } = await openMeteo.getWindsAloft(lat, lon);
if (isStale()) return;
dispatch(windsAloftReceived(windsAloft));
- return { elevation: windsAloft.elevationInM, weather };
+ return { elevation: windsAloft.elevationInM };
} catch (error) {
if (!isStale()) dispatch(windsAloftFailed());
@@ -679,7 +675,7 @@ export const getWeather =
}
}
- async function loadPointData(fallbackWeather?: Weather) {
+ async function loadPointData() {
if (getState().weather.weather === "pending") return;
dispatch(weatherLoading());
if (getState().weather.weather !== "pending") return;
@@ -731,8 +727,7 @@ export const getWeather =
// Likely Mexico or Canada
// We still need the timezone, so try to fall back anyways
- const weather =
- fallbackWeather ?? (await openMeteo.getWeather(lat, lon));
+ const weather = await openMeteo.getWeather(lat, lon);
if (isStale()) return;
@@ -855,8 +850,8 @@ export const getWeather =
}
}
- async function loadWeatherAndDiscussion(fallbackWeather?: Weather) {
- const gridId = await loadPointData(fallbackWeather);
+ async function loadWeatherAndDiscussion() {
+ const gridId = await loadPointData();
if (isStale()) return;
diff --git a/src/locales/de.json b/src/locales/de.json
index 093111a..51f5dcf 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -42,5 +42,7 @@
"Lapse rate value information": "Temperaturabnahme <0>{{lapseRate}}0>",
"Spread": "Differenz",
"Dewpt": "Taupkt",
- "Relative humidity acronym": "LF"
+ "Relative humidity acronym": "LF",
+ "Extended Forecast": "Erweiterte Vorhersage",
+ "Metadata": "Metadaten"
}
diff --git a/src/locales/en.json b/src/locales/en.json
index b9db444..3d1a358 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -42,5 +42,7 @@
"Lapse rate value information": "Lapse rate <0>{{lapseRate}}0>",
"Spread": "Spread",
"Dewpt": "Dewpt",
- "Relative humidity acronym": "RH"
+ "Relative humidity acronym": "RH",
+ "Extended Forecast": "Extended Forecast",
+ "Metadata": "Metadata"
}
diff --git a/src/locales/es.json b/src/locales/es.json
index 7fde8c3..42b31bb 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -42,5 +42,7 @@
"Lapse rate value information": "Tasa de enfriamiento adiabático <0>{{lapseRate}}0>",
"Spread": "Diferencia",
"Dewpt": "Écart",
- "Relative humidity acronym": "HR"
+ "Relative humidity acronym": "HR",
+ "Extended Forecast": "Pronóstico Extendido",
+ "Metadata": "Metadatos"
}
diff --git a/src/locales/fr.json b/src/locales/fr.json
index 1e26bc3..2dcc9cc 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -42,5 +42,7 @@
"Lapse rate value information": "Valeur du gradient adiabatique <0>{{lapseRate}}0>",
"Spread": "Écart",
"Dewpt": "P. rosée",
- "Relative humidity acronym": "HR"
+ "Relative humidity acronym": "HR",
+ "Extended Forecast": "Prévisions Étendues",
+ "Metadata": "Métadonnées"
}
diff --git a/src/locales/nl.json b/src/locales/nl.json
index fe7d2cd..d4ce2fc 100644
--- a/src/locales/nl.json
+++ b/src/locales/nl.json
@@ -42,5 +42,7 @@
"Lapse rate value information": "Dalingsnelheid <0>{{lapseRate}}0>",
"Spread": "Verschil",
"Dewpt": "Dauwpnt",
- "Relative humidity acronym": "RV"
+ "Relative humidity acronym": "RV",
+ "Extended Forecast": "Uitgebreide Voorspelling",
+ "Metadata": "Metadata"
}
diff --git a/src/services/nwsWeather.ts b/src/services/nwsWeather.ts
index 744c28f..053bc17 100644
--- a/src/services/nwsWeather.ts
+++ b/src/services/nwsWeather.ts
@@ -19,6 +19,7 @@ export interface NWSWeather extends Coordinates {
windSpeed: Property;
windGust: Property;
windDirection: Property;
+ temperature: Property;
/**
* The NWS office, like "MKX"
diff --git a/src/services/openMeteo.ts b/src/services/openMeteo.ts
index e103489..dcef612 100644
--- a/src/services/openMeteo.ts
+++ b/src/services/openMeteo.ts
@@ -8,7 +8,8 @@ import { notEmpty } from "../helpers/array";
import zipObject from "lodash/zipObject";
import * as velitherm from "velitherm";
-const FORECAST_DAYS = 2;
+const FORECAST_DAYS = 7;
+const FORECAST_DAYS_WINDS_ALOFT = 2;
/**
* in hPa
@@ -19,10 +20,10 @@ const PRESSURE_ALTITUDES = [
const PRESSURE_ALTITUDE_METRICS = [
"temperature",
- "windspeed",
- "winddirection",
+ "wind_speed",
+ "wind_direction",
"geopotential_height",
- "relativehumidity",
+ "relative_humidity",
] as const;
/**
@@ -30,31 +31,34 @@ const PRESSURE_ALTITUDE_METRICS = [
*/
const AGL_ALTITUDES = [80, 120, 180] as const;
-const AGL_METRICS = ["windspeed", "winddirection", "temperature"] as const;
+const AGL_METRICS = ["wind_speed", "wind_direction", "temperature"] as const;
const SPECIAL_ALOFT_VARIABLES = [
"cape",
+ "convective_inhibition",
"temperature_2m",
- "dewpoint_2m",
- "relativehumidity_2m",
+ "dew_point_2m",
+ "relative_humidity_2m",
"surface_pressure",
"pressure_msl",
// No temperature_10m, so have to break it out from AGL_ALTITUDES
// (will fudge it and use temperature_2m)
- "winddirection_10m",
- "windspeed_10m",
- "windgusts_10m",
+ "wind_direction_10m",
+ "wind_speed_10m",
+ "wind_gusts_10m",
] as const;
const WEATHER_VARIABLES = [
"precipitation_probability",
- "weathercode",
- "cloudcover",
+ "weather_code",
+ "cloud_cover",
+ "temperature",
- "windspeed_10m",
- "windgusts_10m",
+ "wind_speed_10m",
+ "wind_gusts_10m",
+ "wind_direction_10m",
] as const;
type HourlyPressureParams =
@@ -84,6 +88,8 @@ interface OpenMeteoWeatherHour {
/** kph */
windGust: number;
cloudCover: number;
+ windDirection: number;
+ temperature: number;
}
export async function getWeather(
@@ -110,15 +116,13 @@ export async function getWindsAloft(
longitude: number,
): Promise<{
windsAloft: WindsAloftReport;
- weather: OpenMeteoWeather;
elevationInM: number;
}> {
- const openMeteoResponse = await getOpenMeteoWindsAloft(latitude, longitude);
+ const aloft = await getOpenMeteoWindsAloft(latitude, longitude);
return {
- windsAloft: interpolate(convertOpenMeteoToWindsAloft(openMeteoResponse)),
- weather: convertOpenMeteoToWeather(openMeteoResponse),
- elevationInM: openMeteoResponse.elevation,
+ windsAloft: interpolate(convertOpenMeteoToWindsAloft(aloft)),
+ elevationInM: aloft.elevation,
};
}
@@ -131,10 +135,12 @@ function convertOpenMeteoToWeather(
openMeteoResponse.hourly.time.map((_, index) => ({
precipitationChance:
openMeteoResponse.hourly.precipitation_probability[index],
- weather: openMeteoResponse.hourly.weathercode[index],
- windSpeed: openMeteoResponse.hourly.windspeed_10m[index],
- windGust: openMeteoResponse.hourly.windgusts_10m[index],
- cloudCover: openMeteoResponse.hourly.cloudcover[index],
+ weather: openMeteoResponse.hourly.weather_code[index],
+ windSpeed: openMeteoResponse.hourly.wind_speed_10m[index],
+ windGust: openMeteoResponse.hourly.wind_gusts_10m[index],
+ windDirection: openMeteoResponse.hourly.wind_direction_10m[index],
+ cloudCover: openMeteoResponse.hourly.cloud_cover[index],
+ temperature: openMeteoResponse.hourly.temperature[index],
})),
),
};
@@ -205,12 +211,9 @@ async function getOpenMeteoWindsAloft(
params: {
latitude,
longitude,
- forecast_days: FORECAST_DAYS,
+ forecast_days: FORECAST_DAYS_WINDS_ALOFT,
timeformat: "unixtime",
- hourly: [
- ...generateWindsAloftParams(),
- ...generateWeatherParams(),
- ].join(","),
+ hourly: generateWindsAloftParams().join(","),
},
})
).data;
@@ -226,14 +229,16 @@ function convertOpenMeteoToWindsAloft(
index
],
windSpeedInKph:
- openMeteoResponse.hourly[`windspeed_${pressureAltitude}hPa`][index],
+ openMeteoResponse.hourly[`wind_speed_${pressureAltitude}hPa`][index],
windDirectionInDeg:
- openMeteoResponse.hourly[`winddirection_${pressureAltitude}hPa`][index],
+ openMeteoResponse.hourly[`wind_direction_${pressureAltitude}hPa`][
+ index
+ ],
temperatureInC:
openMeteoResponse.hourly[`temperature_${pressureAltitude}hPa`][index],
pressure: pressureAltitude,
dewpointInC: velitherm.dewPoint(
- openMeteoResponse.hourly[`relativehumidity_${pressureAltitude}hPa`][
+ openMeteoResponse.hourly[`relative_humidity_${pressureAltitude}hPa`][
index
],
openMeteoResponse.hourly[`temperature_${pressureAltitude}hPa`][index],
@@ -270,14 +275,15 @@ function convertOpenMeteoToWindsAloft(
return {
date: new Date(time * 1_000).toISOString(),
cape: openMeteoResponse.hourly.cape[index],
+ cin: openMeteoResponse.hourly.convective_inhibition[index],
altitudes: [
{
altitudeInM: openMeteoResponse.elevation,
- windSpeedInKph: openMeteoResponse.hourly.windspeed_10m[index],
+ windSpeedInKph: openMeteoResponse.hourly.wind_speed_10m[index],
windDirectionInDeg:
- openMeteoResponse.hourly.winddirection_10m[index],
+ openMeteoResponse.hourly.wind_direction_10m[index],
temperatureInC: openMeteoResponse.hourly.temperature_2m[index],
- dewpointInC: openMeteoResponse.hourly.dewpoint_2m[index],
+ dewpointInC: openMeteoResponse.hourly.dew_point_2m[index],
pressure: Math.round(
openMeteoResponse.hourly.surface_pressure[index],
),
@@ -287,9 +293,9 @@ function convertOpenMeteoToWindsAloft(
AGL_ALTITUDES.map((agl) => ({
altitudeInM: openMeteoResponse.elevation + agl,
windSpeedInKph:
- openMeteoResponse.hourly[`windspeed_${agl}m`][index],
+ openMeteoResponse.hourly[`wind_speed_${agl}m`][index],
windDirectionInDeg:
- openMeteoResponse.hourly[`winddirection_${agl}m`][index],
+ openMeteoResponse.hourly[`wind_direction_${agl}m`][index],
temperatureInC:
openMeteoResponse.hourly[`temperature_${agl}m`][index],
dewpointInC: velitherm.dewPoint(
@@ -297,7 +303,7 @@ function convertOpenMeteoToWindsAloft(
agl,
openMeteoResponse.hourly[`surface_pressure`][index],
openMeteoResponse.hourly[`temperature_${agl}m`][index],
- openMeteoResponse.hourly[`relativehumidity_2m`][index],
+ openMeteoResponse.hourly[`relative_humidity_2m`][index],
),
openMeteoResponse.hourly[`temperature_${agl}m`][index],
),
diff --git a/src/shared/Tooltip.tsx b/src/shared/Tooltip.tsx
index c3dbcfc..297c853 100644
--- a/src/shared/Tooltip.tsx
+++ b/src/shared/Tooltip.tsx
@@ -52,7 +52,7 @@ export const TooltipContainer = styled.div<{ interactive: boolean }>`
interface TooltipProps {
children?: React.ReactNode;
- contents: () => React.ReactNode;
+ contents: () => React.ReactNode | undefined;
mouseOnly?: boolean;
interactive?: boolean;
offset?: number;
@@ -110,6 +110,10 @@ export default function Tooltip({
});
}, [isMounted]);
+ const renderedContent = contents();
+
+ if (!renderedContent) return children;
+
return (
<>
to determine if tooltip is open */
interactive={interactive ?? false}
>
- {contents()}
+ {renderedContent}