From 0917f8d010e862529a427aa66dbfa0bb938d8b26 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 4 Mar 2026 17:11:20 +0800 Subject: [PATCH] feat: Add multi-source weather data collection and a web application for display. --- src/data_collection/weather_sources.py | 63 ++++++++++++++------------ web/app.py | 4 ++ web/static/app.js | 24 +++++++--- 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 2fa70798..85156ea7 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -328,37 +328,40 @@ class WeatherDataCollector: # 3. 提取最近 4 条报文的多维数据(温度 + 风/云/压强,用于趋势和 shock_score) recent_temps_raw = [] # [(local_time_str, temp_c), ...] recent_obs_raw = [] # [{time, temp, wdir, wspd, clouds, altim}, ...] - for obs in data[:4]: # data 已按时间倒序 + today_obs_raw = [] # [(local_time_str, temp_c), ...] 今天全部观测 + cloud_rank_map = { + "CLR": 0, "SKC": 0, "FEW": 1, "SCT": 2, "BKN": 3, "OVC": 4, + } + for i_obs, obs in enumerate(data): # data 已按时间倒序 obs_temp = obs.get("temp") obs_dt_iter = _parse_rawob_time(obs) if obs_temp is not None and obs_dt_iter: local_rt = obs_dt_iter + timedelta(seconds=utc_offset) - recent_temps_raw.append((local_rt.strftime("%H:%M"), obs_temp)) - # 云量码映射: CLR=0, FEW=1, SCT=2, BKN=3, OVC=4 - cloud_rank_map = { - "CLR": 0, - "SKC": 0, - "FEW": 1, - "SCT": 2, - "BKN": 3, - "OVC": 4, - } - clouds = obs.get("clouds", []) - max_cloud_rank = 0 - for c in clouds: - rank = cloud_rank_map.get(c.get("cover", ""), 0) - if rank > max_cloud_rank: - max_cloud_rank = rank - recent_obs_raw.append( - { - "time": local_rt.strftime("%H:%M"), - "temp": obs_temp, - "wdir": obs.get("wdir"), - "wspd": obs.get("wspd"), - "cloud_rank": max_cloud_rank, # 0~4 - "altim": obs.get("altim"), - } - ) + time_str = local_rt.strftime("%H:%M") + + # 收集今天全部观测点(用于图表叠加) + if obs_dt_iter >= utc_midnight: + today_obs_raw.append((time_str, obs_temp)) + + # 只取前4条用于趋势分析和 shock_score + if i_obs < 4: + recent_temps_raw.append((time_str, obs_temp)) + clouds = obs.get("clouds", []) + max_cloud_rank = 0 + for c in clouds: + rank = cloud_rank_map.get(c.get("cover", ""), 0) + if rank > max_cloud_rank: + max_cloud_rank = rank + recent_obs_raw.append( + { + "time": time_str, + "temp": obs_temp, + "wdir": obs.get("wdir"), + "wspd": obs.get("wspd"), + "cloud_rank": max_cloud_rank, # 0~4 + "altim": obs.get("altim"), + } + ) # 转换为单位 if use_fahrenheit: @@ -366,16 +369,19 @@ class WeatherDataCollector: max_so_far = max_so_far_c * 9 / 5 + 32 if max_so_far_c > -900 else None dewp = dewp_c * 9 / 5 + 32 if dewp_c is not None else None unit = "fahrenheit" - # 转换最近温度 recent_temps = [ (t, round(v * 9 / 5 + 32, 1)) for t, v in recent_temps_raw ] + today_obs = [ + (t, round(v * 9 / 5 + 32, 1)) for t, v in today_obs_raw + ] else: temp = temp_c max_so_far = max_so_far_c if max_so_far_c > -900 else None dewp = dewp_c unit = "celsius" recent_temps = [(t, v) for t, v in recent_temps_raw] + today_obs = [(t, v) for t, v in today_obs_raw] result = { "source": "metar", @@ -399,6 +405,7 @@ class WeatherDataCollector: "clouds": latest.get("clouds", []), }, "recent_temps": recent_temps, # 最近4条: [("15:00", 5), ("14:20", 5), ...] + "today_obs": today_obs, # 今天全部观测: [("00:00", 3), ("01:00", 2.5), ...] "recent_obs": recent_obs_raw, # 最近4条多维数据(风/云/压强) "unit": unit, } diff --git a/web/app.py b/web/app.py index 150af51d..d62b5ef0 100644 --- a/web/app.py +++ b/web/app.py @@ -445,6 +445,10 @@ def _analyze(city: str) -> Dict[str, Any]: "status": peak_status, }, "hourly": today_hourly, + "metar_today_obs": [ + {"time": t, "temp": v} + for t, v in (metar.get("today_obs", []) if metar else []) + ], "ai_analysis": ai_text, "updated_at": datetime.now(timezone.utc).isoformat(), } diff --git a/web/static/app.js b/web/static/app.js index 6068358a..8e58188f 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -476,15 +476,25 @@ function renderChart(data) { curIdx < 0 || i >= curIdx ? t : null, ); - // METAR observation scatter points + // METAR observation scatter points — use full today's obs if available const metarPoints = new Array(times.length).fill(null); - const metarRecent = data.trend?.recent || []; - if (metarRecent.length > 0) { - metarRecent.forEach((r) => { - // Match METAR time (e.g. "15:00") to chart time labels - const idx = times.indexOf(r.time); + const metarSrc = data.metar_today_obs?.length + ? data.metar_today_obs + : data.trend?.recent || []; + if (metarSrc.length > 0) { + metarSrc.forEach((r) => { + // METAR time may be "14:20" but chart labels are whole hours "14:00" + const parts = r.time.split(":"); + let h = parseInt(parts[0], 10); + const m = parseInt(parts[1] || "0", 10); + if (m >= 30) h = (h + 1) % 24; // round to nearest hour + const hourKey = h.toString().padStart(2, "0") + ":00"; + const idx = times.indexOf(hourKey); if (idx >= 0) { - metarPoints[idx] = r.temp; + // If multiple METARs map to the same hour, keep the latest (first in array) + if (metarPoints[idx] === null) { + metarPoints[idx] = r.temp; + } } }); }