import { toNumber } from "@/lib/weather-utils"; import type { ImgwCurrentWeather, RawImgwHybridWeatherResponse, RawImgwHybridWeatherRow } from "@/types/imgw-current"; const STALE_TEN_MINUTE_THRESHOLD_MS = 2 * 60 * 60 * 1000; interface NormalizedHybridRow { row: RawImgwHybridWeatherRow; measuredAt: string; timestamp: number; } function toCelsius(value: unknown) { const temperature = toNumber(value); if (temperature === null) return null; return temperature > 150 ? temperature - 273.15 : temperature; } function toHectopascals(value: unknown) { const pressure = toNumber(value); if (pressure === null) return null; return pressure > 2_000 ? pressure / 100 : pressure; } function normalizeDate(value: unknown) { if (typeof value !== "string") return null; const date = new Date(value); return Number.isNaN(date.getTime()) ? null : date.toISOString(); } function normalizeHybridRow(candidate: RawImgwHybridWeatherRow): NormalizedHybridRow | null { const measuredAt = normalizeDate(candidate.Date); if (!measuredAt) return null; const timestamp = new Date(measuredAt).getTime(); return Number.isNaN(timestamp) ? null : { row: candidate, measuredAt, timestamp }; } function getWeatherCode(iconCode: unknown) { if (typeof iconCode !== "string") return null; const match = iconCode.match(/z(\d{2})/i); return match ? Number(match[1]) : null; } function getCondition(weatherCode: number | null, rainfall10m: number | null, snowfall10m: number | null) { if (weatherCode !== null && weatherCode >= 95) return "thunderstorm" as const; if ((snowfall10m ?? 0) > 0) return "snow" as const; if ((rainfall10m ?? 0) > 0) return "rain" as const; return null; } function hasNumericValue(value: unknown) { return toNumber(value) !== null; } function isFullWeatherRow(candidate: RawImgwHybridWeatherRow) { return hasNumericValue(candidate.Temperature) && hasNumericValue(candidate.Chill) && hasNumericValue(candidate.Humidity) && hasNumericValue(candidate.Wind_Speed) && hasNumericValue(candidate.PressureMSL); } function hasPrecipitationValue(candidate: RawImgwHybridWeatherRow) { return hasNumericValue(candidate.Precipitation10m) || hasNumericValue(candidate.Rain10m) || hasNumericValue(candidate.Snow10m); } function pickFullWeatherRow(rows: NormalizedHybridRow[]) { const tenMinuteRow = rows.find((candidate) => candidate.row.Type === "Type_Ten_Minutes" && isFullWeatherRow(candidate.row)); const hourlyRow = rows.find((candidate) => candidate.row.Type === "Type_Hour" && isFullWeatherRow(candidate.row)); if (!tenMinuteRow) return hourlyRow ?? null; if (!hourlyRow) return tenMinuteRow; return hourlyRow.timestamp - tenMinuteRow.timestamp > STALE_TEN_MINUTE_THRESHOLD_MS ? hourlyRow : tenMinuteRow; } function pickPrecipitationRow(rows: NormalizedHybridRow[], fullRow: NormalizedHybridRow | null) { if (fullRow && hasPrecipitationValue(fullRow.row)) return fullRow; const precipitationRows = rows.filter((candidate) => candidate.row.Type === "Type_Ten_Minutes" && hasPrecipitationValue(candidate.row)); if (!precipitationRows.length) return null; if (!fullRow) return precipitationRows[0]; return precipitationRows.reduce((best, candidate) => ( Math.abs(candidate.timestamp - fullRow.timestamp) < Math.abs(best.timestamp - fullRow.timestamp) ? candidate : best )); } export function normalizeImgwCurrentWeather(payload: RawImgwHybridWeatherResponse): ImgwCurrentWeather | null { if (!payload.data?.Valid || !Array.isArray(payload.data.Data)) return null; const rows = payload.data.Data .flatMap((candidate): NormalizedHybridRow[] => { if (!candidate || typeof candidate !== "object") return []; const row = candidate as RawImgwHybridWeatherRow; if (row.Type !== "Type_Ten_Minutes" && row.Type !== "Type_Hour") return []; const normalizedRow = normalizeHybridRow(row); return normalizedRow ? [normalizedRow] : []; }); const fullRow = pickFullWeatherRow(rows); const precipitationRow = pickPrecipitationRow(rows, fullRow); const row = fullRow ?? precipitationRow; if (!row) return null; const precipitationSource = precipitationRow?.row ?? row.row; const rainfall10m = toNumber(precipitationSource.Rain10m); const snowfall10m = toNumber(precipitationSource.Snow10m); const weatherCode = getWeatherCode(row.row.Icon10); return { coverage: fullRow?.row.Type === "Type_Ten_Minutes" ? "full" : fullRow ? "hourly" : "precipitation-only", measuredAt: row.measuredAt, temperature: toCelsius(row.row.Temperature), feelsLike: toCelsius(row.row.Chill), windSpeed: toNumber(row.row.Wind_Speed), windDirection: toNumber(row.row.Wind_Dir), humidity: toNumber(row.row.Humidity), pressure: toHectopascals(row.row.PressureMSL), precipitation10m: toNumber(precipitationSource.Precipitation10m), rainfall10m, snowfall10m, cloudCover: toNumber(row.row.Cloud), weatherCode, condition: getCondition(weatherCode, rainfall10m, snowfall10m), }; } export async function fetchImgwCurrentWeather(latitude: number, longitude: number, signal?: AbortSignal) { const params = new URLSearchParams({ latitude: String(latitude), longitude: String(longitude) }); const response = await fetch(`/api/imgw-current?${params}`, { signal }); if (!response.ok) throw new Error("Nie udało się pobrać bieżącej analizy IMGW Hybrid."); return normalizeImgwCurrentWeather(await response.json() as RawImgwHybridWeatherResponse); }