fix: use settlement runway endpoint temperatures

This commit is contained in:
2569718930@qq.com
2026-05-28 10:55:32 +08:00
parent d83a0f0eef
commit 79b82a34cb
7 changed files with 367 additions and 86 deletions
@@ -438,37 +438,39 @@ export function runTests() {
);
assert(hongKongHkoSeries?.label === "HKO", "Hong Kong HKO settlement observations should remain visible as the HKO curve");
const chengduFromAmosSnapshot = __buildTemperatureChartDataForTest(
{
city: "chengdu",
local_date: "2026-05-26",
local_time: "05:25",
tz_offset_seconds: 8 * 60 * 60,
airport: "ZUUU",
} as any,
{
localTime: "05:25",
times: ["00:00", "06:00", "12:00", "18:00"],
temps: [24, 28, 31, 27],
amos: {
observation_time: "2026-05-25T21:25:00+00:00",
observation_time_local: "2026-05-26 05:25:00",
runway_obs: {
runway_pairs: [
["02L", "20R"],
["02R", "20L"],
],
temperatures: [
[24.4, null],
[24.2, null],
],
point_temperatures: [
{ runway: "02L/20R", tdz_temp: 24.4, mid_temp: null, end_temp: 24.8 },
{ runway: "02R/20L", tdz_temp: 24.2, mid_temp: null, end_temp: 24.6 },
],
},
const chengduRow = {
city: "chengdu",
local_date: "2026-05-26",
local_time: "05:25",
tz_offset_seconds: 8 * 60 * 60,
airport: "ZUUU",
} as any;
const chengduSnapshotHourly = {
localTime: "05:25",
times: ["00:00", "06:00", "12:00", "18:00"],
temps: [24, 28, 31, 27],
amos: {
observation_time: "2026-05-25T21:25:00+00:00",
observation_time_local: "2026-05-26 05:25:00",
runway_obs: {
runway_pairs: [
["02L", "20R"],
["02R", "20L"],
],
temperatures: [
[24.4, null],
[24.2, null],
],
point_temperatures: [
{ runway: "02L/20R", tdz_temp: 24.4, mid_temp: null, end_temp: 24.8 },
{ runway: "02R/20L", tdz_temp: 24.2, mid_temp: null, end_temp: 24.6 },
],
},
} as any,
},
} as any;
const chengduFromAmosSnapshot = __buildTemperatureChartDataForTest(
chengduRow,
chengduSnapshotHourly,
"1D",
);
@@ -477,6 +479,14 @@ export function runTests() {
assert(chengduSettlementRunway.color === "#009688", "AMOS snapshot settlement runway should use highlight cyan");
assert(chengduSettlementRunway.featured === true, "AMOS snapshot settlement runway should be featured");
assert(!chengduSettlementRunway.dashed, "AMOS snapshot settlement runway should be solid");
const chengduEndpointMetrics = __getObservationDisplayMetricsForTest(
chengduRow,
chengduSnapshotHourly,
);
assert(
chengduEndpointMetrics.currentRunwayTemp === 24.4,
"Chengdu 02L settlement should use TDZ temperature from 02L/20R, not runway max/end temperature",
);
const chengduAuxRunway = seriesByKey(chengduFromAmosSnapshot.series, "runway_02R_20L") as any;
assert(chengduAuxRunway, "AMOS runway_obs snapshot should create auxiliary runway chart lines");
@@ -23,10 +23,21 @@ const SETTLEMENT_RUNWAY_PAIRS: Record<string, Array<[string, string]>> = {
chengdu: [["02L", "20R"]],
chongqing: [["20R", "02L"]],
wuhan: [["04", "22"]],
qingdao: [["16", "34"]],
seoul: [["15R", "33L"]],
busan: [["SR", "SL"]],
};
const SETTLEMENT_RUNWAY_TARGETS: Record<string, string> = {
shanghai: "35R",
beijing: "01",
guangzhou: "02L",
chengdu: "02L",
chongqing: "02L",
wuhan: "04",
qingdao: "34",
};
function normalizeRunwayLabel(value?: string | null) {
return String(value || "").trim().toUpperCase().replace(/\s+/g, "");
}
@@ -43,6 +54,21 @@ function pairKey(pair: [string, string]) {
return pair.map(normalizeRunwayLabel).sort().join("/");
}
function settlementEndpointTempForPair(
cityKey: string,
pair: [string, string],
tdz: number | null,
end: number | null,
) {
const target = normalizeRunwayLabel(SETTLEMENT_RUNWAY_TARGETS[cityKey]);
if (!target) return null;
const first = normalizeRunwayLabel(pair[0]);
const second = normalizeRunwayLabel(pair[1]);
if (target === first) return tdz ?? end;
if (target === second) return end ?? tdz;
return null;
}
function runwaySeriesKey(rwy: string) {
return `runway_${String(rwy || "unknown")
.split("/")
@@ -138,10 +164,16 @@ function buildRunwayPlates(
const isSettlement = settlementKeys.has(pairKey(pair));
const pointTemp = pointTemps[index] as any;
const aggregateRunwayTemp = validNumber(pointTemp?.temp) ?? validNumber(pointTemp?.target_runway_max);
const tdz = validNumber(pointTemp?.tdz_temp);
const mid = validNumber(pointTemp?.mid_temp);
const end = validNumber(pointTemp?.end_temp);
const endpointTemp = isSettlement
? settlementEndpointTempForPair(cityKey, pair, tdz, end)
: null;
const aggregateRunwayTemp =
endpointTemp ??
validNumber(pointTemp?.temp) ??
validNumber(pointTemp?.target_runway_max);
const isAmosTempDewTuple = String(amos.source || "").toLowerCase() === "amos";
const historyVals = !isAmosTempDewTuple && Array.isArray(runwayTemps[index])
@@ -152,7 +184,9 @@ function buildRunwayPlates(
const tdzVal = tdz !== null ? [tdz] : [];
const midVal = mid !== null ? [mid] : [];
const endVal = end !== null ? [end] : [];
const allVals = [...historyVals, ...aggregateVal, ...tdzVal, ...midVal, ...endVal];
const allVals = isSettlement && endpointTemp !== null
? [...historyVals, endpointTemp]
: [...historyVals, ...aggregateVal, ...tdzVal, ...midVal, ...endVal];
const maxTemp = allVals.length ? Math.max(...allVals) : null;
const dailyHigh = historyVals.length ? Math.max(...historyVals) : maxTemp;
@@ -1167,18 +1201,29 @@ function buildRunwayHistorySeries(
return runwayTemps
.map((rawTemps, index) => {
if (!Array.isArray(rawTemps)) return null;
const rwy = runwayLabelFromPair(runwayPairs[index], index);
const rawPair = runwayPairs[index];
const rwy = runwayLabelFromPair(rawPair, index);
const isSettlement = isSettlementRunway(row, rwy);
const pointTemp = Array.isArray(pointTemps) ? (pointTemps[index] as any) : null;
const pair = Array.isArray(rawPair) && rawPair.length >= 2
? [String(rawPair[0]), String(rawPair[1])] as [string, string]
: rwy.split("/").length >= 2
? [rwy.split("/")[0], rwy.split("/")[1]] as [string, string]
: [rwy, rwy] as [string, string];
const tdz = validNumber(pointTemp?.tdz_temp);
const mid = validNumber(pointTemp?.mid_temp);
const end = validNumber(pointTemp?.end_temp);
const endpointTemp = isSettlement
? settlementEndpointTempForPair(normalizeCityKey(row?.city), pair, tdz, end)
: null;
const aggregateRunwayTemp =
endpointTemp ??
validNumber(pointTemp?.temp) ??
validNumber(pointTemp?.target_runway_max) ??
(isAmosTempDewTuple ? runwayTemperatureFromPairTuple(rawTemps) : null);
const snapshotValues = [
aggregateRunwayTemp,
validNumber(pointTemp?.tdz_temp),
validNumber(pointTemp?.mid_temp),
validNumber(pointTemp?.end_temp),
...(isSettlement && endpointTemp !== null ? [] : [tdz, mid, end]),
].filter((value): value is number => value !== null);
const samples = isAmosTempDewTuple
? []