unify weather data fetching
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
export {
|
||||
fetchWeekForecast,
|
||||
fetchModelComparison,
|
||||
fetchEnsembleForecast,
|
||||
range,
|
||||
getTimestamps,
|
||||
getDates,
|
||||
getValues,
|
||||
getInt64Values,
|
||||
extractValues,
|
||||
unitToDisplayString
|
||||
} from './weather';
|
||||
|
||||
export type {
|
||||
WeatherLocation,
|
||||
WeatherUnitParams,
|
||||
MarkArea,
|
||||
WeekForecastParams,
|
||||
WeekHourlyData,
|
||||
WeekDailyData,
|
||||
WeekForecastResult,
|
||||
ModelCompareParams,
|
||||
ModelSeriesData,
|
||||
ModelCompareResult,
|
||||
EnsembleForecastParams,
|
||||
EnsembleVariableData,
|
||||
EnsembleForecastResult
|
||||
} from './weather';
|
||||
@@ -0,0 +1,736 @@
|
||||
/**
|
||||
* 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, utcOffsetSeconds: number): number[] {
|
||||
const start = Number(timeBlock.time());
|
||||
const end = Number(timeBlock.timeEnd());
|
||||
const interval = timeBlock.interval();
|
||||
return range(start, end, interval).map((t) => (t + utcOffsetSeconds) * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts Date array (with UTC offset applied) from a VariablesWithTime block.
|
||||
*/
|
||||
export function getDates(timeBlock: VariablesWithTime, utcOffsetSeconds: number): Date[] {
|
||||
return getTimestamps(timeBlock, utcOffsetSeconds).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;
|
||||
}
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
markAreas: MarkArea[];
|
||||
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;
|
||||
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'
|
||||
] 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 ?? 1;
|
||||
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
|
||||
};
|
||||
|
||||
// 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 hourlyBlock = response.hourly()!;
|
||||
const dailyBlock = response.daily()!;
|
||||
|
||||
// Hourly: variables are in the same order as WEEK_HOURLY_VARS
|
||||
const hourlyTimestamps = getTimestamps(hourlyBlock, utcOffsetSeconds);
|
||||
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)!)
|
||||
};
|
||||
|
||||
// Daily: variables are in the same order as WEEK_DAILY_VARS
|
||||
const dailyDates = getDates(dailyBlock, utcOffsetSeconds);
|
||||
|
||||
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, utcOffsetSeconds);
|
||||
|
||||
return {
|
||||
hourly,
|
||||
daily,
|
||||
utcOffsetSeconds,
|
||||
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> = {
|
||||
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'
|
||||
};
|
||||
|
||||
const responses = await fetchWeatherApi(FORECAST_URL, forecastApiParams);
|
||||
|
||||
// With multiple models, we get one response per model
|
||||
const firstResponse = responses[0];
|
||||
const utcOffsetSeconds = firstResponse.utcOffsetSeconds();
|
||||
|
||||
// Extract timestamps from the first response's hourly block
|
||||
const hourlyBlock = firstResponse.hourly()!;
|
||||
const timestamps = getTimestamps(hourlyBlock, utcOffsetSeconds);
|
||||
|
||||
// Extract sunrise/sunset from the first response's daily block
|
||||
let markAreas: MarkArea[] = [];
|
||||
const dailyBlock = firstResponse.daily();
|
||||
if (dailyBlock) {
|
||||
const sunriseVar = dailyBlock.variables(0)!;
|
||||
const sunsetVar = dailyBlock.variables(1)!;
|
||||
const sunrise = getInt64Values(sunriseVar);
|
||||
const sunset = getInt64Values(sunsetVar);
|
||||
markAreas = buildDaylightMarkAreas(sunrise, sunset, utcOffsetSeconds);
|
||||
}
|
||||
|
||||
// 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,
|
||||
markAreas,
|
||||
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> = {
|
||||
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'
|
||||
};
|
||||
|
||||
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 hourlyBlock = ensembleResponse.hourly()!;
|
||||
const timestamps = getTimestamps(hourlyBlock, utcOffsetSeconds);
|
||||
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, dailyResponse.utcOffsetSeconds());
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
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}`;
|
||||
}
|
||||
@@ -7,14 +7,9 @@
|
||||
import {
|
||||
buildAverageSeries,
|
||||
buildCurrentTimeSeries,
|
||||
buildDaylightMarkAreas,
|
||||
buildDaylightSeries,
|
||||
buildSpreadSeries,
|
||||
calculateAverage,
|
||||
calculateSpread,
|
||||
composeChartOption,
|
||||
convertTimestamps,
|
||||
findUnit,
|
||||
getThemeColors
|
||||
} from '$lib/utils/echarts';
|
||||
|
||||
@@ -23,6 +18,12 @@
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
|
||||
import {
|
||||
type EnsembleForecastResult,
|
||||
type MarkArea,
|
||||
fetchEnsembleForecast
|
||||
} from '$lib/services/weather';
|
||||
|
||||
import { defaultParameters } from '../options';
|
||||
|
||||
import type * as echarts from 'echarts';
|
||||
@@ -52,11 +53,10 @@
|
||||
// ─── Cached API Response ────────────────────────────────────────────────────
|
||||
|
||||
interface FetchedData {
|
||||
hourly: Record<string, unknown>;
|
||||
hourly_units: Record<string, string>;
|
||||
utc_offset_seconds: number;
|
||||
markAreas: Array<[{ xAxis: number; itemStyle: { color: string } }, { xAxis: number }]>;
|
||||
ensembleResult: EnsembleForecastResult;
|
||||
timestamps: number[];
|
||||
utc_offset_seconds: number;
|
||||
markAreas: MarkArea[];
|
||||
}
|
||||
|
||||
let fetchedData: FetchedData | null = $state(null);
|
||||
@@ -92,35 +92,22 @@
|
||||
chartInstances = [];
|
||||
chartComponents = [];
|
||||
|
||||
const [dataDaily, dataReq] = await Promise.all([
|
||||
fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&timeformat=unixtime&daily=sunset,sunrise&forecast_days=14`
|
||||
),
|
||||
fetch(
|
||||
`https://ensemble-api.open-meteo.com/v1/ensemble?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${hourlyVars.join(',')}&models=${modelList.join(',')}&timeformat=unixtime&forecast_days=14`
|
||||
)
|
||||
]);
|
||||
|
||||
const [wd, data] = await Promise.all([dataDaily.json(), dataReq.json()]);
|
||||
|
||||
let markAreas: FetchedData['markAreas'] = [];
|
||||
|
||||
if ('daily' in wd && 'sunrise' in wd.daily && 'sunset' in wd.daily) {
|
||||
markAreas = buildDaylightMarkAreas(
|
||||
wd.daily.sunrise,
|
||||
wd.daily.sunset,
|
||||
data.utc_offset_seconds
|
||||
);
|
||||
}
|
||||
|
||||
const timestamps = convertTimestamps(data.hourly.time, data.utc_offset_seconds);
|
||||
const result: EnsembleForecastResult = await fetchEnsembleForecast({
|
||||
latitude: location.latitude!,
|
||||
longitude: location.longitude!,
|
||||
hourlyVariables: hourlyVars,
|
||||
models: modelList,
|
||||
forecast_days: 14,
|
||||
temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit',
|
||||
wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn',
|
||||
precipitation_unit: params.precipitation_unit as 'mm' | 'inch'
|
||||
});
|
||||
|
||||
fetchedData = {
|
||||
hourly: data.hourly,
|
||||
hourly_units: data.hourly_units,
|
||||
utc_offset_seconds: data.utc_offset_seconds,
|
||||
markAreas,
|
||||
timestamps
|
||||
ensembleResult: result,
|
||||
timestamps: result.timestamps,
|
||||
utc_offset_seconds: result.utcOffsetSeconds,
|
||||
markAreas: result.markAreas
|
||||
};
|
||||
|
||||
loading = false;
|
||||
@@ -134,32 +121,26 @@
|
||||
$effect(() => {
|
||||
if (!fetchedData) return;
|
||||
|
||||
const {
|
||||
hourly: hourlyData,
|
||||
hourly_units,
|
||||
utc_offset_seconds,
|
||||
markAreas,
|
||||
timestamps
|
||||
} = fetchedData;
|
||||
const { ensembleResult, timestamps, utc_offset_seconds, markAreas } = fetchedData;
|
||||
const _showLegend = showLegend;
|
||||
|
||||
const colors = getThemeColors();
|
||||
const variableCount = params.hourly?.length || 0;
|
||||
const timeLength = (hourlyData.time as number[]).length;
|
||||
const newOptions: Array<Record<string, unknown>> = [];
|
||||
|
||||
for (let vi = 0; vi < variableCount; vi++) {
|
||||
const variable = params.hourly![vi];
|
||||
const unit = findUnit(hourly_units, hourlyData, variable);
|
||||
const varData = ensembleResult.variables[variable];
|
||||
if (!varData) continue;
|
||||
|
||||
const { average } = calculateAverage(hourlyData, variable, timeLength);
|
||||
const { minValues, maxValues } = calculateSpread(hourlyData, variable, timeLength);
|
||||
const unit = varData.unit;
|
||||
const { average, min: minValues, max: maxValues } = varData;
|
||||
|
||||
const series: Array<Record<string, unknown>> = [];
|
||||
|
||||
const spreadData: Array<[number, number, number]> = minValues.map(
|
||||
(min, index) =>
|
||||
[timestamps[index], min ?? 0, maxValues[index] ?? 0] as [number, number, number]
|
||||
(minVal, index) =>
|
||||
[timestamps[index], minVal ?? 0, maxValues[index] ?? 0] as [number, number, number]
|
||||
);
|
||||
|
||||
series.push(...buildSpreadSeries({ variable, spreadData }));
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
import {
|
||||
buildAverageSeries,
|
||||
buildCurrentTimeSeries,
|
||||
buildDaylightMarkAreas,
|
||||
buildDaylightSeries,
|
||||
buildModelSeries,
|
||||
calculateAverage,
|
||||
composeChartOption,
|
||||
convertTimestamps,
|
||||
findUnit,
|
||||
getThemeColors
|
||||
} from '$lib/utils/echarts';
|
||||
@@ -24,6 +22,12 @@
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
|
||||
import {
|
||||
type MarkArea,
|
||||
type ModelCompareResult,
|
||||
fetchModelComparison
|
||||
} from '$lib/services/weather';
|
||||
|
||||
import { hourly, models as modelsFlat } from '../options';
|
||||
import { defaultParameters } from '../options';
|
||||
|
||||
@@ -65,7 +69,7 @@
|
||||
hourly: Record<string, unknown>;
|
||||
hourly_units: Record<string, string>;
|
||||
utc_offset_seconds: number;
|
||||
markAreas: Array<[{ xAxis: number; itemStyle: { color: string } }, { xAxis: number }]>;
|
||||
markAreas: MarkArea[];
|
||||
timestamps: number[];
|
||||
}
|
||||
|
||||
@@ -102,38 +106,22 @@
|
||||
chartInstances = [];
|
||||
chartComponents = [];
|
||||
|
||||
const dataReq = await fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${hourlyVars.join(',')}&models=${modelList.join(',')}&timeformat=unixtime&daily=sunset,sunrise`
|
||||
);
|
||||
const data = await dataReq.json();
|
||||
|
||||
let markAreas: FetchedData['markAreas'] = [];
|
||||
|
||||
if ('daily' in data) {
|
||||
let dailyFirstModelKey: string[] | string = Object.keys(data.daily)[1].split('_');
|
||||
dailyFirstModelKey.shift();
|
||||
dailyFirstModelKey = dailyFirstModelKey.join('_');
|
||||
|
||||
const sunriseKey = 'sunrise_' + dailyFirstModelKey;
|
||||
const sunsetKey = 'sunset_' + dailyFirstModelKey;
|
||||
|
||||
if (sunriseKey in data.daily && sunsetKey in data.daily) {
|
||||
markAreas = buildDaylightMarkAreas(
|
||||
data.daily[sunriseKey],
|
||||
data.daily[sunsetKey],
|
||||
data.utc_offset_seconds
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const timestamps = convertTimestamps(data.hourly.time, data.utc_offset_seconds);
|
||||
const result: ModelCompareResult = await fetchModelComparison({
|
||||
latitude: location.latitude!,
|
||||
longitude: location.longitude!,
|
||||
hourlyVariables: hourlyVars,
|
||||
models: modelList,
|
||||
temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit',
|
||||
wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn',
|
||||
precipitation_unit: params.precipitation_unit as 'mm' | 'inch'
|
||||
});
|
||||
|
||||
fetchedData = {
|
||||
hourly: data.hourly,
|
||||
hourly_units: data.hourly_units,
|
||||
utc_offset_seconds: data.utc_offset_seconds,
|
||||
markAreas,
|
||||
timestamps
|
||||
hourly: result.hourlyFlat,
|
||||
hourly_units: result.hourlyUnitsFlat,
|
||||
utc_offset_seconds: result.utcOffsetSeconds,
|
||||
markAreas: result.markAreas,
|
||||
timestamps: result.timestamps
|
||||
};
|
||||
|
||||
loading = false;
|
||||
@@ -158,7 +146,7 @@
|
||||
|
||||
const colors = getThemeColors();
|
||||
const variableCount = params.hourly?.length || 0;
|
||||
const timeLength = (hourlyData.time as number[]).length;
|
||||
const timeLength = timestamps.length;
|
||||
const newOptions: Array<Record<string, unknown>> = [];
|
||||
|
||||
for (let vi = 0; vi < variableCount; vi++) {
|
||||
|
||||
@@ -7,13 +7,7 @@
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import {
|
||||
buildCurrentTimeSeries,
|
||||
buildDaylightMarkAreas,
|
||||
buildDaylightSeries,
|
||||
convertTimestamps,
|
||||
getThemeColors
|
||||
} from '$lib/utils/echarts';
|
||||
import { buildCurrentTimeSeries, buildDaylightSeries, getThemeColors } from '$lib/utils/echarts';
|
||||
import { pad } from '$lib/utils/index';
|
||||
|
||||
import { ChartContainer, ChartToolbar, EChart } from '$lib/components/charts';
|
||||
@@ -21,6 +15,14 @@
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
|
||||
import {
|
||||
type MarkArea,
|
||||
type WeekDailyData,
|
||||
type WeekForecastResult,
|
||||
type WeekHourlyData,
|
||||
fetchWeekForecast
|
||||
} from '$lib/services/weather';
|
||||
|
||||
import { defaultParameters, models } from '../../options';
|
||||
import { getColor } from '../../utils/colors';
|
||||
import weatherCodes from '../../utils/weather-codes';
|
||||
@@ -61,42 +63,16 @@
|
||||
|
||||
// ─── Fetched Data ───────────────────────────────────────────────────────────
|
||||
|
||||
interface HourlyData {
|
||||
time: number[];
|
||||
temperature_2m: number[];
|
||||
precipitation: number[];
|
||||
precipitation_probability: number[];
|
||||
weather_code: number[];
|
||||
windspeed_10m: number[];
|
||||
winddirection_10m: number[];
|
||||
cloud_cover: number[];
|
||||
relative_humidity_2m: number[];
|
||||
}
|
||||
|
||||
interface DailyData {
|
||||
time: string[];
|
||||
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[];
|
||||
}
|
||||
|
||||
interface FetchedHourly {
|
||||
hourly: HourlyData;
|
||||
hourly: WeekHourlyData;
|
||||
utc_offset_seconds: number;
|
||||
timestamps: number[];
|
||||
hourlyDates: Date[];
|
||||
markAreas: Array<[{ xAxis: number; itemStyle: { color: string } }, { xAxis: number }]>;
|
||||
markAreas: MarkArea[];
|
||||
}
|
||||
|
||||
interface FetchedDaily {
|
||||
daily: DailyData;
|
||||
daily: WeekDailyData;
|
||||
dailyDates: Date[];
|
||||
}
|
||||
|
||||
@@ -276,73 +252,28 @@
|
||||
chartInstances = [];
|
||||
chartComponents = [];
|
||||
|
||||
const model = modelList[0];
|
||||
const hourlyVars = [
|
||||
'temperature_2m',
|
||||
'precipitation',
|
||||
'precipitation_probability',
|
||||
'weather_code',
|
||||
'windspeed_10m',
|
||||
'winddirection_10m',
|
||||
'cloud_cover',
|
||||
'relative_humidity_2m'
|
||||
].join(',');
|
||||
|
||||
const dailyVars = [
|
||||
'weather_code',
|
||||
'temperature_2m_max',
|
||||
'temperature_2m_min',
|
||||
'sunrise',
|
||||
'sunset',
|
||||
'sunshine_duration',
|
||||
'precipitation_sum',
|
||||
'windspeed_10m_max',
|
||||
'windgusts_10m_max',
|
||||
'winddirection_10m_dominant'
|
||||
].join(',');
|
||||
|
||||
const baseParams = `latitude=${loc.latitude}&longitude=${loc.longitude}&temperature_unit=${params.temperature_unit}&wind_speed_unit=${params.wind_speed_unit}&precipitation_unit=${params.precipitation_unit}`;
|
||||
const modelParam = model === 'best_match' ? '' : `&models=${model}`;
|
||||
|
||||
const [hourlyResp, dailyResp] = await Promise.all([
|
||||
fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?${baseParams}&hourly=${hourlyVars}${modelParam}&timeformat=unixtime&forecast_days=6&past_days=1&daily=sunrise,sunset`
|
||||
),
|
||||
fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?${baseParams}&daily=${dailyVars}${modelParam}&timeformat=unixtime&forecast_days=6&past_days=1`
|
||||
)
|
||||
]);
|
||||
|
||||
const [hourlyJson, dailyJson] = await Promise.all([hourlyResp.json(), dailyResp.json()]);
|
||||
|
||||
const utcOffset = hourlyJson.utc_offset_seconds ?? 0;
|
||||
const timestamps = convertTimestamps(hourlyJson.hourly.time, utcOffset);
|
||||
const hourlyDates = timestamps.map((t: number) => new Date(t));
|
||||
|
||||
let markAreas: FetchedHourly['markAreas'] = [];
|
||||
if (hourlyJson.daily?.sunrise && hourlyJson.daily?.sunset) {
|
||||
markAreas = buildDaylightMarkAreas(
|
||||
hourlyJson.daily.sunrise,
|
||||
hourlyJson.daily.sunset,
|
||||
utcOffset
|
||||
);
|
||||
}
|
||||
const result: WeekForecastResult = await fetchWeekForecast({
|
||||
latitude: loc.latitude!,
|
||||
longitude: loc.longitude!,
|
||||
model: modelList[0],
|
||||
temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit',
|
||||
wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn',
|
||||
precipitation_unit: params.precipitation_unit as 'mm' | 'inch',
|
||||
forecast_days: 6,
|
||||
past_days: 1
|
||||
});
|
||||
|
||||
fetchedHourly = {
|
||||
hourly: hourlyJson.hourly,
|
||||
utc_offset_seconds: utcOffset,
|
||||
timestamps,
|
||||
hourlyDates,
|
||||
markAreas
|
||||
hourly: result.hourly,
|
||||
utc_offset_seconds: result.utcOffsetSeconds,
|
||||
timestamps: result.hourlyTimestamps,
|
||||
hourlyDates: result.hourlyDates,
|
||||
markAreas: result.markAreas
|
||||
};
|
||||
|
||||
const dailyDates = (dailyJson.daily.time as number[]).map(
|
||||
(t: number) => new Date((t + (dailyJson.utc_offset_seconds ?? 0)) * 1000)
|
||||
);
|
||||
|
||||
fetchedDaily = {
|
||||
daily: dailyJson.daily,
|
||||
dailyDates
|
||||
daily: result.daily,
|
||||
dailyDates: result.dailyDates
|
||||
};
|
||||
|
||||
loading = false;
|
||||
|
||||
Reference in New Issue
Block a user