Avoid stale IMGW Hybrid current readings
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import { toNumber } from "@/lib/weather-utils";
|
||||
import type { ImgwCurrentWeather, RawImgwHybridWeatherResponse, RawImgwHybridWeatherRow } from "@/types/imgw-current";
|
||||
|
||||
const STALE_TEN_MINUTE_THRESHOLD_MS = 2 * 60 * 60 * 1000;
|
||||
|
||||
interface NormalizedHybridRow {
|
||||
row: RawImgwHybridWeatherRow;
|
||||
measuredAt: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
function toCelsius(value: unknown) {
|
||||
const temperature = toNumber(value);
|
||||
if (temperature === null) return null;
|
||||
@@ -19,6 +27,13 @@ function normalizeDate(value: unknown) {
|
||||
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
||||
}
|
||||
|
||||
function normalizeHybridRow(candidate: RawImgwHybridWeatherRow): NormalizedHybridRow | null {
|
||||
const measuredAt = normalizeDate(candidate.Date);
|
||||
if (!measuredAt) return null;
|
||||
const timestamp = new Date(measuredAt).getTime();
|
||||
return Number.isNaN(timestamp) ? null : { row: candidate, measuredAt, timestamp };
|
||||
}
|
||||
|
||||
function getWeatherCode(iconCode: unknown) {
|
||||
if (typeof iconCode !== "string") return null;
|
||||
const match = iconCode.match(/z(\d{2})/i);
|
||||
@@ -48,43 +63,58 @@ function hasPrecipitationValue(candidate: RawImgwHybridWeatherRow) {
|
||||
return hasNumericValue(candidate.Precipitation10m) || hasNumericValue(candidate.Rain10m) || hasNumericValue(candidate.Snow10m);
|
||||
}
|
||||
|
||||
function pickFullWeatherRow(rows: NormalizedHybridRow[]) {
|
||||
const tenMinuteRow = rows.find((candidate) => candidate.row.Type === "Type_Ten_Minutes" && isFullWeatherRow(candidate.row));
|
||||
const hourlyRow = rows.find((candidate) => candidate.row.Type === "Type_Hour" && isFullWeatherRow(candidate.row));
|
||||
if (!tenMinuteRow) return hourlyRow ?? null;
|
||||
if (!hourlyRow) return tenMinuteRow;
|
||||
return hourlyRow.timestamp - tenMinuteRow.timestamp > STALE_TEN_MINUTE_THRESHOLD_MS ? hourlyRow : tenMinuteRow;
|
||||
}
|
||||
|
||||
function pickPrecipitationRow(rows: NormalizedHybridRow[], fullRow: NormalizedHybridRow | null) {
|
||||
if (fullRow && hasPrecipitationValue(fullRow.row)) return fullRow;
|
||||
const precipitationRows = rows.filter((candidate) => candidate.row.Type === "Type_Ten_Minutes" && hasPrecipitationValue(candidate.row));
|
||||
if (!precipitationRows.length) return null;
|
||||
if (!fullRow) return precipitationRows[0];
|
||||
return precipitationRows.reduce((best, candidate) => (
|
||||
Math.abs(candidate.timestamp - fullRow.timestamp) < Math.abs(best.timestamp - fullRow.timestamp) ? candidate : best
|
||||
));
|
||||
}
|
||||
|
||||
export function normalizeImgwCurrentWeather(payload: RawImgwHybridWeatherResponse): ImgwCurrentWeather | null {
|
||||
if (!payload.data?.Valid || !Array.isArray(payload.data.Data)) return null;
|
||||
|
||||
const rows = payload.data.Data
|
||||
.filter((candidate): candidate is RawImgwHybridWeatherRow => {
|
||||
if (!candidate || typeof candidate !== "object") return false;
|
||||
return (candidate.Type === "Type_Ten_Minutes" || candidate.Type === "Type_Hour") && normalizeDate(candidate.Date) !== null;
|
||||
})
|
||||
.sort((left, right) => String(left.Date).localeCompare(String(right.Date)));
|
||||
const fullRow = rows.find((candidate) => candidate.Type === "Type_Ten_Minutes" && isFullWeatherRow(candidate))
|
||||
?? rows.find((candidate) => candidate.Type === "Type_Hour" && isFullWeatherRow(candidate));
|
||||
const precipitationRow = fullRow && hasPrecipitationValue(fullRow)
|
||||
? fullRow
|
||||
: rows.find((candidate) => candidate.Type === "Type_Ten_Minutes" && hasPrecipitationValue(candidate));
|
||||
.flatMap((candidate): NormalizedHybridRow[] => {
|
||||
if (!candidate || typeof candidate !== "object") return [];
|
||||
const row = candidate as RawImgwHybridWeatherRow;
|
||||
if (row.Type !== "Type_Ten_Minutes" && row.Type !== "Type_Hour") return [];
|
||||
const normalizedRow = normalizeHybridRow(row);
|
||||
return normalizedRow ? [normalizedRow] : [];
|
||||
});
|
||||
const fullRow = pickFullWeatherRow(rows);
|
||||
const precipitationRow = pickPrecipitationRow(rows, fullRow);
|
||||
const row = fullRow ?? precipitationRow;
|
||||
if (!row) return null;
|
||||
|
||||
const measuredAt = normalizeDate(row.Date);
|
||||
if (!measuredAt) return null;
|
||||
const precipitationSource = precipitationRow ?? row;
|
||||
const precipitationSource = precipitationRow?.row ?? row.row;
|
||||
const rainfall10m = toNumber(precipitationSource.Rain10m);
|
||||
const snowfall10m = toNumber(precipitationSource.Snow10m);
|
||||
const weatherCode = getWeatherCode(row.Icon10);
|
||||
const weatherCode = getWeatherCode(row.row.Icon10);
|
||||
|
||||
return {
|
||||
coverage: fullRow?.Type === "Type_Ten_Minutes" ? "full" : fullRow ? "hourly" : "precipitation-only",
|
||||
measuredAt,
|
||||
temperature: toCelsius(row.Temperature),
|
||||
feelsLike: toCelsius(row.Chill),
|
||||
windSpeed: toNumber(row.Wind_Speed),
|
||||
windDirection: toNumber(row.Wind_Dir),
|
||||
humidity: toNumber(row.Humidity),
|
||||
pressure: toHectopascals(row.PressureMSL),
|
||||
coverage: fullRow?.row.Type === "Type_Ten_Minutes" ? "full" : fullRow ? "hourly" : "precipitation-only",
|
||||
measuredAt: row.measuredAt,
|
||||
temperature: toCelsius(row.row.Temperature),
|
||||
feelsLike: toCelsius(row.row.Chill),
|
||||
windSpeed: toNumber(row.row.Wind_Speed),
|
||||
windDirection: toNumber(row.row.Wind_Dir),
|
||||
humidity: toNumber(row.row.Humidity),
|
||||
pressure: toHectopascals(row.row.PressureMSL),
|
||||
precipitation10m: toNumber(precipitationSource.Precipitation10m),
|
||||
rainfall10m,
|
||||
snowfall10m,
|
||||
cloudCover: toNumber(row.Cloud),
|
||||
cloudCover: toNumber(row.row.Cloud),
|
||||
weatherCode,
|
||||
condition: getCondition(weatherCode, rainfall10m, snowfall10m),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user