diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 85156ea7..e0c820f1 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -490,7 +490,7 @@ class WeatherDataCollector: "station_name": latest.get("istasyonAd") or latest.get("adi") or latest.get("merkezAd") - or "Ankara Esenboğa", + or "Ankara Bölge", } # 2. 每日预报(尝试两个可能的 API 路径) @@ -535,6 +535,27 @@ class WeatherDataCollector: except Exception as e: logger.debug(f"MGM forecast URL {forecast_url} failed: {e}") + # 3. 小时预报 + try: + hourly_resp = self.session.get( + f"{base_url}/tahminler/saatlik?istno={istno}", + headers=headers, + timeout=self.timeout + ) + if hourly_resp.status_code == 200: + h_data = hourly_resp.json() + if h_data and isinstance(h_data, list): + tahmin_list = h_data[0].get("tahmin", []) + results["hourly"] = [] + for t_data in tahmin_list: + if "tarih" in t_data and "sicaklik" in t_data: + results["hourly"].append({ + "time": t_data["tarih"], + "temp": t_data["sicaklik"] + }) + except Exception as e: + logger.debug(f"MGM hourly failed: {e}") + return results if "current" in results else None except Exception as e: logger.error(f"MGM API 请求失败 ({istno}): {e}") @@ -1168,9 +1189,9 @@ class WeatherDataCollector: if metar_data: results["metar"] = metar_data - # 对安卡拉,额外获取 MGM 官方数据 + # 对安卡拉,额外获取 MGM 官方数据 (17130 为 Ankara Bölge 市区测站) if city_lower == "ankara": - mgm_data = self.fetch_from_mgm("17128") + mgm_data = self.fetch_from_mgm("17130") if mgm_data: results["mgm"] = mgm_data diff --git a/web/app.py b/web/app.py index 322f4b3c..2d866592 100644 --- a/web/app.py +++ b/web/app.py @@ -353,9 +353,18 @@ def _analyze(city: str) -> Dict[str, Any]: mgm_data = {} if mgm: mgc = mgm.get("current", {}) + mgm_time_str = mgc.get("time", "") + if mgm_time_str and "T" in mgm_time_str: + try: + dt = datetime.fromisoformat(mgm_time_str.replace("Z", "+00:00")) + local_dt = dt.astimezone(timezone(timedelta(seconds=utc_offset))) + mgm_time_str = local_dt.strftime("%H:%M") + except Exception: + pass + mgm_data = { "temp": _sf(mgc.get("temp")), - "time": mgc.get("time"), + "time": mgm_time_str, "feels_like": _sf(mgc.get("feels_like")), "humidity": _sf(mgc.get("humidity")), "wind_dir": _sf(mgc.get("wind_dir")), @@ -363,8 +372,24 @@ def _analyze(city: str) -> Dict[str, Any]: "pressure": _sf(mgc.get("pressure")), "cloud_cover": mgc.get("cloud_cover"), "rain_24h": _sf(mgc.get("rain_24h")), + "hourly": [], } + mgm_hourly = mgm.get("hourly", []) + for h in mgm_hourly: + dt_str = h.get("time") + val = _sf(h.get("temp")) + if dt_str and "T" in dt_str and val is not None: + try: + dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00")) + local_dt = dt.astimezone(timezone(timedelta(seconds=utc_offset))) + mgm_data["hourly"].append({ + "time": local_dt.strftime("%Y-%m-%dT%H:%M"), + "temp": val + }) + except Exception: + pass + # ── 15. Extended Multi-Model Daily ── multi_model_daily = {} diff --git a/web/static/app.js b/web/static/app.js index 8fab25d4..eec6ac24 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -538,10 +538,33 @@ function renderChart(data) { const ctx = document.getElementById("tempChart").getContext("2d"); if (tempChart) tempChart.destroy(); + // MGM Hourly Forecast (Ankara specific) + const mgmHourlyPoints = new Array(times.length).fill(null); + let hasMgmHourly = false; + if (data.mgm?.hourly?.length > 0) { + data.mgm.hourly.forEach((hData) => { + const match = hData.time.match(/T?(\d{2}):(\d{2})/); + if (match) { + const hourKey = match[1] + ":00"; + const idx = times.indexOf(hourKey); + if (idx >= 0) { + mgmHourlyPoints[idx] = hData.temp; + hasMgmHourly = true; + } + } + }); + } + const validDebTemps = debTemps.filter((t) => t != null); const validMetar = metarPoints.filter((t) => t != null); const validMgm = mgmPoints.filter((t) => t != null); - const allVals = [...validDebTemps, ...validMetar, ...validMgm]; + const validMgmHourly = mgmHourlyPoints.filter((t) => t != null); + const allVals = [ + ...validDebTemps, + ...validMetar, + ...validMgm, + ...validMgmHourly, + ]; if (allVals.length === 0) { document.getElementById("chartLegend").textContent = "暂无数据"; return; @@ -550,8 +573,25 @@ function renderChart(data) { const maxTemp = Math.ceil(Math.max(...allVals)) + 1; // Build datasets - const datasets = [ - { + const datasets = []; + + if (hasMgmHourly) { + // If MGM is available, replace DEB curve with MGM hourly curve + datasets.push({ + label: "MGM预报", + data: mgmHourlyPoints, + borderColor: "rgba(234, 179, 8, 0.8)", // Yellow + backgroundColor: "rgba(234, 179, 8, 0.05)", + borderWidth: 2, + pointRadius: 3, + pointHoverRadius: 6, + fill: false, + tension: 0.3, + spanGaps: true, // Connect gaps because MGM is every 3 hours + }); + } else { + // Standard DEB curves + datasets.push({ label: "DEB预期", data: debPast, borderColor: "rgba(52, 211, 153, 0.6)", @@ -562,8 +602,8 @@ function renderChart(data) { fill: true, tension: 0.3, spanGaps: false, - }, - { + }); + datasets.push({ label: "DEB预报", data: debFuture, borderColor: "rgba(52, 211, 153, 0.35)", @@ -573,20 +613,22 @@ function renderChart(data) { fill: false, tension: 0.3, spanGaps: false, - }, - { - label: "METAR实测", - data: metarPoints, - borderColor: "#22d3ee", - backgroundColor: "#22d3ee", - borderWidth: 0, - pointRadius: 5, - pointHoverRadius: 7, - pointStyle: "circle", - fill: false, - order: 0, - }, - ]; + }); + } + + // Add METAR + datasets.push({ + label: "METAR实测", + data: metarPoints, + borderColor: "#22d3ee", + backgroundColor: "#22d3ee", + borderWidth: 0, + pointRadius: 5, + pointHoverRadius: 7, + pointStyle: "circle", + fill: false, + order: 0, + }); if (validMgm.length > 0) { datasets.push({ @@ -604,8 +646,8 @@ function renderChart(data) { }); } - // Add subtle OM reference line if DEB offset is significant - if (Math.abs(offset) > 0.3) { + // Add subtle OM reference line if DEB offset is significant, ONLY if we aren't replacing with MGM + if (!hasMgmHourly && Math.abs(offset) > 0.3) { datasets.push({ label: "OM原始", data: temps, @@ -695,10 +737,18 @@ function renderChart(data) { if (data.mgm?.temp != null) { legendParts.push(`MGM: ${data.mgm.temp}${data.temp_symbol}`); } - if (debMax != null && omMax != null && Math.abs(offset) > 0.3) { + if ( + !hasMgmHourly && + debMax != null && + omMax != null && + Math.abs(offset) > 0.3 + ) { const sign = offset > 0 ? "+" : ""; legendParts.push(`DEB偏移 ${sign}${offset.toFixed(1)}° vs OM`); } + if (hasMgmHourly) { + legendParts.push(`已使用MGM高精预报替换DEB分析曲线`); + } if (data.trend?.recent?.length) { const recentStr = [...data.trend.recent] .slice(0, 4)