761 lines
23 KiB
TypeScript
761 lines
23 KiB
TypeScript
/**
|
|
* Weather Data Service
|
|
*
|
|
* Centralized, type-safe weather data fetching using the Open-Meteo SDK
|
|
* with protobuf (FlatBuffers) transport for efficient data transfer.
|
|
*
|
|
* All weather data fetching flows through this service, providing:
|
|
* - Type-safe request parameters and response structures
|
|
* - Automatic retries with exponential backoff (via the SDK)
|
|
* - Efficient binary protobuf transport instead of JSON
|
|
* - Consistent timestamp and unit handling
|
|
*/
|
|
import { Unit } from '@openmeteo/sdk/unit';
|
|
import { fetchWeatherApi } from 'openmeteo';
|
|
|
|
import { buildDaylightMarkAreas } from '$lib/utils/echarts';
|
|
|
|
import type { VariableWithValues } from '@openmeteo/sdk/variable-with-values';
|
|
import type { VariablesWithTime } from '@openmeteo/sdk/variables-with-time';
|
|
|
|
// ─── Constants ──────────────────────────────────────────────────────────────────
|
|
|
|
const FORECAST_URL = 'https://api.open-meteo.com/v1/forecast';
|
|
const ENSEMBLE_URL = 'https://ensemble-api.open-meteo.com/v1/ensemble';
|
|
|
|
// ─── Core Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Generates an array of numbers from start (inclusive) to stop (exclusive) with the given step.
|
|
* Used to reconstruct timestamp arrays from the protobuf time/timeEnd/interval fields.
|
|
*/
|
|
export function range(start: number, stop: number, step: number): number[] {
|
|
return Array.from(
|
|
{ length: Math.max(0, Math.ceil((stop - start) / step)) },
|
|
(_, i) => start + i * step
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Extracts timestamp array (in milliseconds, with UTC offset applied) from a VariablesWithTime block.
|
|
*/
|
|
export function getTimestamps(timeBlock: VariablesWithTime): number[] {
|
|
const start = Number(timeBlock.time());
|
|
const end = Number(timeBlock.timeEnd());
|
|
const interval = timeBlock.interval();
|
|
return range(start, end, interval).map((t) => t * 1000);
|
|
}
|
|
|
|
/**
|
|
* Extracts Date array (with UTC offset applied) from a VariablesWithTime block.
|
|
*/
|
|
export function getDates(timeBlock: VariablesWithTime): Date[] {
|
|
return getTimestamps(timeBlock).map((t) => new Date(t));
|
|
}
|
|
|
|
/**
|
|
* Extracts a Float32Array of values from a VariableWithValues, returning a regular number[].
|
|
* Falls back to an empty array if no values are present.
|
|
*/
|
|
export function getValues(variable: VariableWithValues): number[] {
|
|
const arr = variable.valuesArray();
|
|
if (!arr) return [];
|
|
return Array.from(arr);
|
|
}
|
|
|
|
/**
|
|
* Extracts Int64 (BigInt) values from a VariableWithValues, converting to number[].
|
|
* Used for variables stored as unix timestamps (e.g. sunrise, sunset).
|
|
*/
|
|
export function getInt64Values(variable: VariableWithValues): number[] {
|
|
const len = variable.valuesInt64Length();
|
|
const result: number[] = [];
|
|
for (let i = 0; i < len; i++) {
|
|
const val = variable.valuesInt64(i);
|
|
result.push(val !== null ? Number(val) : 0);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Converts the SDK Unit enum to a human-readable display string.
|
|
*/
|
|
export function unitToDisplayString(unit: Unit): string {
|
|
switch (unit) {
|
|
case Unit.celsius:
|
|
return '°C';
|
|
case Unit.fahrenheit:
|
|
return '°F';
|
|
case Unit.millimetre:
|
|
return 'mm';
|
|
case Unit.inch:
|
|
return 'in';
|
|
case Unit.kilometres_per_hour:
|
|
return 'km/h';
|
|
case Unit.metre_per_second:
|
|
return 'm/s';
|
|
case Unit.miles_per_hour:
|
|
return 'mph';
|
|
case Unit.knots:
|
|
return 'kn';
|
|
case Unit.percentage:
|
|
return '%';
|
|
case Unit.hectopascal:
|
|
return 'hPa';
|
|
case Unit.degree_direction:
|
|
return '°';
|
|
case Unit.wmo_code:
|
|
return 'wmo code';
|
|
case Unit.seconds:
|
|
return 's';
|
|
case Unit.hours:
|
|
return 'h';
|
|
case Unit.watt_per_square_metre:
|
|
return 'W/m²';
|
|
case Unit.megajoule_per_square_metre:
|
|
return 'MJ/m²';
|
|
case Unit.joule_per_kilogram:
|
|
return 'J/kg';
|
|
case Unit.metre:
|
|
return 'm';
|
|
case Unit.centimetre:
|
|
return 'cm';
|
|
case Unit.kilogram_per_square_metre:
|
|
return 'kg/m²';
|
|
case Unit.kilopascal:
|
|
return 'kPa';
|
|
case Unit.pascal:
|
|
return 'Pa';
|
|
case Unit.fraction:
|
|
return '';
|
|
case Unit.dimensionless:
|
|
return '';
|
|
case Unit.dimensionless_integer:
|
|
return '';
|
|
case Unit.unix_time:
|
|
return 'unixtime';
|
|
case Unit.grains_per_cubic_metre:
|
|
return 'grains/m³';
|
|
case Unit.micrograms_per_cubic_metre:
|
|
return 'µg/m³';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// ─── Shared Types ───────────────────────────────────────────────────────────────
|
|
|
|
export interface WeatherLocation {
|
|
latitude: number;
|
|
longitude: number;
|
|
timezone?: string;
|
|
}
|
|
|
|
export interface WeatherUnitParams {
|
|
temperature_unit?: 'celsius' | 'fahrenheit';
|
|
wind_speed_unit?: 'kmh' | 'ms' | 'mph' | 'kn';
|
|
precipitation_unit?: 'mm' | 'inch';
|
|
}
|
|
|
|
export type MarkArea = [{ xAxis: number; itemStyle: { color: string } }, { xAxis: number }];
|
|
|
|
// ─── Week Forecast Types ────────────────────────────────────────────────────────
|
|
|
|
export interface WeekForecastParams extends WeatherLocation, WeatherUnitParams {
|
|
model?: string;
|
|
forecast_days?: number;
|
|
past_days?: number;
|
|
}
|
|
|
|
export interface WeekHourlyData {
|
|
temperature_2m: number[];
|
|
precipitation: number[];
|
|
precipitation_probability: number[];
|
|
weather_code: number[];
|
|
windspeed_10m: number[];
|
|
winddirection_10m: number[];
|
|
cloud_cover: number[];
|
|
relative_humidity_2m: number[];
|
|
apparent_temperature: number[];
|
|
dew_point_2m: number[];
|
|
}
|
|
|
|
export interface WeekDailyData {
|
|
weather_code: number[];
|
|
temperature_2m_max: number[];
|
|
temperature_2m_min: number[];
|
|
sunrise: number[];
|
|
sunset: number[];
|
|
sunshine_duration: number[];
|
|
precipitation_sum: number[];
|
|
windspeed_10m_max: number[];
|
|
windgusts_10m_max: number[];
|
|
winddirection_10m_dominant: number[];
|
|
}
|
|
|
|
export interface WeekForecastResult {
|
|
hourly: WeekHourlyData;
|
|
daily: WeekDailyData;
|
|
utcOffsetSeconds: number;
|
|
timezone: string;
|
|
hourlyTimestamps: number[];
|
|
hourlyDates: Date[];
|
|
dailyDates: Date[];
|
|
markAreas: MarkArea[];
|
|
}
|
|
|
|
// ─── Model Comparison Types ─────────────────────────────────────────────────────
|
|
|
|
export interface ModelCompareParams extends WeatherLocation, WeatherUnitParams {
|
|
hourlyVariables: string[];
|
|
models: string[];
|
|
}
|
|
|
|
export interface ModelSeriesData {
|
|
modelName: string;
|
|
variables: Record<string, number[]>;
|
|
}
|
|
|
|
export interface ModelCompareResult {
|
|
models: ModelSeriesData[];
|
|
timestamps: number[];
|
|
utcOffsetSeconds: number;
|
|
timezone: string;
|
|
markAreas: MarkArea[];
|
|
sunrise: number[];
|
|
sunset: number[];
|
|
units: Record<string, string>;
|
|
/** Flat record compatible with the existing chart utilities (keys like "temperature_2m_icon_seamless") */
|
|
hourlyFlat: Record<string, number[]>;
|
|
hourlyUnitsFlat: Record<string, string>;
|
|
}
|
|
|
|
// ─── Ensemble Forecast Types ────────────────────────────────────────────────────
|
|
|
|
export interface EnsembleForecastParams extends WeatherLocation, WeatherUnitParams {
|
|
hourlyVariables: string[];
|
|
models: string[];
|
|
forecast_days?: number;
|
|
}
|
|
|
|
export interface EnsembleVariableData {
|
|
members: number[][];
|
|
average: number[];
|
|
min: number[];
|
|
max: number[];
|
|
unit: string;
|
|
}
|
|
|
|
export interface EnsembleForecastResult {
|
|
variables: Record<string, EnsembleVariableData>;
|
|
timestamps: number[];
|
|
utcOffsetSeconds: number;
|
|
timezone: string;
|
|
markAreas: MarkArea[];
|
|
/** Flat record compatible with existing chart utilities (keys like "temperature_2m_member00") */
|
|
hourlyFlat: Record<string, number[]>;
|
|
hourlyUnitsFlat: Record<string, string>;
|
|
}
|
|
|
|
// ─── Week Forecast Fetch ────────────────────────────────────────────────────────
|
|
|
|
const WEEK_HOURLY_VARS = [
|
|
'temperature_2m',
|
|
'precipitation',
|
|
'precipitation_probability',
|
|
'weather_code',
|
|
'wind_speed_10m',
|
|
'wind_direction_10m',
|
|
'cloud_cover',
|
|
'relative_humidity_2m',
|
|
'apparent_temperature',
|
|
'dew_point_2m'
|
|
] as const;
|
|
|
|
const WEEK_DAILY_VARS = [
|
|
'weather_code',
|
|
'temperature_2m_max',
|
|
'temperature_2m_min',
|
|
'sunrise',
|
|
'sunset',
|
|
'sunshine_duration',
|
|
'precipitation_sum',
|
|
'wind_speed_10m_max',
|
|
'wind_gusts_10m_max',
|
|
'wind_direction_10m_dominant'
|
|
] as const;
|
|
|
|
/**
|
|
* Fetches the 7-day (week) weather forecast for a single location and model.
|
|
* Returns typed hourly and daily data structures.
|
|
*/
|
|
export async function fetchWeekForecast(params: WeekForecastParams): Promise<WeekForecastResult> {
|
|
const forecastDays = params.forecast_days ?? 6;
|
|
const pastDays = params.past_days ?? 0;
|
|
const modelParam = params.model && params.model !== 'best_match' ? params.model : undefined;
|
|
|
|
const apiParams: Record<string, string | number | undefined> = {
|
|
latitude: params.latitude,
|
|
longitude: params.longitude,
|
|
hourly: WEEK_HOURLY_VARS.join(','),
|
|
daily: WEEK_DAILY_VARS.join(','),
|
|
temperature_unit: params.temperature_unit ?? 'celsius',
|
|
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
|
|
precipitation_unit: params.precipitation_unit ?? 'mm',
|
|
forecast_days: forecastDays,
|
|
past_days: pastDays,
|
|
models: modelParam,
|
|
timezone: params.timezone
|
|
};
|
|
|
|
// Remove undefined values
|
|
const cleanParams: Record<string, string> = {};
|
|
for (const [key, value] of Object.entries(apiParams)) {
|
|
if (value !== undefined) {
|
|
cleanParams[key] = String(value);
|
|
}
|
|
}
|
|
|
|
const responses = await fetchWeatherApi(FORECAST_URL, cleanParams);
|
|
const response = responses[0];
|
|
const utcOffsetSeconds = response.utcOffsetSeconds();
|
|
const timezone = response.timezone() ?? params.timezone ?? 'UTC';
|
|
|
|
const hourlyBlock = response.hourly()!;
|
|
const dailyBlock = response.daily()!;
|
|
|
|
// Hourly: variables are in the same order as WEEK_HOURLY_VARS
|
|
const hourlyTimestamps = getTimestamps(hourlyBlock);
|
|
const hourlyDates = hourlyTimestamps.map((t) => new Date(t));
|
|
|
|
const hourly: WeekHourlyData = {
|
|
temperature_2m: getValues(hourlyBlock.variables(0)!),
|
|
precipitation: getValues(hourlyBlock.variables(1)!),
|
|
precipitation_probability: getValues(hourlyBlock.variables(2)!),
|
|
weather_code: getValues(hourlyBlock.variables(3)!),
|
|
windspeed_10m: getValues(hourlyBlock.variables(4)!),
|
|
winddirection_10m: getValues(hourlyBlock.variables(5)!),
|
|
cloud_cover: getValues(hourlyBlock.variables(6)!),
|
|
relative_humidity_2m: getValues(hourlyBlock.variables(7)!),
|
|
apparent_temperature: getValues(hourlyBlock.variables(8)!),
|
|
dew_point_2m: getValues(hourlyBlock.variables(9)!)
|
|
};
|
|
|
|
// Daily: variables are in the same order as WEEK_DAILY_VARS
|
|
const dailyDates = getDates(dailyBlock);
|
|
|
|
const sunriseVar = dailyBlock.variables(3)!;
|
|
const sunsetVar = dailyBlock.variables(4)!;
|
|
|
|
const daily: WeekDailyData = {
|
|
weather_code: getValues(dailyBlock.variables(0)!),
|
|
temperature_2m_max: getValues(dailyBlock.variables(1)!),
|
|
temperature_2m_min: getValues(dailyBlock.variables(2)!),
|
|
sunrise: getInt64Values(sunriseVar),
|
|
sunset: getInt64Values(sunsetVar),
|
|
sunshine_duration: getValues(dailyBlock.variables(5)!),
|
|
precipitation_sum: getValues(dailyBlock.variables(6)!),
|
|
windspeed_10m_max: getValues(dailyBlock.variables(7)!),
|
|
windgusts_10m_max: getValues(dailyBlock.variables(8)!),
|
|
winddirection_10m_dominant: getValues(dailyBlock.variables(9)!)
|
|
};
|
|
|
|
const markAreas = buildDaylightMarkAreas(daily.sunrise, daily.sunset);
|
|
|
|
return {
|
|
hourly,
|
|
daily,
|
|
utcOffsetSeconds,
|
|
timezone,
|
|
hourlyTimestamps,
|
|
hourlyDates,
|
|
dailyDates,
|
|
markAreas
|
|
};
|
|
}
|
|
|
|
// ─── Model Comparison Fetch ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Fetches forecast data for multiple models for comparison.
|
|
* Also fetches daily sunrise/sunset for daylight mark areas.
|
|
*
|
|
* Returns both a typed model array structure and a flat record structure
|
|
* compatible with existing chart utilities.
|
|
*/
|
|
export async function fetchModelComparison(
|
|
params: ModelCompareParams
|
|
): Promise<ModelCompareResult> {
|
|
const forecastApiParams: Record<string, string | number | undefined> = {
|
|
latitude: String(params.latitude),
|
|
longitude: String(params.longitude),
|
|
hourly: params.hourlyVariables.join(','),
|
|
models: params.models.join(','),
|
|
daily: 'sunrise,sunset',
|
|
temperature_unit: params.temperature_unit ?? 'celsius',
|
|
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
|
|
precipitation_unit: params.precipitation_unit ?? 'mm',
|
|
timezone: params.timezone
|
|
};
|
|
|
|
const responses = await fetchWeatherApi(FORECAST_URL, forecastApiParams);
|
|
|
|
// With multiple models, we get one response per model
|
|
const firstResponse = responses[0];
|
|
const utcOffsetSeconds = firstResponse.utcOffsetSeconds();
|
|
const timezone = firstResponse.timezone() ?? params.timezone ?? 'UTC';
|
|
|
|
const hourlyBlock = firstResponse.hourly()!;
|
|
const timestamps = getTimestamps(hourlyBlock);
|
|
|
|
// Extract sunrise/sunset from the first response's daily block
|
|
let markAreas: MarkArea[] = [];
|
|
let sunrise: number[] = [];
|
|
let sunset: number[] = [];
|
|
const dailyBlock = firstResponse.daily();
|
|
if (dailyBlock) {
|
|
const sunriseVar = dailyBlock.variables(0)!;
|
|
const sunsetVar = dailyBlock.variables(1)!;
|
|
sunrise = getInt64Values(sunriseVar);
|
|
sunset = getInt64Values(sunsetVar);
|
|
markAreas = buildDaylightMarkAreas(sunrise, sunset);
|
|
}
|
|
|
|
// Process each model's response
|
|
const models: ModelSeriesData[] = [];
|
|
const hourlyFlat: Record<string, number[]> = {};
|
|
const hourlyUnitsFlat: Record<string, string> = {};
|
|
const units: Record<string, string> = {};
|
|
|
|
// Add time to flat record
|
|
const timeInUnixSeconds = range(
|
|
Number(hourlyBlock.time()),
|
|
Number(hourlyBlock.timeEnd()),
|
|
hourlyBlock.interval()
|
|
);
|
|
hourlyFlat['time'] = timeInUnixSeconds;
|
|
|
|
for (const response of responses) {
|
|
const modelHourly = response.hourly();
|
|
if (!modelHourly) continue;
|
|
|
|
// Determine model name from the response
|
|
const modelEnum = response.model();
|
|
const modelName = modelEnumToString(modelEnum);
|
|
|
|
const modelData: ModelSeriesData = {
|
|
modelName,
|
|
variables: {}
|
|
};
|
|
|
|
for (let vi = 0; vi < params.hourlyVariables.length; vi++) {
|
|
const varName = params.hourlyVariables[vi];
|
|
const variable = modelHourly.variables(vi);
|
|
if (!variable) continue;
|
|
|
|
const values = getValues(variable);
|
|
modelData.variables[varName] = values;
|
|
|
|
// Build flat key like "temperature_2m_icon_seamless"
|
|
const flatKey = `${varName}_${modelName}`;
|
|
hourlyFlat[flatKey] = values;
|
|
|
|
// Record unit
|
|
const unitStr = unitToDisplayString(variable.unit());
|
|
units[varName] = unitStr;
|
|
hourlyUnitsFlat[flatKey] = unitStr;
|
|
}
|
|
|
|
models.push(modelData);
|
|
}
|
|
|
|
return {
|
|
models,
|
|
timestamps,
|
|
utcOffsetSeconds,
|
|
timezone,
|
|
markAreas,
|
|
sunrise,
|
|
sunset,
|
|
units,
|
|
hourlyFlat,
|
|
hourlyUnitsFlat
|
|
};
|
|
}
|
|
|
|
// ─── Ensemble Forecast Fetch ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Fetches ensemble forecast data from the ensemble API.
|
|
* Separately fetches daily sunrise/sunset from the standard forecast API.
|
|
*
|
|
* Returns typed ensemble data with per-variable member arrays, averages, and spreads,
|
|
* plus a flat record structure for compatibility with existing chart utilities.
|
|
*/
|
|
export async function fetchEnsembleForecast(
|
|
params: EnsembleForecastParams
|
|
): Promise<EnsembleForecastResult> {
|
|
const forecastDays = params.forecast_days ?? 14;
|
|
|
|
const ensembleParams: Record<string, string | number | undefined> = {
|
|
latitude: String(params.latitude),
|
|
longitude: String(params.longitude),
|
|
hourly: params.hourlyVariables.join(','),
|
|
models: params.models.join(','),
|
|
forecast_days: String(forecastDays),
|
|
temperature_unit: params.temperature_unit ?? 'celsius',
|
|
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
|
|
precipitation_unit: params.precipitation_unit ?? 'mm',
|
|
timezone: params.timezone
|
|
};
|
|
|
|
const dailyParams: Record<string, string> = {
|
|
latitude: String(params.latitude),
|
|
longitude: String(params.longitude),
|
|
daily: 'sunrise,sunset',
|
|
forecast_days: String(forecastDays),
|
|
temperature_unit: params.temperature_unit ?? 'celsius'
|
|
};
|
|
|
|
// Fetch ensemble and daily data in parallel
|
|
const [ensembleResponses, dailyResponses] = await Promise.all([
|
|
fetchWeatherApi(ENSEMBLE_URL, ensembleParams),
|
|
fetchWeatherApi(FORECAST_URL, dailyParams)
|
|
]);
|
|
|
|
const ensembleResponse = ensembleResponses[0];
|
|
const utcOffsetSeconds = ensembleResponse.utcOffsetSeconds();
|
|
const timezone = ensembleResponse.timezone() ?? params.timezone ?? 'UTC';
|
|
|
|
const hourlyBlock = ensembleResponse.hourly()!;
|
|
const timestamps = getTimestamps(hourlyBlock);
|
|
const timeLength = timestamps.length;
|
|
|
|
// Extract sunrise/sunset for mark areas
|
|
let markAreas: MarkArea[] = [];
|
|
if (dailyResponses.length > 0) {
|
|
const dailyResponse = dailyResponses[0];
|
|
const dailyBlock = dailyResponse.daily();
|
|
if (dailyBlock) {
|
|
const sunrise = getInt64Values(dailyBlock.variables(0)!);
|
|
const sunset = getInt64Values(dailyBlock.variables(1)!);
|
|
markAreas = buildDaylightMarkAreas(sunrise, sunset);
|
|
}
|
|
}
|
|
|
|
// Process ensemble variables
|
|
// Each requested variable will have multiple entries in the variables list (one per ensemble member)
|
|
const variables: Record<string, EnsembleVariableData> = {};
|
|
const hourlyFlat: Record<string, number[]> = {};
|
|
const hourlyUnitsFlat: Record<string, string> = {};
|
|
|
|
// Add time to flat record
|
|
const timeInUnixSeconds = range(
|
|
Number(hourlyBlock.time()),
|
|
Number(hourlyBlock.timeEnd()),
|
|
hourlyBlock.interval()
|
|
);
|
|
hourlyFlat['time'] = timeInUnixSeconds;
|
|
|
|
// Group variables by their requested variable name
|
|
// The SDK provides variables indexed sequentially:
|
|
// For N requested variables and M ensemble members, we get N*M variables
|
|
// ordered as: var0_member0, var0_member1, ..., var0_memberM-1, var1_member0, ...
|
|
const totalVariables = hourlyBlock.variablesLength();
|
|
const numRequestedVars = params.hourlyVariables.length;
|
|
|
|
if (totalVariables > 0 && numRequestedVars > 0) {
|
|
const membersPerVar = Math.floor(totalVariables / numRequestedVars);
|
|
|
|
for (let vi = 0; vi < numRequestedVars; vi++) {
|
|
const varName = params.hourlyVariables[vi];
|
|
const members: number[][] = [];
|
|
let unitStr = '';
|
|
|
|
for (let mi = 0; mi < membersPerVar; mi++) {
|
|
const varIdx = vi * membersPerVar + mi;
|
|
const variable = hourlyBlock.variables(varIdx);
|
|
if (!variable) continue;
|
|
|
|
const values = getValues(variable);
|
|
members.push(values);
|
|
|
|
if (mi === 0) {
|
|
unitStr = unitToDisplayString(variable.unit());
|
|
}
|
|
|
|
// Build flat key compatible with JSON API format
|
|
const memberStr = String(mi).padStart(2, '0');
|
|
const flatKey = `${varName}_member${memberStr}`;
|
|
hourlyFlat[flatKey] = values;
|
|
hourlyUnitsFlat[flatKey] = unitStr;
|
|
}
|
|
|
|
// Calculate average, min, max across members
|
|
const average = new Array<number>(timeLength).fill(0);
|
|
const min = new Array<number>(timeLength).fill(Infinity);
|
|
const max = new Array<number>(timeLength).fill(-Infinity);
|
|
|
|
for (let t = 0; t < timeLength; t++) {
|
|
let count = 0;
|
|
for (const memberValues of members) {
|
|
const val = memberValues[t];
|
|
if (val !== null && val !== undefined && !isNaN(val)) {
|
|
average[t] += val;
|
|
count++;
|
|
if (val < min[t]) min[t] = val;
|
|
if (val > max[t]) max[t] = val;
|
|
}
|
|
}
|
|
if (count > 0) {
|
|
average[t] = Math.round((average[t] / count) * 10) / 10;
|
|
}
|
|
if (min[t] === Infinity) min[t] = 0;
|
|
if (max[t] === -Infinity) max[t] = 0;
|
|
}
|
|
|
|
variables[varName] = {
|
|
members,
|
|
average,
|
|
min,
|
|
max,
|
|
unit: unitStr
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
variables,
|
|
timestamps,
|
|
utcOffsetSeconds,
|
|
timezone,
|
|
markAreas,
|
|
hourlyFlat,
|
|
hourlyUnitsFlat
|
|
};
|
|
}
|
|
|
|
// ─── Model Enum Mapping ─────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Maps the SDK Model enum integer to a string model name.
|
|
* This table must stay in sync with the @openmeteo/sdk Model enum.
|
|
*/
|
|
function modelEnumToString(modelEnum: number): string {
|
|
const modelMap: Record<number, string> = {
|
|
0: 'undefined',
|
|
1: 'best_match',
|
|
2: 'gfs_seamless',
|
|
3: 'gfs_global',
|
|
4: 'gfs_hrrr',
|
|
5: 'meteofrance_seamless',
|
|
6: 'meteofrance_arpege_seamless',
|
|
7: 'meteofrance_arpege_world',
|
|
8: 'meteofrance_arpege_europe',
|
|
9: 'meteofrance_arome_seamless',
|
|
10: 'meteofrance_arome_france',
|
|
11: 'meteofrance_arome_france_hd',
|
|
12: 'jma_seamless',
|
|
13: 'jma_msm',
|
|
14: 'jms_gsm',
|
|
15: 'jma_gsm',
|
|
16: 'gem_seamless',
|
|
17: 'gem_global',
|
|
18: 'gem_regional',
|
|
19: 'gem_hrdps_continental',
|
|
20: 'icon_seamless',
|
|
21: 'icon_global',
|
|
22: 'icon_eu',
|
|
23: 'icon_d2',
|
|
24: 'ecmwf_ifs04',
|
|
25: 'metno_nordic',
|
|
26: 'era5_seamless',
|
|
27: 'era5',
|
|
28: 'cerra',
|
|
29: 'era5_land',
|
|
30: 'ecmwf_ifs',
|
|
31: 'gwam',
|
|
32: 'ewam',
|
|
33: 'glofas_seamless_v3',
|
|
34: 'glofas_forecast_v3',
|
|
35: 'glofas_consolidated_v3',
|
|
36: 'glofas_seamless_v4',
|
|
37: 'glofas_forecast_v4',
|
|
38: 'glofas_consolidated_v4',
|
|
39: 'gfs025',
|
|
40: 'gfs05',
|
|
41: 'CMCC_CM2_VHR4',
|
|
42: 'FGOALS_f3_H_highresSST',
|
|
43: 'FGOALS_f3_H',
|
|
44: 'HiRAM_SIT_HR',
|
|
45: 'MRI_AGCM3_2_S',
|
|
46: 'EC_Earth3P_HR',
|
|
47: 'MPI_ESM1_2_XR',
|
|
48: 'NICAM16_8S',
|
|
49: 'cams_europe',
|
|
50: 'cams_global',
|
|
51: 'cfsv2',
|
|
52: 'era5_ocean',
|
|
53: 'cma_grapes_global',
|
|
54: 'bom_access_global',
|
|
55: 'bom_access_global_ensemble',
|
|
56: 'arpae_cosmo_seamless',
|
|
57: 'arpae_cosmo_2i',
|
|
58: 'arpae_cosmo_2i_ruc',
|
|
59: 'arpae_cosmo_5m',
|
|
60: 'ecmwf_ifs025',
|
|
61: 'ecmwf_aifs025',
|
|
62: 'gfs013',
|
|
63: 'gfs_graphcast025',
|
|
64: 'ecmwf_wam025',
|
|
65: 'meteofrance_wave',
|
|
66: 'meteofrance_currents',
|
|
67: 'ecmwf_wam025_ensemble',
|
|
68: 'ncep_gfswave025',
|
|
69: 'ncep_gefswave025',
|
|
70: 'knmi_seamless',
|
|
71: 'knmi_harmonie_arome_europe',
|
|
72: 'knmi_harmonie_arome_netherlands',
|
|
73: 'dmi_seamless',
|
|
74: 'dmi_harmonie_arome_europe',
|
|
75: 'metno_seamless',
|
|
76: 'era5_ensemble',
|
|
77: 'ecmwf_ifs_analysis',
|
|
78: 'ecmwf_ifs_long_window',
|
|
79: 'ecmwf_ifs_analysis_long_window',
|
|
80: 'ukmo_global_deterministic_10km',
|
|
81: 'ukmo_uk_deterministic_2km',
|
|
82: 'ukmo_seamless',
|
|
83: 'ncep_gfswave016',
|
|
84: 'ncep_nbm_conus',
|
|
85: 'ukmo_global_ensemble_20km',
|
|
86: 'ecmwf_aifs025_single',
|
|
87: 'jma_jaxa_himawari',
|
|
88: 'eumetsat_sarah3',
|
|
89: 'eumetsat_lsa_saf_msg',
|
|
90: 'eumetsat_lsa_saf_iodc',
|
|
91: 'satellite_radiation_seamless',
|
|
92: 'kma_gdps',
|
|
93: 'kma_ldps',
|
|
94: 'kma_seamless',
|
|
95: 'italia_meteo_arpae_icon_2i',
|
|
96: 'ukmo_uk_ensemble_2km',
|
|
97: 'meteofrance_arome_france_hd_15min',
|
|
98: 'meteofrance_arome_france_15min',
|
|
99: 'meteoswiss_icon_ch1',
|
|
100: 'meteoswiss_icon_ch2',
|
|
101: 'meteoswiss_icon_ch1_ensemble',
|
|
102: 'meteoswiss_icon_ch2_ensemble',
|
|
103: 'meteoswiss_icon_seamless',
|
|
104: 'ncep_nam_conus',
|
|
105: 'icon_d2_ruc',
|
|
106: 'ecmwf_seas5',
|
|
107: 'ecmwf_ec46',
|
|
108: 'ecmwf_seasonal_seamless',
|
|
109: 'ecmwf_ifs_seamless',
|
|
110: 'jma_jaxa_mtg_fci',
|
|
111: 'gem_hrdps_west'
|
|
};
|
|
return modelMap[modelEnum] ?? `model_${modelEnum}`;
|
|
}
|