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",
|
"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(
|
const guangzhouRunwayWithBadMadisChart = buildFullDayChartData(
|
||||||
{
|
{
|
||||||
city: "guangzhou",
|
city: "guangzhou",
|
||||||
|
|||||||
@@ -901,6 +901,16 @@ function canonicalAirportPrimarySourceLabel(hourly: ChartRenderState) {
|
|||||||
return "";
|
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(
|
function airportCodeForSeriesLabel(
|
||||||
hourly: ChartRenderState,
|
hourly: ChartRenderState,
|
||||||
row?: ScanOpportunityRow | null,
|
row?: ScanOpportunityRow | null,
|
||||||
@@ -943,12 +953,15 @@ function airportPrimarySeriesLabel(
|
|||||||
const cityKey = normalizeCityKey(row?.city);
|
const cityKey = normalizeCityKey(row?.city);
|
||||||
const canonicalLabel = canonicalAirportPrimarySourceLabel(hourly);
|
const canonicalLabel = canonicalAirportPrimarySourceLabel(hourly);
|
||||||
if (canonicalLabel === "MGM") return canonicalLabel;
|
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")) {
|
if ((cityKey === "ankara" || cityKey === "istanbul") && (!canonicalLabel || canonicalLabel === "NOAA MADIS")) {
|
||||||
return "MGM";
|
return "MGM";
|
||||||
}
|
}
|
||||||
const payloadLabel = String(hourly?.airportPrimary?.source_label || "").trim();
|
const payloadLabel = String(hourly?.airportPrimary?.source_label || "").trim();
|
||||||
if (payloadLabel && !isGenericAirportPrimaryLabel(payloadLabel)) return payloadLabel;
|
if (payloadLabel && !isGenericAirportPrimaryLabel(payloadLabel)) return payloadLabel;
|
||||||
const stationCode = airportCodeForSeriesLabel(hourly, row);
|
|
||||||
const isUsAirport = isUsAirportCode(stationCode);
|
const isUsAirport = isUsAirportCode(stationCode);
|
||||||
if (!isUsAirport) {
|
if (!isUsAirport) {
|
||||||
if (canonicalLabel && canonicalLabel !== "NOAA MADIS") return canonicalLabel;
|
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(
|
function mergeAirportCondition(
|
||||||
base: AirportCurrentConditions | null | undefined,
|
base: AirportCurrentConditions | null | undefined,
|
||||||
live: AirportCurrentConditions | null | undefined,
|
live: AirportCurrentConditions | null | undefined,
|
||||||
@@ -1338,6 +1362,19 @@ function mergeAirportCondition(
|
|||||||
return merged;
|
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>) {
|
function runwayHistoryPointKey(point: Record<string, unknown>) {
|
||||||
const time = String(
|
const time = String(
|
||||||
point.timestamp ??
|
point.timestamp ??
|
||||||
@@ -1677,6 +1714,10 @@ function mergeHourlyWithLiveObservations(
|
|||||||
runway_plate_history: runwayPlateHistory,
|
runway_plate_history: runwayPlateHistory,
|
||||||
} as AmosData
|
} as AmosData
|
||||||
: detailSource.amos;
|
: detailSource.amos;
|
||||||
|
const useLiveAirportPrimary =
|
||||||
|
airportPrimaryObservationSourceChanged(base, live) &&
|
||||||
|
Array.isArray(live.airportPrimaryTodayObs) &&
|
||||||
|
live.airportPrimaryTodayObs.length > 0;
|
||||||
return {
|
return {
|
||||||
...detailSource,
|
...detailSource,
|
||||||
localDate,
|
localDate,
|
||||||
@@ -1696,16 +1737,22 @@ function mergeHourlyWithLiveObservations(
|
|||||||
: forecastFallback.probabilities || null,
|
: forecastFallback.probabilities || null,
|
||||||
runwayPlateHistory,
|
runwayPlateHistory,
|
||||||
amos,
|
amos,
|
||||||
airportCurrent: mergeAirportCondition(base.airportCurrent, live.airportCurrent, row, localDate),
|
airportCurrent: useLiveAirportPrimary
|
||||||
airportPrimary: mergeAirportCondition(base.airportPrimary, live.airportPrimary, row, localDate),
|
? 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,
|
settlementTodayObs: mergeRawObservationPoints(base.settlementTodayObs, live.settlementTodayObs) as ObsPoint[] | undefined,
|
||||||
settlementStationCode: detailSource.settlementStationCode || forecastFallback.settlementStationCode || row?.metar_context?.station || null,
|
settlementStationCode: detailSource.settlementStationCode || forecastFallback.settlementStationCode || row?.metar_context?.station || null,
|
||||||
settlementStationLabel: detailSource.settlementStationLabel || forecastFallback.settlementStationLabel || null,
|
settlementStationLabel: detailSource.settlementStationLabel || forecastFallback.settlementStationLabel || null,
|
||||||
metarTodayObs: mergeRawObservationPoints(base.metarTodayObs, live.metarTodayObs) as ObsPoint[] | undefined,
|
metarTodayObs: mergeRawObservationPoints(base.metarTodayObs, live.metarTodayObs) as ObsPoint[] | undefined,
|
||||||
airportPrimaryTodayObs: mergeRawObservationPoints(
|
airportPrimaryTodayObs: useLiveAirportPrimary
|
||||||
base.airportPrimaryTodayObs,
|
? live.airportPrimaryTodayObs
|
||||||
live.airportPrimaryTodayObs,
|
: mergeRawObservationPoints(
|
||||||
),
|
base.airportPrimaryTodayObs,
|
||||||
|
live.airportPrimaryTodayObs,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user