Add Moscow and broaden Turkish MGM support
This commit is contained in:
@@ -76,6 +76,11 @@ const CITY_SPECIFIC_SOURCES: Record<string, OfficialSourceLink[]> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
istanbul: [
|
istanbul: [
|
||||||
|
{
|
||||||
|
label: "MGM",
|
||||||
|
href: "https://www.mgm.gov.tr/",
|
||||||
|
kind: "agency",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "NOAA LTFM Timeseries",
|
label: "NOAA LTFM Timeseries",
|
||||||
href: "https://www.weather.gov/wrh/timeseries?site=LTFM",
|
href: "https://www.weather.gov/wrh/timeseries?site=LTFM",
|
||||||
@@ -92,6 +97,23 @@ const CITY_SPECIFIC_SOURCES: Record<string, OfficialSourceLink[]> = {
|
|||||||
kind: "metar",
|
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: [
|
london: [
|
||||||
{
|
{
|
||||||
label: "Met Office",
|
label: "Met Office",
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ function containsCjk(text: string) {
|
|||||||
return /[\u3400-\u9fff]/.test(text);
|
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 {
|
function getObservationSourceCode(detail: CityDetail): string {
|
||||||
const source = String(detail.current?.settlement_source || "")
|
const source = String(detail.current?.settlement_source || "")
|
||||||
.trim()
|
.trim()
|
||||||
@@ -182,8 +189,7 @@ export function getHeroMetaItems(detail: CityDetail, locale: Locale = "zh-CN") {
|
|||||||
const current = detail.current || {};
|
const current = detail.current || {};
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
const sourceTag = getObservationSourceTag(detail);
|
const sourceTag = getObservationSourceTag(detail);
|
||||||
const suppressAnkaraMgmObservation =
|
const suppressAnkaraMgmObservation = isTurkishMgmCity(detail);
|
||||||
String(detail.name || "").trim().toLowerCase() === "ankara";
|
|
||||||
|
|
||||||
if (current.obs_time) {
|
if (current.obs_time) {
|
||||||
const ageText =
|
const ageText =
|
||||||
@@ -253,8 +259,7 @@ export function getTemperatureChartData(
|
|||||||
const hourly = detail.hourly || {};
|
const hourly = detail.hourly || {};
|
||||||
const times = hourly.times || [];
|
const times = hourly.times || [];
|
||||||
const temps = hourly.temps || [];
|
const temps = hourly.temps || [];
|
||||||
const suppressAnkaraMgmObservation =
|
const suppressAnkaraMgmObservation = isTurkishMgmCity(detail);
|
||||||
String(detail.name || "").trim().toLowerCase() === "ankara";
|
|
||||||
|
|
||||||
if (!times.length) return null;
|
if (!times.length) return null;
|
||||||
|
|
||||||
@@ -2070,7 +2075,7 @@ export function getShortTermNowcastLines(
|
|||||||
const nearby = Array.isArray(detail.mgm_nearby) ? detail.mgm_nearby : [];
|
const nearby = Array.isArray(detail.mgm_nearby) ? detail.mgm_nearby : [];
|
||||||
const nearbySource = String(detail.nearby_source || "").toLowerCase();
|
const nearbySource = String(detail.nearby_source || "").toLowerCase();
|
||||||
const sourceLabel =
|
const sourceLabel =
|
||||||
nearbySource === "mgm" || detail.name === "ankara"
|
nearbySource === "mgm" || isTurkishMgmCity(detail)
|
||||||
? isEnglish(locale)
|
? isEnglish(locale)
|
||||||
? "MGM nearby stations"
|
? "MGM nearby stations"
|
||||||
: "MGM 周边站"
|
: "MGM 周边站"
|
||||||
@@ -2435,11 +2440,11 @@ export function getSettlementRiskNarrative(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detail.name === "ankara") {
|
if (isTurkishMgmCity(detail)) {
|
||||||
lines.push(
|
lines.push(
|
||||||
isEnglish(locale)
|
isEnglish(locale)
|
||||||
? "For Ankara, focus on LTAC / Esenboğa plus MGM nearby-station linkage, not urban sensation alone."
|
? "For Turkish MGM-supported cities, focus on the airport station plus MGM nearby-station linkage, not urban sensation alone."
|
||||||
: "Ankara 需要重点看 LTAC / Esenboğa 与 MGM 周边站联动,不能只看城区体感。",
|
: "对接入 MGM 的土耳其城市,需要重点看机场站与 MGM 周边站联动,不能只看城区体感。",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,23 @@ CITY_REGISTRY = {
|
|||||||
"distance_km": 34.0,
|
"distance_km": 34.0,
|
||||||
"warning": "市场现按 NOAA LTFM 整度°C口径结算;以 weather.gov Temp 列当天最终完成质控后的最高整度摄氏值为准。",
|
"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": {
|
"london": {
|
||||||
"name": "London",
|
"name": "London",
|
||||||
"lat": 51.5048,
|
"lat": 51.5048,
|
||||||
@@ -541,7 +558,7 @@ CITY_REGISTRY = {
|
|||||||
|
|
||||||
ALIASES = {
|
ALIASES = {
|
||||||
# English shortcuts
|
# 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",
|
"nyc": "new york", "ny": "new york", "chi": "chicago",
|
||||||
"la": "los angeles", "lax": "los angeles", "losangeles": "los angeles",
|
"la": "los angeles", "lax": "los angeles", "losangeles": "los angeles",
|
||||||
"sf": "san francisco", "sfo": "san francisco", "sanfrancisco": "san francisco",
|
"sf": "san francisco", "sfo": "san francisco", "sanfrancisco": "san francisco",
|
||||||
@@ -564,6 +581,7 @@ ALIASES = {
|
|||||||
# Chinese names
|
# Chinese names
|
||||||
"安卡拉": "ankara",
|
"安卡拉": "ankara",
|
||||||
"伊斯坦布尔": "istanbul",
|
"伊斯坦布尔": "istanbul",
|
||||||
|
"莫斯科": "moscow",
|
||||||
"伦敦": "london",
|
"伦敦": "london",
|
||||||
"巴黎": "paris",
|
"巴黎": "paris",
|
||||||
"纽约": "new york",
|
"纽约": "new york",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
|||||||
CITY_METAR_CLUSTERS = {
|
CITY_METAR_CLUSTERS = {
|
||||||
"buenos aires": ["SAEZ", "SABE", "SADP", "SADF", "SADL", "SADJ"],
|
"buenos aires": ["SAEZ", "SABE", "SADP", "SADF", "SADL", "SADJ"],
|
||||||
"istanbul": ["LTFM", "LTBA", "LTFJ"],
|
"istanbul": ["LTFM", "LTBA", "LTFJ"],
|
||||||
|
"moscow": ["UUWW", "UUEE", "UUDD"],
|
||||||
"london": ["EGLL", "EGLC", "EGKK", "EGSS", "EGGW"],
|
"london": ["EGLL", "EGLC", "EGKK", "EGSS", "EGGW"],
|
||||||
"new york": ["KLGA", "KJFK", "KEWR", "KTEB", "KHPN"],
|
"new york": ["KLGA", "KJFK", "KEWR", "KTEB", "KHPN"],
|
||||||
"los angeles": ["KLAX", "KBUR", "KLGB", "KSNA", "KVNY"],
|
"los angeles": ["KLAX", "KBUR", "KLGB", "KSNA", "KVNY"],
|
||||||
@@ -435,6 +436,12 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
|||||||
"istanbul": "Istanbul",
|
"istanbul": "Istanbul",
|
||||||
"ist": "Istanbul",
|
"ist": "Istanbul",
|
||||||
"ltfm": "Istanbul",
|
"ltfm": "Istanbul",
|
||||||
|
"moscow": "Moscow",
|
||||||
|
"mos": "Moscow",
|
||||||
|
"mow": "Moscow",
|
||||||
|
"uuww": "Moscow",
|
||||||
|
"vnukovo": "Moscow",
|
||||||
|
"莫斯科": "Moscow",
|
||||||
"伊斯坦布尔": "Istanbul",
|
"伊斯坦布尔": "Istanbul",
|
||||||
"seoul": "Seoul",
|
"seoul": "Seoul",
|
||||||
"首尔": "Seoul",
|
"首尔": "Seoul",
|
||||||
@@ -607,6 +614,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
|||||||
if not mgm_data:
|
if not mgm_data:
|
||||||
return
|
return
|
||||||
results["mgm"] = mgm_data
|
results["mgm"] = mgm_data
|
||||||
|
results["nearby_source"] = "mgm"
|
||||||
nearby = self.fetch_mgm_nearby_stations(province, root_ist_no=istno)
|
nearby = self.fetch_mgm_nearby_stations(province, root_ist_no=istno)
|
||||||
if nearby:
|
if nearby:
|
||||||
results["mgm_nearby"] = nearby
|
results["mgm_nearby"] = nearby
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ from src.analysis.metar_narrator import describe_metar_report
|
|||||||
from src.data_collection.city_registry import ALIASES
|
from src.data_collection.city_registry import ALIASES
|
||||||
from src.models.lgbm_daily_high import predict_lgbm_daily_high
|
from src.models.lgbm_daily_high import predict_lgbm_daily_high
|
||||||
|
|
||||||
|
TURKISH_MGM_CITIES = {"ankara", "istanbul"}
|
||||||
|
|
||||||
|
|
||||||
def _interpolate_hourly_value(
|
def _interpolate_hourly_value(
|
||||||
times: list,
|
times: list,
|
||||||
@@ -773,7 +775,7 @@ def _build_taf_signal(
|
|||||||
def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||||
"""Fetch, analyse, and return structured weather data for one city."""
|
"""Fetch, analyse, and return structured weather data for one city."""
|
||||||
# Check cache
|
# 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:
|
if not force_refresh:
|
||||||
cached = _cache.get(city)
|
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"):
|
if not mc.get("wx_desc"):
|
||||||
cloud_desc = "晴朗"
|
cloud_desc = "晴朗"
|
||||||
|
|
||||||
# ── 14. MGM data (Ankara-specific) ──
|
# ── 14. MGM data (Turkish MGM-supported cities) ──
|
||||||
mgm_data = {}
|
mgm_data = {}
|
||||||
if mgm:
|
if mgm:
|
||||||
mgc = mgm.get("current", {})
|
mgc = mgm.get("current", {})
|
||||||
@@ -1511,7 +1513,7 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
},
|
},
|
||||||
"mgm": mgm_data,
|
"mgm": mgm_data,
|
||||||
"mgm_nearby": raw.get("mgm_nearby", []),
|
"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": {
|
"forecast": {
|
||||||
"today_high": om_today,
|
"today_high": om_today,
|
||||||
"daily": forecast_daily,
|
"daily": forecast_daily,
|
||||||
@@ -1724,7 +1726,7 @@ def _build_city_detail_payload(
|
|||||||
"weather_gov": {},
|
"weather_gov": {},
|
||||||
"mgm": data.get("mgm") or {},
|
"mgm": data.get("mgm") or {},
|
||||||
"mgm_nearby": data.get("mgm_nearby") 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": {
|
"timeseries": {
|
||||||
"metar_recent_obs": data.get("metar_recent_obs") or [],
|
"metar_recent_obs": data.get("metar_recent_obs") or [],
|
||||||
@@ -1746,7 +1748,7 @@ def _build_city_detail_payload(
|
|||||||
"market_scan": market_scan,
|
"market_scan": market_scan,
|
||||||
"risk": data.get("risk"),
|
"risk": data.get("risk"),
|
||||||
"airport_current": data.get("airport_current") or {},
|
"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 "",
|
"ai_analysis": data.get("ai_analysis") or "",
|
||||||
"errors": {},
|
"errors": {},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user