Fix Ankara chart stale MGM observations
This commit is contained in:
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user