feat: Create a web map application with a FastAPI backend and Leaflet frontend for displaying weather data and analysis.
This commit is contained in:
+29
@@ -359,6 +359,34 @@ def _analyze(city: str) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── 15. Extended Multi-Model Daily ──
|
||||||
|
multi_model_daily = {}
|
||||||
|
mm_daily_raw = mm.get("daily_forecasts", {})
|
||||||
|
for i, d_str in enumerate(dates):
|
||||||
|
if i == 0:
|
||||||
|
day_m = current_forecasts.copy()
|
||||||
|
d_val, d_winfo = deb_val, deb_weights
|
||||||
|
else:
|
||||||
|
day_m = mm_daily_raw.get(d_str, {}).copy()
|
||||||
|
if i < len(maxtemps) and maxtemps[i] is not None:
|
||||||
|
day_m["Open-Meteo"] = _sf(maxtemps[i])
|
||||||
|
|
||||||
|
d_val, d_winfo = None, ""
|
||||||
|
if day_m:
|
||||||
|
try:
|
||||||
|
blended, winfo = calculate_dynamic_weights(city, day_m)
|
||||||
|
if blended is not None:
|
||||||
|
d_val = blended
|
||||||
|
d_winfo = winfo
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if day_m:
|
||||||
|
multi_model_daily[d_str] = {
|
||||||
|
"models": day_m,
|
||||||
|
"deb": {"prediction": d_val, "weights_info": d_winfo}
|
||||||
|
}
|
||||||
|
|
||||||
# ── Assemble result ──
|
# ── Assemble result ──
|
||||||
result = {
|
result = {
|
||||||
"name": city,
|
"name": city,
|
||||||
@@ -402,6 +430,7 @@ def _analyze(city: str) -> Dict[str, Any]:
|
|||||||
"sunshine_hours": sunshine_h,
|
"sunshine_hours": sunshine_h,
|
||||||
},
|
},
|
||||||
"multi_model": {k: v for k, v in current_forecasts.items() if v is not None},
|
"multi_model": {k: v for k, v in current_forecasts.items() if v is not None},
|
||||||
|
"multi_model_daily": multi_model_daily,
|
||||||
"deb": {"prediction": deb_val, "weights_info": deb_weights},
|
"deb": {"prediction": deb_val, "weights_info": deb_weights},
|
||||||
"ensemble": ens_data,
|
"ensemble": ens_data,
|
||||||
"probabilities": {
|
"probabilities": {
|
||||||
|
|||||||
+35
-6
@@ -38,6 +38,7 @@ function saveCache() {
|
|||||||
let selectedCity = null;
|
let selectedCity = null;
|
||||||
let tempChart = null;
|
let tempChart = null;
|
||||||
const AUTO_REFRESH_MS = 60 * 60 * 1000; // 1 hour
|
const AUTO_REFRESH_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
let selectedForecastDate = null;
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────
|
||||||
// Map Setup
|
// Map Setup
|
||||||
@@ -231,6 +232,7 @@ async function fetchCityDetail(cityName) {
|
|||||||
// ──────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────
|
||||||
async function loadCityDetail(cityName) {
|
async function loadCityDetail(cityName) {
|
||||||
selectedCity = cityName;
|
selectedCity = cityName;
|
||||||
|
selectedForecastDate = null; // Reset selection for new city
|
||||||
setActiveCityItem(cityName);
|
setActiveCityItem(cityName);
|
||||||
setSelectedMarker(cityName);
|
setSelectedMarker(cityName);
|
||||||
|
|
||||||
@@ -316,9 +318,11 @@ function renderPanel(data) {
|
|||||||
renderChart(data);
|
renderChart(data);
|
||||||
// Probabilities
|
// Probabilities
|
||||||
renderProbabilities(data);
|
renderProbabilities(data);
|
||||||
// Multi-model
|
// Multi-model & Forecast synchronization
|
||||||
|
if (!selectedForecastDate) {
|
||||||
|
selectedForecastDate = data.local_date;
|
||||||
|
}
|
||||||
renderModels(data);
|
renderModels(data);
|
||||||
// Forecast
|
|
||||||
renderForecast(data);
|
renderForecast(data);
|
||||||
// AI
|
// AI
|
||||||
renderAI(data);
|
renderAI(data);
|
||||||
@@ -653,8 +657,18 @@ function renderProbabilities(data) {
|
|||||||
|
|
||||||
function renderModels(data) {
|
function renderModels(data) {
|
||||||
const container = document.getElementById("modelBars");
|
const container = document.getElementById("modelBars");
|
||||||
const models = data.multi_model || {};
|
const targetDate = selectedForecastDate || data.local_date;
|
||||||
const deb = data.deb?.prediction;
|
|
||||||
|
let models = {};
|
||||||
|
let deb = null;
|
||||||
|
|
||||||
|
if (data.multi_model_daily && data.multi_model_daily[targetDate]) {
|
||||||
|
models = data.multi_model_daily[targetDate].models || {};
|
||||||
|
deb = data.multi_model_daily[targetDate].deb?.prediction;
|
||||||
|
} else {
|
||||||
|
models = data.multi_model || {};
|
||||||
|
deb = data.deb?.prediction;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(models).length === 0) {
|
if (Object.keys(models).length === 0) {
|
||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
@@ -717,9 +731,13 @@ function renderForecast(data) {
|
|||||||
let html = "";
|
let html = "";
|
||||||
daily.forEach((d, i) => {
|
daily.forEach((d, i) => {
|
||||||
const isToday = i === 0;
|
const isToday = i === 0;
|
||||||
const dateLabel = isToday ? "今天" : d.date.substring(5);
|
const isSelected = d.date === selectedForecastDate;
|
||||||
|
const dateLabel = isToday ? "今天" : d.date.substring(5).replace("-", "/");
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="forecast-day ${isToday ? "today" : ""}">
|
<div class="forecast-day ${isToday ? "today" : ""} ${isSelected ? "selected" : ""}"
|
||||||
|
onclick="switchForecastDate('${data.name}', '${d.date}')"
|
||||||
|
style="cursor: pointer;">
|
||||||
<div class="f-date">${dateLabel}</div>
|
<div class="f-date">${dateLabel}</div>
|
||||||
<div class="f-temp">${d.max_temp}${sym}</div>
|
<div class="f-temp">${d.max_temp}${sym}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -737,6 +755,17 @@ function renderForecast(data) {
|
|||||||
sunEl.innerHTML = parts.map((p) => `<span>${p}</span>`).join("");
|
sunEl.innerHTML = parts.map((p) => `<span>${p}</span>`).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchForecastDate(cityName, dateStr) {
|
||||||
|
if (selectedCity !== cityName) return;
|
||||||
|
selectedForecastDate = dateStr;
|
||||||
|
|
||||||
|
const data = cityDataCache[cityName];
|
||||||
|
if (data) {
|
||||||
|
renderModels(data);
|
||||||
|
renderForecast(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderAI(data) {
|
function renderAI(data) {
|
||||||
const container = document.getElementById("aiAnalysis");
|
const container = document.getElementById("aiAnalysis");
|
||||||
const text = data.ai_analysis || "";
|
const text = data.ai_analysis || "";
|
||||||
|
|||||||
@@ -768,6 +768,15 @@ body {
|
|||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forecast-day.selected {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
background: rgba(34, 211, 238, 0.1);
|
||||||
|
box-shadow: 0 0 12px rgba(34, 211, 238, 0.2);
|
||||||
|
}
|
||||||
|
.forecast-day.selected .f-date {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
.sun-info {
|
.sun-info {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user