feat: Introduce PolyWeather web application with multi-source weather data collection from various APIs.
This commit is contained in:
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user