Normalize Wunderground labels to METAR

This commit is contained in:
2569718930@qq.com
2026-04-22 23:43:27 +08:00
parent e3a9f7fb00
commit fe804bc915
8 changed files with 81 additions and 34 deletions
+10 -6
View File
@@ -17,6 +17,7 @@ import {
getRiskBadgeLabel, getRiskBadgeLabel,
getTemperatureChartData, getTemperatureChartData,
} from "@/lib/dashboard-utils"; } from "@/lib/dashboard-utils";
import { normalizeObservationSourceLabel } from "@/lib/source-labels";
function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) { function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
const { locale, t } = useI18n(); const { locale, t } = useI18n();
@@ -178,17 +179,20 @@ export function DetailPanel() {
() => getTodayPolymarketUrl(detail, locale), () => getTodayPolymarketUrl(detail, locale),
[detail, locale], [detail, locale],
); );
const basicSettlementLabel = const basicSettlementLabel = normalizeObservationSourceLabel(
selectedSummary?.current?.settlement_source_label || selectedSummary?.current?.settlement_source_label ||
selectedCityItem?.settlement_source_label || selectedCityItem?.settlement_source_label ||
selectedCityItem?.settlement_source || selectedCityItem?.settlement_source,
(locale === "en-US" ? "Settlement source pending" : "结算口径待确认"); locale === "en-US" ? "Settlement source pending" : "结算口径待确认",
);
const basicAirportLabel = const basicAirportLabel =
selectedCityItem?.airport || selectedCityItem?.airport ||
selectedSummary?.icao || selectedSummary?.icao ||
(locale === "en-US" ? "Airport pending" : "机场待确认"); (locale === "en-US" ? "Airport pending" : "机场待确认");
const heroSettlementLabel = const heroSettlementLabel = normalizeObservationSourceLabel(
detail?.current?.settlement_source_label || basicSettlementLabel; detail?.current?.settlement_source_label,
basicSettlementLabel,
);
const heroAirportLabel = detail?.risk?.airport || basicAirportLabel; const heroAirportLabel = detail?.risk?.airport || basicAirportLabel;
const isSparsePanelDetail = Boolean( const isSparsePanelDetail = Boolean(
detail && detail &&
@@ -32,6 +32,10 @@ import {
getWeatherSummary, getWeatherSummary,
} from "@/lib/dashboard-utils"; } from "@/lib/dashboard-utils";
import { dashboardClient } from "@/lib/dashboard-client"; import { dashboardClient } from "@/lib/dashboard-client";
import {
normalizeObservationSourceCode,
normalizeObservationSourceLabel,
} from "@/lib/source-labels";
import type { IntradayMeteorologySignal, MarketScan } from "@/lib/dashboard-types"; import type { IntradayMeteorologySignal, MarketScan } from "@/lib/dashboard-types";
function normalizeMarketValue(value?: number | null) { function normalizeMarketValue(value?: number | null) {
@@ -1331,9 +1335,9 @@ export function FutureForecastModal() {
String(intradayMeteorology.peak_window || "").trim() || String(intradayMeteorology.peak_window || "").trim() ||
paceView?.peakWindowText || paceView?.peakWindowText ||
"--"; "--";
const settlementSourceCode = String( const settlementSourceCode = normalizeObservationSourceCode(
detail.current?.settlement_source || "", detail.current?.settlement_source || "",
).trim().toLowerCase(); );
const settlementStationCode = String( const settlementStationCode = String(
detail.current?.station_code || detail.risk?.icao || "", detail.current?.station_code || detail.risk?.icao || "",
) )
@@ -1345,19 +1349,20 @@ export function FutureForecastModal() {
(locale === "en-US" ? "Anchor station" : "锚点站"); (locale === "en-US" ? "Anchor station" : "锚点站");
const airportMetarAnchor = const airportMetarAnchor =
settlementSourceCode === "metar" || settlementSourceCode === "metar" ||
settlementSourceCode === "wunderground" ||
Boolean(settlementStationCode && /^[A-Z]{4}$/.test(settlementStationCode)); Boolean(settlementStationCode && /^[A-Z]{4}$/.test(settlementStationCode));
const anchorSourceLabel = airportMetarAnchor const anchorSourceLabel = airportMetarAnchor
? settlementStationCode ? settlementStationCode
? `${settlementStationCode} METAR` ? `${settlementStationCode} METAR`
: "METAR" : "METAR"
: detail.current?.settlement_source_label || : normalizeObservationSourceLabel(
detail.current?.settlement_source || detail.current?.settlement_source_label ||
(locale === "en-US" ? "Official observation" : "官方观测"); detail.current?.settlement_source,
locale === "en-US" ? "Official observation" : "官方观测",
);
const anchorRuleText = airportMetarAnchor const anchorRuleText = airportMetarAnchor
? locale === "en-US" ? locale === "en-US"
? `Airport contract anchor: use the ${anchorSourceLabel} reports. Wunderground is only a history display page when present.` ? `Airport contract anchor: use the ${anchorSourceLabel} reports. Third-party history pages are display-only when present.`
: `机场合约锚点:以 ${anchorSourceLabel} 报文为准;若出现 Wunderground,它只是历史展示页面` : `机场合约锚点:以 ${anchorSourceLabel} 报文为准;第三方历史页只作为展示入口`
: locale === "en-US" : locale === "en-US"
? `Official anchor: use ${anchorSourceLabel} observations for this contract.` ? `Official anchor: use ${anchorSourceLabel} observations for this contract.`
: `官方锚点:该合约按 ${anchorSourceLabel} 观测口径判断。`; : `官方锚点:该合约按 ${anchorSourceLabel} 观测口径判断。`;
+13 -10
View File
@@ -20,6 +20,10 @@ import {
getTemperatureChartData, getTemperatureChartData,
getWeatherSummary, getWeatherSummary,
} from "@/lib/dashboard-utils"; } from "@/lib/dashboard-utils";
import {
normalizeObservationSourceCode,
normalizeObservationSourceLabel,
} from "@/lib/source-labels";
function EmptyState({ text }: { text: string }) { function EmptyState({ text }: { text: string }) {
return ( return (
@@ -548,22 +552,21 @@ export function HeroSummary() {
const { weatherIcon, weatherText } = getWeatherSummary(data, locale); const { weatherIcon, weatherText } = getWeatherSummary(data, locale);
const metaItems = getHeroMetaItems(data, locale); const metaItems = getHeroMetaItems(data, locale);
const current = data.current || {}; const current = data.current || {};
const settlementSourceCode = String(current.settlement_source || "metar") const settlementSourceCode = normalizeObservationSourceCode(
.trim() current.settlement_source || "metar",
.toLowerCase(); );
const settlementIcao = String( const settlementIcao = String(
current.station_code || data.risk?.icao || "", current.station_code || data.risk?.icao || "",
) )
.trim() .trim()
.toUpperCase(); .toUpperCase();
const settlementSource = const settlementSource =
settlementSourceCode === "wunderground" settlementSourceCode === "metar" && settlementIcao
? settlementIcao ? `${settlementIcao} METAR`
? `${settlementIcao} METAR` : normalizeObservationSourceLabel(
: "METAR" current.settlement_source_label || current.settlement_source,
: String(current.settlement_source_label || current.settlement_source || "METAR") "METAR",
.trim() ).toUpperCase();
.toUpperCase();
const isMax = const isMax =
current.max_so_far != null && current.max_so_far != null &&
current.temp != null && current.temp != null &&
@@ -20,6 +20,7 @@ import type {
CitySummary, CitySummary,
RiskLevel, RiskLevel,
} from "@/lib/dashboard-types"; } from "@/lib/dashboard-types";
import { normalizeObservationSourceLabel } from "@/lib/source-labels";
const loadHistoryModal = () => const loadHistoryModal = () =>
import("@/components/dashboard/HistoryModal").then( import("@/components/dashboard/HistoryModal").then(
@@ -214,10 +215,12 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
detail?.risk?.level; detail?.risk?.level;
const deviation = summary?.deviation_monitor || detail?.deviation_monitor; const deviation = summary?.deviation_monitor || detail?.deviation_monitor;
const observationSource = const observationSource =
summary?.current?.settlement_source_label || normalizeObservationSourceLabel(
detail?.current?.settlement_source_label || summary?.current?.settlement_source_label ||
city.settlement_source_label || detail?.current?.settlement_source_label ||
city.airport; city.settlement_source_label,
"METAR",
);
const probabilityBuckets = const probabilityBuckets =
detail?.probabilities?.distribution || detail?.probabilities?.distribution ||
(detail?.local_date (detail?.local_date
+5 -1
View File
@@ -1,4 +1,5 @@
import type { CityDetail } from "@/lib/dashboard-types"; import type { CityDetail } from "@/lib/dashboard-types";
import { normalizeObservationSourceLabel } from "@/lib/source-labels";
export type OfficialSourceLink = { export type OfficialSourceLink = {
label: string; label: string;
@@ -749,7 +750,10 @@ export function getOfficialSourceLinks(detail: CityDetail): OfficialSourceLink[]
}), }),
}; };
} }
return link; return {
...link,
label: normalizeObservationSourceLabel(link.label, link.label),
};
}); });
const seen = new Set<string>(); const seen = new Set<string>();
return links.filter((link) => { return links.filter((link) => {
+5 -1
View File
@@ -5,6 +5,7 @@ import {
HistoryPoint, HistoryPoint,
NearbyStation, NearbyStation,
} from "@/lib/dashboard-types"; } from "@/lib/dashboard-types";
import { normalizeObservationSourceLabel } from "@/lib/source-labels";
const METAR_WX_MAP: Record< const METAR_WX_MAP: Record<
string, string,
@@ -98,7 +99,10 @@ function getObservationSourceCode(detail: CityDetail): string {
} }
function getObservationSourceTag(detail: CityDetail): string { function getObservationSourceTag(detail: CityDetail): string {
const label = String(detail.current?.settlement_source_label || "") const label = normalizeObservationSourceLabel(
detail.current?.settlement_source_label,
"",
)
.trim() .trim()
.toUpperCase(); .toUpperCase();
if (label) return label; if (label) return label;
+4 -4
View File
@@ -63,9 +63,9 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"history.loading": "正在获取历史数据...", "history.loading": "正在获取历史数据...",
"history.error": "获取历史信息失败", "history.error": "获取历史信息失败",
"history.empty": "近 15 天暂无该城市历史数据", "history.empty": "近 15 天暂无该城市历史数据",
"history.hitRate": "DEB 结算胜率 (WU)", "history.hitRate": "DEB 结算胜率 (METAR)",
"history.mae": "DEB MAE", "history.mae": "DEB MAE",
"history.debHitRate": "DEB 结算胜率 (WU)", "history.debHitRate": "DEB 结算胜率 (METAR)",
"history.debMae": "DEB MAE", "history.debMae": "DEB MAE",
"history.muMae": "μ MAE", "history.muMae": "μ MAE",
"history.bestModelMae": "最佳单模型 MAE", "history.bestModelMae": "最佳单模型 MAE",
@@ -230,9 +230,9 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"history.loading": "Loading historical data...", "history.loading": "Loading historical data...",
"history.error": "Failed to load historical data", "history.error": "Failed to load historical data",
"history.empty": "No historical records for this city in the last 15 days", "history.empty": "No historical records for this city in the last 15 days",
"history.hitRate": "DEB Settlement Hit Rate (WU)", "history.hitRate": "DEB Settlement Hit Rate (METAR)",
"history.mae": "DEB MAE", "history.mae": "DEB MAE",
"history.debHitRate": "DEB Settlement Hit Rate (WU)", "history.debHitRate": "DEB Settlement Hit Rate (METAR)",
"history.debMae": "DEB MAE", "history.debMae": "DEB MAE",
"history.muMae": "μ MAE", "history.muMae": "μ MAE",
"history.bestModelMae": "Best Single-model MAE", "history.bestModelMae": "Best Single-model MAE",
+24
View File
@@ -0,0 +1,24 @@
export function normalizeObservationSourceLabel(
value?: string | null,
fallback = "METAR",
) {
const raw = String(value || "").trim();
if (!raw) return fallback;
const lowered = raw.toLowerCase();
if (
lowered === "wu" ||
lowered === "wunderground"
) {
return "METAR";
}
if (lowered.includes("wunderground")) {
return raw.replace(/wunderground/gi, "METAR");
}
return raw;
}
export function normalizeObservationSourceCode(value?: string | null) {
const code = String(value || "").trim().toLowerCase();
if (code === "wu" || code === "wunderground") return "metar";
return code;
}