Files
wtr/lib/imgw-current-api.ts
zv ee55521803
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s
chore: add prettier formatting
2026-06-14 20:26:56 +02:00

157 lines
6.1 KiB
TypeScript

import { toNumber } from "@/lib/weather-utils";
import type { ImgwCurrentWeather, RawImgwHybridWeatherResponse, RawImgwHybridWeatherRow } from "@/types/imgw-current";
import type { WeatherRegion } from "@/types/weather-region";
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",
region: "PL",
source: "IMGW",
sourceLabel: "IMGW Hybrid",
sourceType: fullRow ? "hybrid" : "model",
isOfficial: true,
fetchedAt: new Date().toISOString(),
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);
}
export async function fetchCurrentWeather(
latitude: number,
longitude: number,
region: WeatherRegion,
signal?: AbortSignal,
) {
const params = new URLSearchParams({ latitude: String(latitude), longitude: String(longitude), region });
const response = await fetch(`/api/current-weather?${params}`, { signal });
if (!response.ok) throw new Error("Nie udało się pobrać bieżących warunków pogodowych.");
return (await response.json()) as ImgwCurrentWeather | null;
}