import type { HydroStation, RawHydroStation, RawSynopStation, RawWarning, SynopStation, WeatherMood, WeatherWarning, WarningKind, } from "@/types/imgw"; import { translate, type Language } from "@/lib/i18n"; import { getProvinceFromTeryt, normalizeProvinceName } from "@/lib/provinces"; import type { CurrentWeatherCondition } from "@/types/imgw-current"; import type { DistanceUnit, PrecipitationUnit, PressureUnit, TemperatureUnit, WindSpeedUnit } from "@/types/units"; const locales: Record = { pl: "pl-PL", en: "en-GB" }; export const DEFAULT_TEMPERATURE_UNIT: TemperatureUnit = "c"; export const TEMPERATURE_UNITS: TemperatureUnit[] = ["c", "f"]; export const DEFAULT_WIND_SPEED_UNIT: WindSpeedUnit = "kmh"; export const WIND_SPEED_UNITS: WindSpeedUnit[] = ["ms", "kmh", "mph", "kt", "bft"]; export const DEFAULT_PRECIPITATION_UNIT: PrecipitationUnit = "mm"; export const PRECIPITATION_UNITS: PrecipitationUnit[] = ["mm", "cm", "in"]; export const DEFAULT_PRESSURE_UNIT: PressureUnit = "hpa"; export const PRESSURE_UNITS: PressureUnit[] = ["hpa", "kpa", "inhg", "mmhg"]; export const DEFAULT_DISTANCE_UNIT: DistanceUnit = "km"; export const DISTANCE_UNITS: DistanceUnit[] = ["km", "mi"]; const temperatureUnitLabels: Record = { c: "°C", f: "°F", }; const windSpeedUnitLabels: Record = { ms: "m/s", kmh: "km/h", mph: "mph", kt: "kt", bft: "Bft", }; const precipitationUnitLabels: Record = { mm: "mm", cm: "cm", in: "in", }; const pressureUnitLabels: Record = { hpa: "hPa", kpa: "kPa", inhg: "inHg", mmhg: "mm Hg", }; const distanceUnitLabels: Record = { km: "km", mi: "mi", }; 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 provinces = [ ...new Set( [ ...(raw.obszary ?? []).map((area) => normalizeProvinceName(area.wojewodztwo)), ...(raw.teryt ?? []).map(getProvinceFromTeryt), ].filter((province) => province !== null), ), ]; const describedAreas = (raw.obszary ?? []) .map((area) => area.opis?.trim() || area.wojewodztwo?.trim()) .filter((area): area is string => Boolean(area)); const areas = describedAreas.length ? describedAreas : provinces; 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, terytCodes: Array.isArray(raw.teryt) ? raw.teryt.filter((code): code is string => typeof code === "string" && code.trim().length > 0) : [], provinces, office: raw.biuro?.trim() || null, }; } export function isTemperatureUnit(value: unknown): value is TemperatureUnit { return typeof value === "string" && TEMPERATURE_UNITS.includes(value as TemperatureUnit); } export function getTemperatureUnitLabel(unit: TemperatureUnit) { return temperatureUnitLabels[unit]; } export function convertTemperature(value: number, unit: TemperatureUnit) { if (unit === "f") return (value * 9) / 5 + 32; return value; } export function formatTemperatureValue( value: number, language: Language = "pl", unit: TemperatureUnit = DEFAULT_TEMPERATURE_UNIT, ) { const formattedValue = new Intl.NumberFormat(locales[language], { maximumFractionDigits: 0, }).format(value); return `${formattedValue}${getTemperatureUnitLabel(unit)}`; } export function formatTemperature( value: number | null, language: Language = "pl", unit: TemperatureUnit = DEFAULT_TEMPERATURE_UNIT, ) { return value === null ? translate(language, "common.noData") : formatTemperatureValue(convertTemperature(value, unit), language, unit); } export function formatHumidity(value: number | null, language: Language = "pl") { return value === null ? translate(language, "common.noData") : `${Math.round(value)}%`; } export function isWindSpeedUnit(value: unknown): value is WindSpeedUnit { return typeof value === "string" && WIND_SPEED_UNITS.includes(value as WindSpeedUnit); } export function getWindSpeedUnitLabel(unit: WindSpeedUnit) { return windSpeedUnitLabels[unit]; } export function isPrecipitationUnit(value: unknown): value is PrecipitationUnit { return typeof value === "string" && PRECIPITATION_UNITS.includes(value as PrecipitationUnit); } export function getPrecipitationUnitLabel(unit: PrecipitationUnit) { return precipitationUnitLabels[unit]; } export function isPressureUnit(value: unknown): value is PressureUnit { return typeof value === "string" && PRESSURE_UNITS.includes(value as PressureUnit); } export function getPressureUnitLabel(unit: PressureUnit) { return pressureUnitLabels[unit]; } export function isDistanceUnit(value: unknown): value is DistanceUnit { return typeof value === "string" && DISTANCE_UNITS.includes(value as DistanceUnit); } export function getDistanceUnitLabel(unit: DistanceUnit) { return distanceUnitLabels[unit]; } export function convertWindSpeed(speed: number, unit: WindSpeedUnit) { if (unit === "kmh") return speed * 3.6; if (unit === "mph") return speed * 2.2369362921; if (unit === "kt") return speed * 1.9438444924; return speed; } export function convertWindSpeedToBeaufort(speed: number) { if (speed < 0.3) return 0; if (speed < 1.6) return 1; if (speed < 3.4) return 2; if (speed < 5.5) return 3; if (speed < 8) return 4; if (speed < 10.8) return 5; if (speed < 13.9) return 6; if (speed < 17.2) return 7; if (speed < 20.8) return 8; if (speed < 24.5) return 9; if (speed < 28.5) return 10; if (speed < 32.7) return 11; return 12; } export function formatWindSpeed( speed: number | null, language: Language = "pl", unit: WindSpeedUnit = DEFAULT_WIND_SPEED_UNIT, ) { if (speed === null) return translate(language, "common.noData"); if (unit === "bft") return `${convertWindSpeedToBeaufort(speed)} ${getWindSpeedUnitLabel(unit)}`; const convertedSpeed = convertWindSpeed(speed, unit); const digits = unit === "ms" ? 1 : 0; const formattedSpeed = new Intl.NumberFormat(locales[language], { minimumFractionDigits: digits, maximumFractionDigits: digits, }).format(convertedSpeed); return `${formattedSpeed} ${getWindSpeedUnitLabel(unit)}`; } export function formatWind( speed: number | null, direction?: number | null, language: Language = "pl", unit: WindSpeedUnit = DEFAULT_WIND_SPEED_UNIT, ) { if (speed === null) return translate(language, "common.noData"); const directionLabel = direction === null || direction === undefined ? "" : ` ${getWindDirection(direction)}`; return `${formatWindSpeed(speed, language, unit)}${directionLabel}`; } export function formatRainfall( value: number | null, language: Language = "pl", unit: PrecipitationUnit = DEFAULT_PRECIPITATION_UNIT, ) { return formatPrecipitation(value, language, unit); } export function convertPrecipitation(value: number, unit: PrecipitationUnit) { if (unit === "cm") return value / 10; if (unit === "in") return value / 25.4; return value; } export function formatPrecipitation( value: number | null, language: Language = "pl", unit: PrecipitationUnit = DEFAULT_PRECIPITATION_UNIT, ) { if (value === null) return translate(language, "common.noData"); const convertedValue = convertPrecipitation(value, unit); const digits = unit === "mm" ? (convertedValue < 1 ? 2 : 1) : unit === "cm" ? 2 : 2; const formattedValue = new Intl.NumberFormat(locales[language], { minimumFractionDigits: unit === "in" ? 2 : 0, maximumFractionDigits: digits, }).format(convertedValue); return `${formattedValue} ${getPrecipitationUnitLabel(unit)}`; } export function convertPressure(value: number, unit: PressureUnit) { if (unit === "kpa") return value / 10; if (unit === "inhg") return value * 0.0295299830714; if (unit === "mmhg") return value * 0.750061683; return value; } export function formatPressure( value: number | null, language: Language = "pl", unit: PressureUnit = DEFAULT_PRESSURE_UNIT, ) { if (value === null) return translate(language, "common.noData"); const convertedValue = convertPressure(value, unit); const digits = unit === "inhg" ? 2 : unit === "kpa" ? 1 : unit === "mmhg" ? 0 : 1; const formattedValue = new Intl.NumberFormat(locales[language], { minimumFractionDigits: unit === "inhg" ? 2 : 0, maximumFractionDigits: digits, }).format(convertedValue); return `${formattedValue} ${getPressureUnitLabel(unit)}`; } export function convertDistance(value: number, unit: DistanceUnit) { if (unit === "mi") return value * 0.621371; return value; } export function formatDistance( value: number | null, language: Language = "pl", unit: DistanceUnit = DEFAULT_DISTANCE_UNIT, ) { if (value === null) return translate(language, "common.noData"); const convertedValue = convertDistance(value, unit); const formattedValue = new Intl.NumberFormat(locales[language], { maximumFractionDigits: convertedValue < 10 ? 1 : 0, }).format(convertedValue); return `${formattedValue} ${getDistanceUnitLabel(unit)}`; } 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"; } function getForecastConditionDescription(code: number | null, language: Language) { if (code === 0) return translate(language, "forecast.condition.clear"); if (code === 1 || code === 2) return translate(language, "forecast.condition.partlyCloudy"); if (code === 3) return translate(language, "forecast.condition.cloudy"); if (code === 45 || code === 48) return translate(language, "forecast.condition.fog"); if (code !== null && code >= 51 && code <= 57) return translate(language, "forecast.condition.drizzle"); if (code !== null && ((code >= 61 && code <= 67) || (code >= 80 && code <= 82))) return translate(language, "forecast.condition.rain"); if (code !== null && ((code >= 71 && code <= 77) || code === 85 || code === 86)) return translate(language, "forecast.condition.snow"); if (code !== null && code >= 95) return translate(language, "forecast.condition.thunderstorm"); return null; } function getCloudCoverDescription(cloudCover: number | null | undefined, language: Language) { if (cloudCover === null || cloudCover === undefined) return null; if (cloudCover >= 75) return translate(language, "forecast.condition.cloudy"); if (cloudCover >= 25) return translate(language, "forecast.condition.partlyCloudy"); return null; } export function getWeatherDescription( station: SynopStation, language: Language = "pl", condition?: CurrentWeatherCondition, currentWeatherCode?: number | null, currentCloudCover?: number | null, forecastWeatherCode?: number | null, ) { if (condition === "thunderstorm") return translate(language, "weather.thunderstorm"); if (condition === "snow") return translate(language, "weather.currentSnow"); if (condition === "rain") return translate(language, "weather.currentRain"); if ((station.windSpeed ?? 0) >= 8) return translate(language, "weather.strongWind"); const currentDescription = getForecastConditionDescription(currentWeatherCode ?? null, language); const cloudCoverDescription = getCloudCoverDescription(currentCloudCover, language); if ( cloudCoverDescription && (currentWeatherCode === null || currentWeatherCode === undefined || currentWeatherCode === 0) ) return cloudCoverDescription; if (currentDescription) return currentDescription; if (cloudCoverDescription) return cloudCoverDescription; const forecastDescription = getForecastConditionDescription(forecastWeatherCode ?? null, language); if (forecastDescription) return forecastDescription; if ((station.humidity ?? 0) >= 90) return translate(language, "weather.humid"); return translate(language, "weather.calm"); }