From 2ee30ce551c7f42e5f64ea806d9df7a4ed13e622 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 30 Mar 2026 19:03:13 +0800 Subject: [PATCH] Add Moscow and broaden Turkish MGM support --- frontend/lib/dashboard-official-sources.ts | 22 ++++++++++++++++++++++ frontend/lib/dashboard-utils.ts | 21 +++++++++++++-------- src/data_collection/city_registry.py | 20 +++++++++++++++++++- src/data_collection/weather_sources.py | 8 ++++++++ web/analysis_service.py | 12 +++++++----- 5 files changed, 69 insertions(+), 14 deletions(-) diff --git a/frontend/lib/dashboard-official-sources.ts b/frontend/lib/dashboard-official-sources.ts index 41a319e9..bb7c13da 100644 --- a/frontend/lib/dashboard-official-sources.ts +++ b/frontend/lib/dashboard-official-sources.ts @@ -76,6 +76,11 @@ const CITY_SPECIFIC_SOURCES: Record = { }, ], istanbul: [ + { + label: "MGM", + href: "https://www.mgm.gov.tr/", + kind: "agency", + }, { label: "NOAA LTFM Timeseries", href: "https://www.weather.gov/wrh/timeseries?site=LTFM", @@ -92,6 +97,23 @@ const CITY_SPECIFIC_SOURCES: Record = { kind: "metar", }, ], + moscow: [ + { + label: "NOAA UUWW Timeseries", + href: "https://www.weather.gov/wrh/timeseries?site=UUWW", + kind: "agency", + }, + { + label: "Vnukovo International Airport", + href: "https://vnukovo.ru/en/", + kind: "airport", + }, + { + label: "UUWW METAR", + href: "https://metar-taf.com/UUWW", + kind: "metar", + }, + ], london: [ { label: "Met Office", diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index 2bbe758b..80f41f93 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -44,6 +44,13 @@ function containsCjk(text: string) { return /[\u3400-\u9fff]/.test(text); } +function isTurkishMgmCity(detail: CityDetail) { + const city = String(detail.name || detail.display_name || "") + .trim() + .toLowerCase(); + return city === "ankara" || city === "istanbul"; +} + function getObservationSourceCode(detail: CityDetail): string { const source = String(detail.current?.settlement_source || "") .trim() @@ -182,8 +189,7 @@ export function getHeroMetaItems(detail: CityDetail, locale: Locale = "zh-CN") { const current = detail.current || {}; const parts: string[] = []; const sourceTag = getObservationSourceTag(detail); - const suppressAnkaraMgmObservation = - String(detail.name || "").trim().toLowerCase() === "ankara"; + const suppressAnkaraMgmObservation = isTurkishMgmCity(detail); if (current.obs_time) { const ageText = @@ -253,8 +259,7 @@ export function getTemperatureChartData( const hourly = detail.hourly || {}; const times = hourly.times || []; const temps = hourly.temps || []; - const suppressAnkaraMgmObservation = - String(detail.name || "").trim().toLowerCase() === "ankara"; + const suppressAnkaraMgmObservation = isTurkishMgmCity(detail); if (!times.length) return null; @@ -2070,7 +2075,7 @@ export function getShortTermNowcastLines( const nearby = Array.isArray(detail.mgm_nearby) ? detail.mgm_nearby : []; const nearbySource = String(detail.nearby_source || "").toLowerCase(); const sourceLabel = - nearbySource === "mgm" || detail.name === "ankara" + nearbySource === "mgm" || isTurkishMgmCity(detail) ? isEnglish(locale) ? "MGM nearby stations" : "MGM 周边站" @@ -2435,11 +2440,11 @@ export function getSettlementRiskNarrative( } } - if (detail.name === "ankara") { + if (isTurkishMgmCity(detail)) { lines.push( isEnglish(locale) - ? "For Ankara, focus on LTAC / Esenboğa plus MGM nearby-station linkage, not urban sensation alone." - : "Ankara 需要重点看 LTAC / Esenboğa 与 MGM 周边站联动,不能只看城区体感。", + ? "For Turkish MGM-supported cities, focus on the airport station plus MGM nearby-station linkage, not urban sensation alone." + : "对接入 MGM 的土耳其城市,需要重点看机场站与 MGM 周边站联动,不能只看城区体感。", ); } diff --git a/src/data_collection/city_registry.py b/src/data_collection/city_registry.py index f9238be5..8c52e9c4 100644 --- a/src/data_collection/city_registry.py +++ b/src/data_collection/city_registry.py @@ -33,6 +33,23 @@ CITY_REGISTRY = { "distance_km": 34.0, "warning": "市场现按 NOAA LTFM 整度°C口径结算;以 weather.gov Temp 列当天最终完成质控后的最高整度摄氏值为准。", }, + "moscow": { + "name": "Moscow", + "lat": 55.5915, + "lon": 37.2615, + "icao": "UUWW", + "settlement_source": "noaa", + "settlement_station_code": "UUWW", + "settlement_station_label": "Vnukovo International Airport", + "tz_offset": 10800, + "use_fahrenheit": False, + "is_major": True, + "risk_level": "medium", + "risk_emoji": "🟡", + "airport_name": "Vnukovo International Airport", + "distance_km": 29.0, + "warning": "市场现按 NOAA UUWW 整度°C口径结算;以 weather.gov Temp 列在当日全部小时最终完成后的最高整度摄氏值为准。", + }, "london": { "name": "London", "lat": 51.5048, @@ -541,7 +558,7 @@ CITY_REGISTRY = { ALIASES = { # English shortcuts - "ank": "ankara", "ist": "istanbul", "ltfm": "istanbul", "lon": "london", "par": "paris", + "ank": "ankara", "ist": "istanbul", "ltfm": "istanbul", "mos": "moscow", "mow": "moscow", "uuww": "moscow", "vnukovo": "moscow", "lon": "london", "par": "paris", "nyc": "new york", "ny": "new york", "chi": "chicago", "la": "los angeles", "lax": "los angeles", "losangeles": "los angeles", "sf": "san francisco", "sfo": "san francisco", "sanfrancisco": "san francisco", @@ -564,6 +581,7 @@ ALIASES = { # Chinese names "安卡拉": "ankara", "伊斯坦布尔": "istanbul", + "莫斯科": "moscow", "伦敦": "london", "巴黎": "paris", "纽约": "new york", diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 3dc0719d..e0d73221 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -33,6 +33,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour CITY_METAR_CLUSTERS = { "buenos aires": ["SAEZ", "SABE", "SADP", "SADF", "SADL", "SADJ"], "istanbul": ["LTFM", "LTBA", "LTFJ"], + "moscow": ["UUWW", "UUEE", "UUDD"], "london": ["EGLL", "EGLC", "EGKK", "EGSS", "EGGW"], "new york": ["KLGA", "KJFK", "KEWR", "KTEB", "KHPN"], "los angeles": ["KLAX", "KBUR", "KLGB", "KSNA", "KVNY"], @@ -435,6 +436,12 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour "istanbul": "Istanbul", "ist": "Istanbul", "ltfm": "Istanbul", + "moscow": "Moscow", + "mos": "Moscow", + "mow": "Moscow", + "uuww": "Moscow", + "vnukovo": "Moscow", + "莫斯科": "Moscow", "伊斯坦布尔": "Istanbul", "seoul": "Seoul", "首尔": "Seoul", @@ -607,6 +614,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour if not mgm_data: return results["mgm"] = mgm_data + results["nearby_source"] = "mgm" nearby = self.fetch_mgm_nearby_stations(province, root_ist_no=istno) if nearby: results["mgm_nearby"] = nearby diff --git a/web/analysis_service.py b/web/analysis_service.py index 6f9dbd29..d1437f12 100644 --- a/web/analysis_service.py +++ b/web/analysis_service.py @@ -26,6 +26,8 @@ from src.analysis.metar_narrator import describe_metar_report from src.data_collection.city_registry import ALIASES from src.models.lgbm_daily_high import predict_lgbm_daily_high +TURKISH_MGM_CITIES = {"ankara", "istanbul"} + def _interpolate_hourly_value( times: list, @@ -773,7 +775,7 @@ def _build_taf_signal( def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: """Fetch, analyse, and return structured weather data for one city.""" # Check cache - ttl = CACHE_TTL_ANKARA if city.lower() == "ankara" else CACHE_TTL + ttl = CACHE_TTL_ANKARA if city.lower() in TURKISH_MGM_CITIES else CACHE_TTL if not force_refresh: cached = _cache.get(city) @@ -1344,7 +1346,7 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: if not mc.get("wx_desc"): cloud_desc = "晴朗" - # ── 14. MGM data (Ankara-specific) ── + # ── 14. MGM data (Turkish MGM-supported cities) ── mgm_data = {} if mgm: mgc = mgm.get("current", {}) @@ -1511,7 +1513,7 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: }, "mgm": mgm_data, "mgm_nearby": raw.get("mgm_nearby", []), - "nearby_source": raw.get("nearby_source") or ("mgm" if city.lower() == "ankara" else "metar_cluster"), + "nearby_source": raw.get("nearby_source") or ("mgm" if city.lower() in TURKISH_MGM_CITIES else "metar_cluster"), "forecast": { "today_high": om_today, "daily": forecast_daily, @@ -1724,7 +1726,7 @@ def _build_city_detail_payload( "weather_gov": {}, "mgm": data.get("mgm") or {}, "mgm_nearby": data.get("mgm_nearby") or [], - "nearby_source": data.get("nearby_source") or ("mgm" if data.get("name") == "ankara" else "metar_cluster"), + "nearby_source": data.get("nearby_source") or ("mgm" if str(data.get("name") or "").lower() in TURKISH_MGM_CITIES else "metar_cluster"), }, "timeseries": { "metar_recent_obs": data.get("metar_recent_obs") or [], @@ -1746,7 +1748,7 @@ def _build_city_detail_payload( "market_scan": market_scan, "risk": data.get("risk"), "airport_current": data.get("airport_current") or {}, - "nearby_source": data.get("nearby_source") or ("mgm" if data.get("name") == "ankara" else "metar_cluster"), + "nearby_source": data.get("nearby_source") or ("mgm" if str(data.get("name") or "").lower() in TURKISH_MGM_CITIES else "metar_cluster"), "ai_analysis": data.get("ai_analysis") or "", "errors": {}, }