feat: Introduce PolyWeather web application with multi-source weather data collection from various APIs.

This commit is contained in:
2569718930@qq.com
2026-03-04 19:00:51 +08:00
parent 99ca0e105e
commit 36d438e811
3 changed files with 122 additions and 26 deletions
+24 -3
View File
@@ -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
+26 -1
View File
@@ -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 = {}
+72 -22
View File
@@ -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)