feat: local time (#7)

Co-authored-by: terraputix <terraputix@mailbox.org>
Reviewed-on: useweb/open-meteo-blue#7
This commit was merged in pull request #7.
This commit is contained in:
2026-02-16 11:41:34 +01:00
co-authored by terraputix
parent 381ffaf3f6
commit 9946cd5851
16 changed files with 948 additions and 701 deletions
+27 -17
View File
@@ -39,18 +39,18 @@ export function range(start: number, stop: number, step: number): number[] {
/**
* Extracts timestamp array (in milliseconds, with UTC offset applied) from a VariablesWithTime block.
*/
export function getTimestamps(timeBlock: VariablesWithTime, utcOffsetSeconds: number): number[] {
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 + utcOffsetSeconds) * 1000);
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, utcOffsetSeconds: number): Date[] {
return getTimestamps(timeBlock, utcOffsetSeconds).map((t) => new Date(t));
export function getDates(timeBlock: VariablesWithTime): Date[] {
return getTimestamps(timeBlock).map((t) => new Date(t));
}
/**
@@ -148,6 +148,7 @@ export function unitToDisplayString(unit: Unit): string {
export interface WeatherLocation {
latitude: number;
longitude: number;
timezone?: string;
}
export interface WeatherUnitParams {
@@ -164,7 +165,6 @@ export interface WeekForecastParams extends WeatherLocation, WeatherUnitParams {
model?: string;
forecast_days?: number;
past_days?: number;
timezone?: string;
}
export interface WeekHourlyData {
@@ -197,6 +197,7 @@ export interface WeekForecastResult {
hourly: WeekHourlyData;
daily: WeekDailyData;
utcOffsetSeconds: number;
timezone: string;
hourlyTimestamps: number[];
hourlyDates: Date[];
dailyDates: Date[];
@@ -219,6 +220,7 @@ export interface ModelCompareResult {
models: ModelSeriesData[];
timestamps: number[];
utcOffsetSeconds: number;
timezone: string;
markAreas: MarkArea[];
units: Record<string, string>;
/** Flat record compatible with the existing chart utilities (keys like "temperature_2m_icon_seamless") */
@@ -246,6 +248,7 @@ 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[]>;
@@ -314,12 +317,13 @@ export async function fetchWeekForecast(params: WeekForecastParams): Promise<Wee
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, utcOffsetSeconds);
const hourlyTimestamps = getTimestamps(hourlyBlock);
const hourlyDates = hourlyTimestamps.map((t) => new Date(t));
const hourly: WeekHourlyData = {
@@ -336,7 +340,7 @@ export async function fetchWeekForecast(params: WeekForecastParams): Promise<Wee
};
// Daily: variables are in the same order as WEEK_DAILY_VARS
const dailyDates = getDates(dailyBlock, utcOffsetSeconds);
const dailyDates = getDates(dailyBlock);
const sunriseVar = dailyBlock.variables(3)!;
const sunsetVar = dailyBlock.variables(4)!;
@@ -354,12 +358,13 @@ export async function fetchWeekForecast(params: WeekForecastParams): Promise<Wee
winddirection_10m_dominant: getValues(dailyBlock.variables(9)!)
};
const markAreas = buildDaylightMarkAreas(daily.sunrise, daily.sunset, utcOffsetSeconds);
const markAreas = buildDaylightMarkAreas(daily.sunrise, daily.sunset);
return {
hourly,
daily,
utcOffsetSeconds,
timezone,
hourlyTimestamps,
hourlyDates,
dailyDates,
@@ -379,7 +384,7 @@ export async function fetchWeekForecast(params: WeekForecastParams): Promise<Wee
export async function fetchModelComparison(
params: ModelCompareParams
): Promise<ModelCompareResult> {
const forecastApiParams: Record<string, string> = {
const forecastApiParams: Record<string, string | number | undefined> = {
latitude: String(params.latitude),
longitude: String(params.longitude),
hourly: params.hourlyVariables.join(','),
@@ -387,7 +392,8 @@ export async function fetchModelComparison(
daily: 'sunrise,sunset',
temperature_unit: params.temperature_unit ?? 'celsius',
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
precipitation_unit: params.precipitation_unit ?? 'mm'
precipitation_unit: params.precipitation_unit ?? 'mm',
timezone: params.timezone
};
const responses = await fetchWeatherApi(FORECAST_URL, forecastApiParams);
@@ -395,10 +401,10 @@ export async function fetchModelComparison(
// With multiple models, we get one response per model
const firstResponse = responses[0];
const utcOffsetSeconds = firstResponse.utcOffsetSeconds();
const timezone = firstResponse.timezone() ?? params.timezone ?? 'UTC';
// Extract timestamps from the first response's hourly block
const hourlyBlock = firstResponse.hourly()!;
const timestamps = getTimestamps(hourlyBlock, utcOffsetSeconds);
const timestamps = getTimestamps(hourlyBlock);
// Extract sunrise/sunset from the first response's daily block
let markAreas: MarkArea[] = [];
@@ -408,7 +414,7 @@ export async function fetchModelComparison(
const sunsetVar = dailyBlock.variables(1)!;
const sunrise = getInt64Values(sunriseVar);
const sunset = getInt64Values(sunsetVar);
markAreas = buildDaylightMarkAreas(sunrise, sunset, utcOffsetSeconds);
markAreas = buildDaylightMarkAreas(sunrise, sunset);
}
// Process each model's response
@@ -463,6 +469,7 @@ export async function fetchModelComparison(
models,
timestamps,
utcOffsetSeconds,
timezone,
markAreas,
units,
hourlyFlat,
@@ -484,7 +491,7 @@ export async function fetchEnsembleForecast(
): Promise<EnsembleForecastResult> {
const forecastDays = params.forecast_days ?? 14;
const ensembleParams: Record<string, string> = {
const ensembleParams: Record<string, string | number | undefined> = {
latitude: String(params.latitude),
longitude: String(params.longitude),
hourly: params.hourlyVariables.join(','),
@@ -492,7 +499,8 @@ export async function fetchEnsembleForecast(
forecast_days: String(forecastDays),
temperature_unit: params.temperature_unit ?? 'celsius',
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
precipitation_unit: params.precipitation_unit ?? 'mm'
precipitation_unit: params.precipitation_unit ?? 'mm',
timezone: params.timezone
};
const dailyParams: Record<string, string> = {
@@ -511,9 +519,10 @@ export async function fetchEnsembleForecast(
const ensembleResponse = ensembleResponses[0];
const utcOffsetSeconds = ensembleResponse.utcOffsetSeconds();
const timezone = ensembleResponse.timezone() ?? params.timezone ?? 'UTC';
const hourlyBlock = ensembleResponse.hourly()!;
const timestamps = getTimestamps(hourlyBlock, utcOffsetSeconds);
const timestamps = getTimestamps(hourlyBlock);
const timeLength = timestamps.length;
// Extract sunrise/sunset for mark areas
@@ -524,7 +533,7 @@ export async function fetchEnsembleForecast(
if (dailyBlock) {
const sunrise = getInt64Values(dailyBlock.variables(0)!);
const sunset = getInt64Values(dailyBlock.variables(1)!);
markAreas = buildDaylightMarkAreas(sunrise, sunset, dailyResponse.utcOffsetSeconds());
markAreas = buildDaylightMarkAreas(sunrise, sunset);
}
}
@@ -613,6 +622,7 @@ export async function fetchEnsembleForecast(
variables,
timestamps,
utcOffsetSeconds,
timezone,
markAreas,
hourlyFlat,
hourlyUnitsFlat