Generalize NOAA settlement handling across dashboard and docs

This commit is contained in:
2569718930@qq.com
2026-03-27 20:58:38 +08:00
parent 15a452dd49
commit 7237278f5a
13 changed files with 374 additions and 51 deletions
@@ -549,10 +549,18 @@ export function FutureForecastModal() {
"--score-position": scorePosition,
} as CSSProperties & { "--score-position": string };
const weatherSummary = getWeatherSummary(detail, locale);
const isTaipeiNoaa =
store.selectedCity === "taipei" &&
(detail.current?.settlement_source === "noaa" ||
detail.current?.settlement_source_label === "NOAA");
const isNoaaSettlement =
detail.current?.settlement_source === "noaa" ||
detail.current?.settlement_source_label === "NOAA";
const noaaStationCode = String(
detail.current?.station_code || detail.risk?.icao || "NOAA",
)
.trim()
.toUpperCase();
const noaaStationName =
String(detail.current?.station_name || "").trim() ||
String(detail.risk?.airport || "").trim() ||
noaaStationCode;
const marketMidpoint = formatMarketPercent(
marketScan?.market_price ?? marketScan?.yes_token?.implied_probability,
);
@@ -810,9 +818,9 @@ export function FutureForecastModal() {
: "香港天文台 (HKO)"
: settlementSourceCode === "noaa"
? locale === "en-US"
? "NOAA RCTP (Taiwan Taoyuan International Airport)"
: "NOAA RCTP(台湾桃园国际机场)"
: settlementSourceCode === "cwa"
? `NOAA ${noaaStationCode} (${noaaStationName})`
: `NOAA ${noaaStationCode}${noaaStationName}`
: settlementSourceCode === "cwa"
? locale === "en-US"
? "Central Weather Administration (CWA)"
: "交通部中央气象署 (CWA)"
@@ -948,7 +956,7 @@ export function FutureForecastModal() {
</button>
</div>
<div className="modal-body future-modal-body">
{isTaipeiNoaa && (
{isNoaaSettlement && (
<div
style={{
marginBottom: "16px",
@@ -962,8 +970,8 @@ export function FutureForecastModal() {
}}
>
{locale === "en-US"
? "Taipei now settles against NOAA RCTP (Taiwan Taoyuan International Airport). The market uses the highest rounded whole-degree Celsius reading in the Temp column after the day is finalized."
: "台北当前按 NOAA RCTP(台湾桃园国际机场)结算。市场最终采用该日 Temp 列完成质控后的最高整度摄氏值,不按小数温度结算。"}
? `${detail.display_name} now settles against NOAA ${noaaStationCode} (${noaaStationName}). The market uses the highest rounded whole-degree Celsius reading in the Temp column after the day is finalized.`
: `${detail.display_name} 当前按 NOAA ${noaaStationCode}${noaaStationName})结算。市场最终采用该日 Temp 列完成质控后的最高整度摄氏值,不按小数温度结算。`}
</div>
)}
{isToday ? (
+31 -9
View File
@@ -12,7 +12,16 @@ function HistoryChart() {
const store = useDashboardStore();
const { locale } = useI18n();
const { data } = useHistoryData();
const isTaipei = store.selectedCity === "taipei";
const isNoaaSettlement =
store.selectedDetail?.current?.settlement_source === "noaa" ||
store.selectedDetail?.current?.settlement_source_label === "NOAA";
const noaaStationCode = String(
store.selectedDetail?.current?.station_code ||
store.selectedDetail?.risk?.icao ||
"NOAA",
)
.trim()
.toUpperCase();
const summary = useMemo(
() => getHistorySummary(data, store.selectedDetail?.local_date),
[data, store.selectedDetail?.local_date],
@@ -34,10 +43,10 @@ function HistoryChart() {
borderColor: "#f87171",
borderWidth: 2,
data: summary.actuals,
label: isTaipei
label: isNoaaSettlement
? locale === "en-US"
? "NOAA Settled High (RCTP)"
: "NOAA 结算最高温 (RCTP)"
? `NOAA Settled High (${noaaStationCode})`
: `NOAA 结算最高温 (${noaaStationCode})`
: locale === "en-US"
? "Observed High"
: "实测最高温",
@@ -142,7 +151,7 @@ function HistoryChart() {
},
type: "line",
} satisfies ChartConfiguration<"line">;
}, [hasBestBaseline, hasMgm, isTaipei, summary, locale]);
}, [hasBestBaseline, hasMgm, isNoaaSettlement, noaaStationCode, summary, locale]);
if (!summary.recentData.length) return null;
@@ -159,7 +168,20 @@ export function HistoryModal() {
const { data, error, isLoading, isOpen } = useHistoryData();
const isPro = store.proAccess.subscriptionActive;
const isProLoading = store.proAccess.loading;
const isTaipei = store.selectedCity === "taipei";
const isNoaaSettlement =
store.selectedDetail?.current?.settlement_source === "noaa" ||
store.selectedDetail?.current?.settlement_source_label === "NOAA";
const noaaStationCode = String(
store.selectedDetail?.current?.station_code ||
store.selectedDetail?.risk?.icao ||
"NOAA",
)
.trim()
.toUpperCase();
const noaaStationName =
String(store.selectedDetail?.current?.station_name || "").trim() ||
String(store.selectedDetail?.risk?.airport || "").trim() ||
noaaStationCode;
const summary = useMemo(
() => getHistorySummary(data, store.selectedDetail?.local_date),
[data, store.selectedDetail?.local_date],
@@ -221,7 +243,7 @@ export function HistoryModal() {
</button>
</div>
<div className="modal-body">
{isTaipei && (
{isNoaaSettlement && (
<div
style={{
marginBottom: "16px",
@@ -235,8 +257,8 @@ export function HistoryModal() {
}}
>
{t("lang") === "en-US"
? "Taipei historical actuals are aligned to NOAA RCTP settlement rules: use the highest rounded whole-degree Celsius reading after the date is finalized."
: "台北历史对账已按 NOAA RCTP 结算口径对齐:采用该日最终完成质控后的最高整度摄氏值。"}
? `${store.selectedDetail?.display_name || store.selectedCity || "This city"} historical actuals are aligned to NOAA ${noaaStationCode} (${noaaStationName}) settlement rules: use the highest rounded whole-degree Celsius reading after the date is finalized.`
: `${store.selectedDetail?.display_name || store.selectedCity || "该城市"}历史对账已按 NOAA ${noaaStationCode}${noaaStationName}结算口径对齐:采用该日最终完成质控后的最高整度摄氏值。`}
</div>
)}
<div className="history-stats">
+4 -4
View File
@@ -188,7 +188,7 @@ export const DOCS_PAGES: DocsPage[] = [
title: "什么叫机场端压温风险偏高",
blocks: [
{ type: "paragraph", text: "它的意思不是整座城市一定更冷,而是作为结算依据的机场站点,在峰值窗口里更可能因为云、阵雨或雷暴扰动,冲不到本来可能达到的更高温度。" },
{ type: "callout", tone: "warning", title: "重点区别", text: "TAF 负责告诉你机场侧未来几个小时会不会出现压温扰动,不直接等于结算温度本身。结算仍然看 METAR、HKO、MGM、NOAA RCTP、Wunderground 指定站点等实际结算源。" },
{ type: "callout", tone: "warning", title: "重点区别", text: "TAF 负责告诉你机场侧未来几个小时会不会出现压温扰动,不直接等于结算温度本身。结算仍然看 METAR、HKO、MGM、NOAA 指定站点、Wunderground 指定站点等实际结算源。" },
],
},
],
@@ -216,7 +216,7 @@ export const DOCS_PAGES: DocsPage[] = [
title: "What airport-side suppression risk means",
blocks: [
{ type: "paragraph", text: "It does not mean the entire city must run cooler. It means the airport station used for settlement is more likely to get capped by clouds, showers, or thunderstorm disruption during the peak window and fail to reach the next warmer bucket." },
{ type: "callout", tone: "warning", title: "Important distinction", text: "TAF explains whether the airport side may face suppressive weather over the next few hours. Settlement still comes from the actual settlement source such as METAR, HKO, MGM, NOAA RCTP, or a designated Wunderground station." },
{ type: "callout", tone: "warning", title: "Important distinction", text: "TAF explains whether the airport side may face suppressive weather over the next few hours. Settlement still comes from the actual settlement source such as METAR, HKO, MGM, a designated NOAA station, or a designated Wunderground station." },
],
},
],
@@ -242,7 +242,7 @@ export const DOCS_PAGES: DocsPage[] = [
id: "city-rules",
title: "当前主要口径",
blocks: [
{ type: "bullets", items: ["多数欧美机场市场:按机场 METAR 或机场主站实况结算。", "香港:按香港天文台 HKO 主口径,不接机场 TAF 作为主结算逻辑。", "台北:按 NOAA RCTP(台湾桃园国际机场)最终完成质控后的最高整度摄氏值结算,机场观测和市区体感不可混用。", "Shenzhen:当前按 Wunderground 指定站点(Shenzhen Bao'an Intl Airport Station)镜像结算页主值。", "Ankara:结算主站以 LTAC / Esenboğa 为准,同时保留 Turkish MGM 作为领先结构参考。"] },
{ type: "bullets", items: ["多数欧美机场市场:按机场 METAR 或机场主站实况结算。", "香港:按香港天文台 HKO 主口径,不接机场 TAF 作为主结算逻辑。", "台北、伊斯坦布尔等 NOAA 市场:按 weather.gov / NOAA 指定站点最终完成质控后的最高整度摄氏值结算,机场观测和市区体感不可混用。", "Shenzhen:当前按 Wunderground 指定站点(Shenzhen Bao'an Intl Airport Station)镜像结算页主值。", "Ankara:结算主站以 LTAC / Esenboğa 为准,同时保留 Turkish MGM 作为领先结构参考。"] },
],
},
{
@@ -269,7 +269,7 @@ export const DOCS_PAGES: DocsPage[] = [
id: "city-rules",
title: "Current primary rules",
blocks: [
{ type: "bullets", items: ["Most airport-linked Western markets: settle on airport METAR or the airport primary observing site.", "Hong Kong: settles on HKO, not on airport TAF as the main settlement logic.", "Taipei: settles against NOAA RCTP using the finalized highest rounded whole-degree Celsius reading; airport observations and downtown feel should not be mixed.", "Shenzhen: currently mirrors the designated Wunderground station value for Shenzhen Bao'an Intl Airport Station.", "Ankara: settlement centers on LTAC / Esenboğa, with Turkish MGM retained as a leading-structure reference."] },
{ type: "bullets", items: ["Most airport-linked Western markets: settle on airport METAR or the airport primary observing site.", "Hong Kong: settles on HKO, not on airport TAF as the main settlement logic.", "NOAA markets such as Taipei and Istanbul settle against the designated weather.gov / NOAA station using the finalized highest rounded whole-degree Celsius reading; airport observations and downtown feel should not be mixed.", "Shenzhen: currently mirrors the designated Wunderground station value for Shenzhen Bao'an Intl Airport Station.", "Ankara: settlement centers on LTAC / Esenboğa, with Turkish MGM retained as a leading-structure reference."] },
],
},
{
+1
View File
@@ -37,6 +37,7 @@ const CITY_MARKER_DISPLAY_OFFSETS: Record<
// Shek Kong sits between the Hong Kong and Shenzhen cards and gets visually buried
// by their wide marker bubbles. Shift only the rendered marker, not the true point.
"shek kong": { x: 34, y: -26, zIndexOffset: 320 },
"lau fau shan": { x: -40, y: 14, zIndexOffset: 300 },
};
function getMarkerDisplayOffset(cityName: string) {
@@ -75,6 +75,23 @@ const CITY_SPECIFIC_SOURCES: Record<string, OfficialSourceLink[]> = {
kind: "metar",
},
],
"lau fau shan": [
{
label: "香港天文台",
href: "https://www.hko.gov.hk/en/index.html",
kind: "agency",
},
{
label: "HKO 区域天气数据",
href: "https://data.weather.gov.hk/weatherAPI/hko_data/regional-weather/latest_1min_temperature.csv",
kind: "agency",
},
{
label: "HKO 实时读数页",
href: "https://www.hko.gov.hk/textonly/v2/forecast/text_readings_e.htm",
kind: "agency",
},
],
taipei: [
{
label: "NOAA RCTP Timeseries",
@@ -92,6 +109,23 @@ const CITY_SPECIFIC_SOURCES: Record<string, OfficialSourceLink[]> = {
kind: "metar",
},
],
istanbul: [
{
label: "NOAA LTFM Timeseries",
href: "https://www.weather.gov/wrh/timeseries?site=LTFM",
kind: "agency",
},
{
label: "Istanbul Airport",
href: "https://www.istairport.com/en",
kind: "airport",
},
{
label: "LTFM METAR",
href: "https://aviationweather.gov/data/metar/?id=LTFM&decoded=1&taf=1",
kind: "metar",
},
],
london: [
{
label: "Met Office",
+2
View File
@@ -50,6 +50,8 @@ export interface CurrentConditions {
wu_settlement: number | null;
settlement_source?: string | null;
settlement_source_label?: string | null;
station_code?: string | null;
station_name?: string | null;
obs_time: string | null;
obs_age_min: number | null;
wind_speed_kt: number | null;
+31 -8
View File
@@ -53,7 +53,13 @@ function getObservationSourceCode(detail: CityDetail): string {
const city = String(detail.name || detail.display_name || "")
.trim()
.toLowerCase();
if (city === "hong kong") return "hko";
if (
city === "hong kong" ||
city === "shek kong" ||
city === "lau fau shan"
) {
return "hko";
}
if (city === "taipei") return "noaa";
return "metar";
}
@@ -72,6 +78,20 @@ function getObservationSourceTag(detail: CityDetail): string {
return "METAR";
}
function getNoaaStationCode(detail: CityDetail): string {
return String(detail.current?.station_code || detail.risk?.icao || "NOAA")
.trim()
.toUpperCase();
}
function getNoaaStationName(detail: CityDetail): string {
return (
String(detail.current?.station_name || "").trim() ||
String(detail.risk?.airport || "").trim() ||
getNoaaStationCode(detail)
);
}
function normalizeCloudSummary(
cloudDesc: string | null | undefined,
locale: Locale,
@@ -297,7 +317,7 @@ export function getTemperatureChartData(
settlementSource && shouldUseMetarFallback
? metarFallbackTag
: observationCode === "noaa"
? "NOAA RCTP"
? `NOAA ${getNoaaStationCode(detail)}`
: observationCode === "wunderground"
? "Wunderground"
: observationTag;
@@ -574,14 +594,15 @@ export function getTemperatureChartData(
} else if (observationCode === "hko") {
legendParts.push(
isEnglish(locale)
? "Hong Kong uses HKO official readings. The chart keeps official HKO points instead of switching to airport METAR."
: "香港按 HKO 官方读数展示;图中保留 HKO 官方点位,不切换到机场 METAR 连续线。",
? "This city uses HKO official readings. The chart keeps official HKO points instead of switching to airport METAR."
: "该城市按 HKO 官方读数展示;图中保留 HKO 官方点位,不切换到机场 METAR 连续线。",
);
} else if (observationCode === "noaa") {
const noaaCode = getNoaaStationCode(detail);
legendParts.push(
isEnglish(locale)
? "Taipei settles on NOAA RCTP using the finalized highest rounded whole-degree Celsius reading; the plotted line is a settlement reference."
: "台北按 NOAA RCTP 最终完成质控后的最高整度摄氏结算;图中曲线仅作为结算参考线。",
? `This city settles on NOAA ${noaaCode} using the finalized highest rounded whole-degree Celsius Temp reading; the plotted line is a settlement reference.`
: `该城市按 NOAA ${noaaCode} 最终完成质控后的最高整度摄氏 Temp 读数结算;图中曲线仅作为结算参考线。`,
);
} else if (observationCode === "wunderground") {
legendParts.push(
@@ -2275,9 +2296,11 @@ export function getCityProfileStats(detail: CityDetail, locale: Locale = "zh-CN"
: "交通部中央气象署 (CWA)";
}
if (sourceCode === "noaa") {
const noaaCode = getNoaaStationCode(detail);
const noaaName = getNoaaStationName(detail);
return isEnglish(locale)
? "NOAA RCTP (Taiwan Taoyuan)"
: "NOAA RCTP(台湾桃园国际机场)";
? `NOAA ${noaaCode} (${noaaName})`
: `NOAA ${noaaCode}${noaaName}`;
}
if (sourceCode === "wunderground") {
const stationName = String(
+2 -2
View File
@@ -54,7 +54,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"guide.title": "📎 PolyWeather 系统技术说明",
"guide.closeAria": "关闭技术说明",
"guide.footer":
"数据源以 METAR、香港天文台(HKO)、NOAA RCTP、Turkish MGM、Open-Meteo、weather.gov 为主。",
"数据源以 METAR、香港天文台(HKO)、NOAA 指定站点、Turkish MGM、Open-Meteo、weather.gov 为主。",
"history.title": "📊 历史准确率对账 - {city}",
"history.closeAria": "关闭历史对账",
@@ -218,7 +218,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"guide.title": "📎 PolyWeather Technical Overview",
"guide.closeAria": "Close technical overview",
"guide.footer":
"Primary data sources are METAR, Hong Kong Observatory (HKO), NOAA RCTP, Turkish MGM, Open-Meteo, and weather.gov.",
"Primary data sources are METAR, Hong Kong Observatory (HKO), designated NOAA stations, Turkish MGM, Open-Meteo, and weather.gov.",
"history.title": "📊 Historical Reconciliation - {city}",
"history.closeAria": "Close history reconciliation",