Files
wtr/lib/weather-utils.ts

187 lines
7.4 KiB
TypeScript

import type {
HydroStation,
RawHydroStation,
RawSynopStation,
RawWarning,
SynopStation,
WeatherMood,
WeatherWarning,
WarningKind,
} from "@/types/imgw";
import { translate, type Language } from "@/lib/i18n";
const locales: Record<Language, string> = { pl: "pl-PL", en: "en-GB" };
export function toNumber(value: unknown): number | null {
if (typeof value === "number") return Number.isFinite(value) ? value : null;
if (typeof value !== "string" || value.trim() === "") return null;
const parsed = Number(value.replace(",", "."));
return Number.isFinite(parsed) ? parsed : null;
}
function normalizeDate(value?: string | null): string | null {
if (!value?.trim()) return null;
const isoCandidate = value.includes("T") ? value : value.replace(" ", "T");
const date = new Date(isoCandidate);
return Number.isNaN(date.getTime()) ? null : date.toISOString();
}
function synopMeasuredAt(date?: string | null, hour?: string | null) {
if (!date?.trim() || !hour?.trim()) return null;
return normalizeDate(`${date}T${hour.padStart(2, "0")}:00:00Z`);
}
export function normalizeSynopStation(raw: RawSynopStation): SynopStation | null {
if (!raw.id_stacji?.trim() || !raw.stacja?.trim()) return null;
return {
id: raw.id_stacji,
name: raw.stacja,
measuredAt: synopMeasuredAt(raw.data_pomiaru, raw.godzina_pomiaru),
temperature: toNumber(raw.temperatura),
windSpeed: toNumber(raw.predkosc_wiatru),
windDirection: toNumber(raw.kierunek_wiatru),
humidity: toNumber(raw.wilgotnosc_wzgledna),
rainfall: toNumber(raw.suma_opadu),
pressure: toNumber(raw.cisnienie),
};
}
export function normalizeHydroStation(raw: RawHydroStation): HydroStation | null {
if (!raw.id_stacji?.trim() || !raw.stacja?.trim()) return null;
return {
id: raw.id_stacji,
name: raw.stacja,
river: raw.rzeka?.trim() || null,
province: raw.wojewodztwo?.trim() || null,
longitude: toNumber(raw.lon),
latitude: toNumber(raw.lat),
waterLevel: toNumber(raw.stan_wody),
waterLevelMeasuredAt: normalizeDate(raw.stan_wody_data_pomiaru),
waterTemperature: toNumber(raw.temperatura_wody),
waterTemperatureMeasuredAt: normalizeDate(raw.temperatura_wody_data_pomiaru),
flow: toNumber(raw.przeplyw),
flowMeasuredAt: normalizeDate(raw.przeplyw_data),
icePhenomenon: toNumber(raw.zjawisko_lodowe),
overgrowthPhenomenon: toNumber(raw.zjawisko_zarastania),
};
}
export function normalizeWarning(raw: RawWarning, kind: WarningKind, index: number): WeatherWarning {
const describedAreas = (raw.obszary ?? [])
.map((area) => area.opis?.trim() || area.wojewodztwo?.trim())
.filter((area): area is string => Boolean(area));
const areas = describedAreas.length ? describedAreas : (raw.teryt ?? []).map((code) => `TERYT ${code}`);
const title = raw.zdarzenie?.trim() || raw.nazwa_zdarzenia?.trim() || "";
return {
id: `${kind}-${raw.id ?? raw.numer ?? index}-${raw.data_od ?? raw.obowiazuje_od ?? "unknown"}`,
kind,
level: toNumber(raw["stopień"] ?? raw.stopien),
title,
description: raw.przebieg?.trim() || raw.tresc?.trim() || null,
comment: raw.komentarz?.trim() || null,
validFrom: normalizeDate(raw.data_od ?? raw.obowiazuje_od),
validTo: raw.data_do?.startsWith("9999-") ? null : normalizeDate(raw.data_do ?? raw.obowiazuje_do),
publishedAt: normalizeDate(raw.opublikowano),
probability: toNumber(raw.prawdopodobienstwo),
areas,
office: raw.biuro?.trim() || null,
};
}
export function formatTemperature(value: number | null, language: Language = "pl") {
return value === null ? translate(language, "common.noData") : `${Math.round(value)}°`;
}
export function formatPressure(value: number | null, language: Language = "pl") {
return value === null ? translate(language, "common.noData") : `${value.toFixed(1)} hPa`;
}
export function formatHumidity(value: number | null, language: Language = "pl") {
return value === null ? translate(language, "common.noData") : `${Math.round(value)}%`;
}
export function formatWind(speed: number | null, direction?: number | null, language: Language = "pl") {
if (speed === null) return translate(language, "common.noData");
const directionLabel = direction === null || direction === undefined ? "" : ` ${getWindDirection(direction)}`;
return `${speed.toFixed(1)} m/s${directionLabel}`;
}
export function formatRainfall(value: number | null, language: Language = "pl") {
return value === null ? translate(language, "common.noData") : `${value.toFixed(value < 1 ? 2 : 1)} mm`;
}
export function formatWaterLevel(value: number | null, language: Language = "pl") {
return value === null ? translate(language, "common.noData") : `${Math.round(value)} cm`;
}
export function formatFlow(value: number | null, language: Language = "pl") {
return value === null ? translate(language, "common.noData") : `${value.toFixed(2)} m³/s`;
}
export function formatDateTime(value: string | null, language: Language = "pl", fallback = translate(language, "common.noData")) {
if (!value) return fallback;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return fallback;
return new Intl.DateTimeFormat(locales[language], {
day: "numeric",
month: "long",
hour: "2-digit",
minute: "2-digit",
}).format(date);
}
export function calculateFeelsLike(temperature: number | null, humidity: number | null, windSpeed: number | null) {
if (temperature === null) return null;
if (temperature <= 10 && windSpeed !== null && windSpeed > 1.34) {
const windKmh = windSpeed * 3.6;
return 13.12 + 0.6215 * temperature - 11.37 * windKmh ** 0.16 + 0.3965 * temperature * windKmh ** 0.16;
}
if (temperature >= 27 && humidity !== null) {
const c1 = -8.78469475556;
const c2 = 1.61139411;
const c3 = 2.33854883889;
const c4 = -0.14611605;
const c5 = -0.012308094;
const c6 = -0.0164248277778;
const c7 = 0.002211732;
const c8 = 0.00072546;
const c9 = -0.000003582;
return c1 + c2 * temperature + c3 * humidity + c4 * temperature * humidity + c5 * temperature ** 2 +
c6 * humidity ** 2 + c7 * temperature ** 2 * humidity + c8 * temperature * humidity ** 2 +
c9 * temperature ** 2 * humidity ** 2;
}
return temperature;
}
export function getWindDirection(degrees: number) {
const labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
return labels[Math.round((((degrees % 360) + 360) % 360) / 45) % 8];
}
export function getWeatherMoodFromData(station: SynopStation, date = new Date()): WeatherMood {
const hour = date.getHours();
if (hour < 6 || hour >= 21) return "night";
if ((station.windSpeed ?? 0) >= 8) return "wind";
if ((station.temperature ?? 15) <= 3) return "cold";
if ((station.humidity ?? 0) >= 80) return "cloudy";
if ((station.temperature ?? 15) >= 20) return "warm";
return "mild";
}
export function getWeatherDescription(station: SynopStation, language: Language = "pl") {
if ((station.windSpeed ?? 0) >= 8) return translate(language, "weather.strongWind");
if ((station.humidity ?? 0) >= 90) return translate(language, "weather.humid");
return translate(language, "weather.calm");
}
export function moodGradient(mood: WeatherMood) {
return {
warm: "from-sky-400 via-blue-500 to-indigo-700",
cloudy: "from-slate-600 via-slate-700 to-slate-900",
wind: "from-cyan-600 via-slate-600 to-blue-950",
cold: "from-cyan-300 via-blue-500 to-indigo-900",
night: "from-slate-800 via-indigo-950 to-slate-950",
mild: "from-sky-500 via-cyan-700 to-blue-900",
}[mood];
}