Fix Ankara chart stale MGM observations

This commit is contained in:
2569718930@qq.com
2026-06-23 19:02:31 +08:00
parent 221edb480c
commit b8a1950932
2 changed files with 135 additions and 7 deletions
@@ -803,6 +803,87 @@ export function runTests() {
"Ankara scan-row-seeded airport-primary curve should default to MGM instead of NOAA MADIS when source metadata is missing",
);
const staleAnkaraDetail = toFullChartDetail({
localDate: "2026-06-23",
localTime: "13:51",
times: ["12:00", "13:00", "14:00", "15:00"],
temps: [22, 24, 25, 24],
airportPrimary: {
temp: 27.1,
obs_time: "2026-06-23T10:39:00Z",
source_code: "mgm",
source_label: "MGM",
},
airportPrimaryTodayObs: [["2026-06-23T10:39:00Z", 27.1]],
metarTodayObs: [["2026-06-23T10:20:00Z", 24.0]],
debHourlyPath: {
times: ["12:00", "13:00", "14:00", "15:00"],
temps: [20.5, 21.0, 21.2, 21.4],
},
modelTimes: ["12:00", "13:00", "14:00", "15:00"],
modelCurves: { ECMWF: [23.8, 24.4, 24.7, 24.0] },
} as any);
const freshAnkaraObservation = observationPayloadToSnapshot({
city: "ankara",
name: "ankara",
display_name: "Ankara",
local_date: "2026-06-23",
local_time: "13:30",
utc_offset_seconds: 3 * 60 * 60,
current: {
temp: 24.5,
source_code: "metar",
source_label: "METAR",
settlement_source: "metar",
settlement_source_label: "METAR",
station_code: "LTAC",
obs_time: "2026-06-23T10:30:00Z",
},
airport_current: {
temp: 24.5,
source_code: "metar",
source_label: "METAR",
station_code: "LTAC",
obs_time: "2026-06-23T10:30:00Z",
},
airport_primary: {
temp: 24.5,
source_code: "metar",
source_label: "METAR",
station_code: "LTAC",
obs_time: "2026-06-23T10:30:00Z",
},
timeseries: {
metar_today_obs: [{ time: "13:30", temp: 24.5 }],
},
metar_today_obs: [{ time: "13:30", temp: 24.5 }],
} as any);
const refreshedAnkaraDetail = mergeObservationSnapshotIntoHourly(
staleAnkaraDetail,
freshAnkaraObservation,
);
const refreshedAnkaraChart = buildFullDayChartData(
{
city: "ankara",
local_date: "2026-06-23",
local_time: "13:51",
tz_offset_seconds: 3 * 60 * 60,
airport: "LTAC",
temp_symbol: "°C",
} as any,
refreshedAnkaraDetail,
false,
);
const refreshedAnkaraAirportSeries = refreshedAnkaraChart.series.find((item) => item.key === "madis");
assert(
!refreshedAnkaraAirportSeries?.values.some((value) => value === 27.1),
"Ankara live observation refresh should discard stale cached MGM airport-primary points when the latest source is METAR",
);
assert(
refreshedAnkaraAirportSeries?.label === "LTAC METAR",
`Ankara refreshed airport-primary curve should be labelled as LTAC METAR, got ${refreshedAnkaraAirportSeries?.label}`,
);
const guangzhouRunwayWithBadMadisChart = buildFullDayChartData(
{
city: "guangzhou",
@@ -901,6 +901,16 @@ function canonicalAirportPrimarySourceLabel(hourly: ChartRenderState) {
return "";
}
function airportPrimaryHasMetarSource(hourly: ChartRenderState) {
const primary = hourly?.airportPrimary;
const tokens = [
primary?.source_code,
primary?.source_label,
(primary as any)?.source,
].map((value) => String(value || "").trim().toLowerCase());
return tokens.some((value) => value === "metar" || value.includes(" metar"));
}
function airportCodeForSeriesLabel(
hourly: ChartRenderState,
row?: ScanOpportunityRow | null,
@@ -943,12 +953,15 @@ function airportPrimarySeriesLabel(
const cityKey = normalizeCityKey(row?.city);
const canonicalLabel = canonicalAirportPrimarySourceLabel(hourly);
if (canonicalLabel === "MGM") return canonicalLabel;
const stationCode = airportCodeForSeriesLabel(hourly, row);
if (airportPrimaryHasMetarSource(hourly)) {
return stationCode ? `${stationCode} METAR` : "METAR";
}
if ((cityKey === "ankara" || cityKey === "istanbul") && (!canonicalLabel || canonicalLabel === "NOAA MADIS")) {
return "MGM";
}
const payloadLabel = String(hourly?.airportPrimary?.source_label || "").trim();
if (payloadLabel && !isGenericAirportPrimaryLabel(payloadLabel)) return payloadLabel;
const stationCode = airportCodeForSeriesLabel(hourly, row);
const isUsAirport = isUsAirportCode(stationCode);
if (!isUsAirport) {
if (canonicalLabel && canonicalLabel !== "NOAA MADIS") return canonicalLabel;
@@ -1316,6 +1329,17 @@ function conditionObservationTime(source: AirportCurrentConditions | null | unde
);
}
function conditionObservationSourceKey(source: AirportCurrentConditions | null | undefined) {
const tokens = [
source?.source_code,
(source as any)?.source,
source?.source_label,
].map((value) => String(value || "").trim().toLowerCase());
if (tokens.some((value) => value === "mgm" || value.includes("turkey_mgm"))) return "mgm";
if (tokens.some((value) => value === "metar" || value.includes(" metar"))) return "metar";
return tokens.find(Boolean) || "";
}
function mergeAirportCondition(
base: AirportCurrentConditions | null | undefined,
live: AirportCurrentConditions | null | undefined,
@@ -1338,6 +1362,19 @@ function mergeAirportCondition(
return merged;
}
function airportPrimaryObservationSourceChanged(
base: ChartRenderState,
live: ChartRenderState,
) {
const baseSource =
conditionObservationSourceKey(base?.airportPrimary) ||
conditionObservationSourceKey(base?.airportCurrent);
const liveSource =
conditionObservationSourceKey(live?.airportPrimary) ||
conditionObservationSourceKey(live?.airportCurrent);
return Boolean(baseSource && liveSource && baseSource !== liveSource);
}
function runwayHistoryPointKey(point: Record<string, unknown>) {
const time = String(
point.timestamp ??
@@ -1677,6 +1714,10 @@ function mergeHourlyWithLiveObservations(
runway_plate_history: runwayPlateHistory,
} as AmosData
: detailSource.amos;
const useLiveAirportPrimary =
airportPrimaryObservationSourceChanged(base, live) &&
Array.isArray(live.airportPrimaryTodayObs) &&
live.airportPrimaryTodayObs.length > 0;
return {
...detailSource,
localDate,
@@ -1696,16 +1737,22 @@ function mergeHourlyWithLiveObservations(
: forecastFallback.probabilities || null,
runwayPlateHistory,
amos,
airportCurrent: mergeAirportCondition(base.airportCurrent, live.airportCurrent, row, localDate),
airportPrimary: mergeAirportCondition(base.airportPrimary, live.airportPrimary, row, localDate),
airportCurrent: useLiveAirportPrimary
? live.airportCurrent || live.airportPrimary || null
: mergeAirportCondition(base.airportCurrent, live.airportCurrent, row, localDate),
airportPrimary: useLiveAirportPrimary
? live.airportPrimary || live.airportCurrent || null
: mergeAirportCondition(base.airportPrimary, live.airportPrimary, row, localDate),
settlementTodayObs: mergeRawObservationPoints(base.settlementTodayObs, live.settlementTodayObs) as ObsPoint[] | undefined,
settlementStationCode: detailSource.settlementStationCode || forecastFallback.settlementStationCode || row?.metar_context?.station || null,
settlementStationLabel: detailSource.settlementStationLabel || forecastFallback.settlementStationLabel || null,
metarTodayObs: mergeRawObservationPoints(base.metarTodayObs, live.metarTodayObs) as ObsPoint[] | undefined,
airportPrimaryTodayObs: mergeRawObservationPoints(
base.airportPrimaryTodayObs,
live.airportPrimaryTodayObs,
),
airportPrimaryTodayObs: useLiveAirportPrimary
? live.airportPrimaryTodayObs
: mergeRawObservationPoints(
base.airportPrimaryTodayObs,
live.airportPrimaryTodayObs,
),
};
}