95 lines
3.7 KiB
TypeScript
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>);
|
|
}
|