Files
wtr/lib/forecast-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

95 lines
3.7 KiB
TypeScript

import type { DailyForecast, ForecastSource, HourlyForecast, WeatherForecast } from "@/types/forecast";
import type { WeatherRegion } from "@/types/weather-region";
import { getWeatherCapabilities } from "@/types/weather-region";
function readNumber(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function readString(value: unknown) {
return typeof value === "string" && value ? value : null;
}
function isForecastSource(value: unknown): value is ForecastSource {
return value === "imgw-alaro" || value === "open-meteo";
}
function normalizeSources(value: unknown): ForecastSource[] {
return Array.isArray(value) ? value.filter(isForecastSource) : [];
}
function normalizeHourlyForecast(value: unknown): HourlyForecast[] {
return Array.isArray(value)
? value.flatMap((candidate) => {
if (!candidate || typeof candidate !== "object") return [];
const row = candidate as Partial<HourlyForecast>;
const time = readString(row.time);
if (!time) return [];
return [
{
time,
temperature: readNumber(row.temperature),
feelsLike: readNumber(row.feelsLike),
precipitationProbability: readNumber(row.precipitationProbability),
precipitation: readNumber(row.precipitation),
weatherCode: readNumber(row.weatherCode),
windSpeed: readNumber(row.windSpeed),
source: isForecastSource(row.source) ? row.source : "open-meteo",
},
];
})
: [];
}
function normalizeDailyForecast(value: unknown): DailyForecast[] {
return Array.isArray(value)
? value.flatMap((candidate) => {
if (!candidate || typeof candidate !== "object") return [];
const row = candidate as Partial<DailyForecast>;
const date = readString(row.date);
if (!date) return [];
return [
{
date,
temperatureMax: readNumber(row.temperatureMax),
temperatureMin: readNumber(row.temperatureMin),
precipitationProbability: readNumber(row.precipitationProbability),
precipitation: readNumber(row.precipitation),
weatherCode: readNumber(row.weatherCode),
sunrise: readString(row.sunrise),
sunset: readString(row.sunset),
sources: normalizeSources(row.sources),
},
];
})
: [];
}
function normalizeForecast(raw: Partial<WeatherForecast>): WeatherForecast {
const latitude = Number(raw.latitude);
const longitude = Number(raw.longitude);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude))
throw new Error("Forecast service returned invalid coordinates.");
const region: WeatherRegion = raw.region === "GLOBAL" ? "GLOBAL" : "PL";
return {
latitude,
longitude,
region,
timezone: typeof raw.timezone === "string" ? raw.timezone : "Europe/Warsaw",
hourly: normalizeHourlyForecast(raw.hourly),
daily: normalizeDailyForecast(raw.daily),
sources: normalizeSources(raw.sources),
capabilities: raw.capabilities ?? getWeatherCapabilities(region),
unavailableReasons: Array.isArray(raw.unavailableReasons)
? raw.unavailableReasons.filter((value): value is string => typeof value === "string")
: [],
};
}
export async function fetchForecast(latitude: number, longitude: number, region: WeatherRegion, signal?: AbortSignal) {
const params = new URLSearchParams({ latitude: String(latitude), longitude: String(longitude), region });
const response = await fetch(`/api/forecast?${params}`, { signal });
if (!response.ok) throw new Error("Unable to load forecast.");
return normalizeForecast((await response.json()) as Partial<WeatherForecast>);
}