This commit is contained in:
Vincent van der Wal
2026-08-01 17:58:03 +02:00
parent b2c8108c8c
commit 01cc6225aa
430 changed files with 3150 additions and 441 deletions
+85 -10
View File
@@ -14,6 +14,7 @@ import { Unit } from '@openmeteo/sdk/unit';
import { fetchWeatherApi } from 'openmeteo';
import { type DaylightBand, buildDaylightBands } from '$lib/charts/bands';
import * as m from '$lib/paraglide/messages';
import type { VariableWithValues } from '@openmeteo/sdk/variable-with-values';
import type { VariablesWithTime } from '@openmeteo/sdk/variables-with-time';
@@ -313,8 +314,8 @@ export function humanizeWeatherError(err: unknown): FriendlyWeatherError {
msg.includes('network request failed')
) {
return {
title: "Couldn't reach the weather service",
hint: 'Check your internet connection and try again.',
title: m.err_network_title(),
hint: m.err_network_hint(),
detail: raw
};
}
@@ -325,21 +326,21 @@ export function humanizeWeatherError(err: unknown): FriendlyWeatherError {
msg.includes('coordinates')
) {
return {
title: 'No data for this location with the selected model',
hint: 'Regional weather models only cover their own area — "Best match" picks a suitable model automatically.',
title: m.err_nodata_title(),
hint: m.err_nodata_hint(),
detail: raw
};
}
if (msg.includes('invalid') || msg.includes('cannot be') || msg.includes('bad request')) {
return {
title: 'The weather service rejected the request',
hint: 'Try different settings, or switch the model back to "Best match".',
title: m.err_rejected_title(),
hint: m.err_rejected_hint(),
detail: raw
};
}
return {
title: 'Loading the weather data failed',
hint: 'Try again in a moment. If it keeps happening, switch the model to "Best match".',
title: m.err_generic_title(),
hint: m.err_generic_hint(),
detail: raw
};
}
@@ -936,6 +937,8 @@ export interface HistoricalForecastParams extends WeatherLocation, WeatherUnitPa
end_date: string;
/** Hourly API variables to request; defaults to the core week set. */
hourlyVariables?: string[];
/** Reanalysis to read from; omitted lets the API pick. */
model?: string;
}
export interface HistoricalForecastResult {
@@ -995,7 +998,8 @@ export async function fetchHistoricalWeather(
temperature_unit: params.temperature_unit ?? 'celsius',
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
precipitation_unit: params.precipitation_unit ?? 'mm',
timezone: params.timezone
timezone: params.timezone,
models: params.model && params.model !== 'best_match' ? params.model : undefined
};
const cleanParams: Record<string, string> = {};
@@ -1201,6 +1205,8 @@ export interface SeasonalForecastParams extends WeatherLocation, WeatherUnitPara
dailyVariables?: string[];
/** Lead time in days; the API allows at most 216. */
forecast_days?: number;
/** Seasonal model; omitted lets the API pick. */
model?: string;
}
export interface SeasonalForecastResult {
@@ -1273,7 +1279,8 @@ export async function fetchSeasonalForecast(
temperature_unit: params.temperature_unit ?? 'celsius',
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
precipitation_unit: params.precipitation_unit ?? 'mm',
timezone: params.timezone
timezone: params.timezone,
models: params.model && params.model !== 'best_match' ? params.model : undefined
};
const cleanParams: Record<string, string> = {};
@@ -1372,3 +1379,71 @@ export async function fetchSeasonalForecast(
timezone
};
}
// ─── Nearby cities snapshot ─────────────────────────────────────────────────────
export interface NearbyDaily {
/** local calendar date ("yyyy-MM-dd") -> that day's summary for this city */
byDate: Record<string, { weatherCode: number; max: number; min: number; precipitation: number }>;
}
export interface NearbySnapshotParams extends WeatherUnitParams {
points: { latitude: number; longitude: number }[];
past_days?: number;
forecast_days?: number;
}
/**
* Fetches a daily summary for several locations in one request - the forecast
* API takes comma-separated coordinates and answers with one response per
* point, in order.
*
* Deliberately runs on best_match: the nearby list can reach well past the
* domain of whatever regional model the page is showing, and a row of dashes
* is worse than a row from a model that covers everywhere.
*/
export async function fetchNearbyDaily(
params: NearbySnapshotParams
): Promise<(NearbyDaily | null)[]> {
if (params.points.length === 0) return [];
const apiParams: Record<string, string> = {
latitude: params.points.map((p) => p.latitude).join(','),
longitude: params.points.map((p) => p.longitude).join(','),
daily: 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum',
temperature_unit: params.temperature_unit ?? 'celsius',
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
precipitation_unit: params.precipitation_unit ?? 'mm',
past_days: String(params.past_days ?? 3),
forecast_days: String(params.forecast_days ?? 16),
timezone: 'auto'
};
const responses = await fetchWeatherApi(FORECAST_URL, apiParams);
return params.points.map((_, i) => {
const response = responses[i];
const dailyBlock = response?.daily();
if (!dailyBlock) return null;
const utcOffsetSeconds = response.utcOffsetSeconds();
const codes = getValues(dailyBlock.variables(0)!);
const max = getValues(dailyBlock.variables(1)!);
const min = getValues(dailyBlock.variables(2)!);
const precip = getValues(dailyBlock.variables(3)!);
// Same convention as the seasonal fetch: shift by the response's own
// offset, then read the calendar date off the ISO string.
const byDate: NearbyDaily['byDate'] = {};
getTimestamps(dailyBlock).forEach((t, d) => {
const key = new Date(t + utcOffsetSeconds * 1000).toISOString().slice(0, 10);
byDate[key] = {
weatherCode: codes[d],
max: max[d],
min: min[d],
precipitation: precip[d]
};
});
return { byDate };
});
}