diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py
index 7db38242..09bc67f2 100644
--- a/src/data_collection/weather_sources.py
+++ b/src/data_collection/weather_sources.py
@@ -658,8 +658,10 @@ class WeatherDataCollector:
if obs_list:
obs = obs_list[0] if isinstance(obs_list, list) else obs_list
temp = obs.get("sicaklik")
+ wind_speed = obs.get("ruzgarHiz")
+ wind_dir = obs.get("ruzgarYon")
if temp is not None and temp > -9000:
- return ist_no, temp
+ return ist_no, {"temp": temp, "wind_speed": wind_speed, "wind_dir": wind_dir}
except:
pass
return None, None
@@ -668,9 +670,9 @@ class WeatherDataCollector:
station_temps = {}
with ThreadPoolExecutor(max_workers=10) as executor:
fetch_results = list(executor.map(fetch_single_station, target_ist_nos))
- for ist_no, temp in fetch_results:
+ for ist_no, data in fetch_results:
if ist_no is not None:
- station_temps[ist_no] = temp
+ station_temps[ist_no] = data
# 4. 组装最终结果
for ist_no, temp in station_temps.items():
@@ -696,7 +698,9 @@ class WeatherDataCollector:
"name": display_name,
"lat": lat,
"lon": lon,
- "temp": temp,
+ "temp": temp.get("temp") if isinstance(temp, dict) else temp,
+ "wind_speed": temp.get("wind_speed") if isinstance(temp, dict) else None,
+ "wind_dir": temp.get("wind_dir") if isinstance(temp, dict) else None,
"istNo": ist_no
})
diff --git a/web/static/app.js b/web/static/app.js
index c5b4e02d..b3e75337 100644
--- a/web/static/app.js
+++ b/web/static/app.js
@@ -70,18 +70,18 @@ function initMap() {
// Initialize Heatmap layer (requires Leaflet.heat)
heatLayer = L.heatLayer([], {
- radius: 70,
- blur: 50,
- maxZoom: 10,
+ radius: 160, // Large radius for field blending
+ blur: 50, // Lower blur for more defined radar-like edges
+ max: 1.0, // We will pass normalized intensities [0, 1]
gradient: {
- 0.0: "blue",
- 0.2: "cyan",
- 0.4: "lime",
- 0.6: "yellow",
- 0.8: "orange",
- 1.0: "red",
+ 0.0: "#3b82f6", // Blue (Coldest in localized set)
+ 0.2: "#06b6d4", // Cyan
+ 0.4: "#10b981", // Green
+ 0.6: "#facc15", // Yellow
+ 0.8: "#f97316", // Orange
+ 1.0: "#ef4444", // Red (Hottest in localized set)
},
- opacity: 0.5,
+ opacity: 0.45,
}).addTo(map);
// Close panel and clear selection when clicking on empty map space
@@ -101,10 +101,10 @@ function updateMapVisibility() {
// These are the "Ankara-style" local station markers
if (zoom < 7) {
if (map.hasLayer(nearbyLayerGroup)) map.removeLayer(nearbyLayerGroup);
- if (map.hasLayer(heatLayer)) map.removeLayer(heatLayer);
+ if (heatLayer && map.hasLayer(heatLayer)) map.removeLayer(heatLayer);
} else {
if (!map.hasLayer(nearbyLayerGroup)) map.addLayer(nearbyLayerGroup);
- if (!map.hasLayer(heatLayer)) map.addLayer(heatLayer);
+ if (heatLayer && !map.hasLayer(heatLayer)) map.addLayer(heatLayer);
}
// 2. Handle Primary City Markers (Major vs Minor)
@@ -309,16 +309,31 @@ function renderNearbyStations(data) {
// Filter out stations too far if needed, but il handles grouping nicely.
// Skip if it is the exact same marker as main (though coordinates might slightly differ)
const sym = data.temp_symbol || "°C";
+ // Wind info if available
+ let windHtml = "";
+ if (st.wind_dir != null) {
+ const rot = (parseFloat(st.wind_dir) + 180) % 360;
+ const speed = st.wind_speed != null ? `${st.wind_speed}k` : "";
+ windHtml = `
+
+ ↑
+ ${speed}
+
+ `;
+ }
+
const iconHtml = `
- ${st.name}: ${st.temp}${sym}
+ ${st.name}
+ ${st.temp}${sym}
+ ${windHtml}
`;
const icon = L.divIcon({
html: iconHtml,
className: "",
iconSize: null,
- iconAnchor: [-5, 5],
+ iconAnchor: [0, 0],
});
const marker = L.marker([st.lat, st.lon], { icon }).addTo(nearbyLayerGroup);
@@ -327,16 +342,42 @@ function renderNearbyStations(data) {
// Update Heatmap
if (heatLayer) {
- const heatData = data.mgm_nearby.map((st) => [
- st.lat,
- st.lon,
- st.temp || 10,
- ]);
- // Add current city center to heat data
+ const allStations = [...data.mgm_nearby];
if (data.lat && data.lon && data.current?.temp != null) {
- heatData.push([data.lat, data.lon, data.current.temp]);
+ allStations.push({
+ lat: data.lat,
+ lon: data.lon,
+ temp: data.current.temp,
+ });
+ }
+
+ // Dynamic Normalization: calculate min/max in current set to show contrast.
+ // This allows seeing the 'heat island' even with subtle 0.5C differences.
+ const temps = allStations
+ .map((s) => parseFloat(s.temp))
+ .filter((t) => !isNaN(t));
+
+ if (temps.length > 0) {
+ const minT = Math.min(...temps);
+ const maxT = Math.max(...temps);
+ const range = maxT - minT || 1.0;
+
+ const heatData = allStations.map((st) => {
+ const val = parseFloat(st.temp) || minT;
+ // Normalize val to 0.0 - 1.0 based on current city data range
+ const normalized = range > 0 ? (val - minT) / range : 0.5;
+ // We use Math.max(0.1, ...) to ensure even the coldest point has some visible 'field'
+ return [st.lat, st.lon, Math.max(0.1, normalized)];
+ });
+
+ // Ensure layer is on map before updating to avoid internal Leaflet.heat errors
+ if (!map.hasLayer(heatLayer) && map.getZoom() >= 7) {
+ heatLayer.addTo(map);
+ }
+ if (map.hasLayer(heatLayer)) {
+ heatLayer.setLatLngs(heatData);
+ }
}
- heatLayer.setLatLngs(heatData);
}
if (latLngs.length > 1) {
diff --git a/web/static/style.css b/web/static/style.css
index a21046e9..3cc9db25 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -825,26 +825,58 @@ body {
/* ── Nearby Markers ── */
.nearby-marker {
- background: rgba(15, 23, 42, 0.85); /* 深蓝玻璃背景 */
- color: var(--accent-cyan); /* 亮青色 */
- border: 1px solid rgba(34, 211, 238, 0.3); /* 青色边框 */
- border-radius: 6px;
- padding: 3px 8px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: rgba(15, 23, 42, 0.9);
+ color: var(--text-secondary);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ padding: 4px 10px;
font-size: 11px;
font-weight: 500;
- box-shadow:
- 0 0 15px rgba(34, 211, 238, 0.1),
- 0 4px 6px rgba(0, 0, 0, 0.3);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
white-space: nowrap;
backdrop-filter: blur(8px);
pointer-events: none;
- animation: markerFadeIn 0.5s ease-out;
+}
+
+.nearby-name {
+ color: var(--text-muted);
}
.nearby-temp {
- font-weight: 700;
+ font-weight: 800;
color: #fff;
- margin: 0 2px;
+}
+
+.nearby-unit {
+ font-weight: 400;
+ font-size: 9px;
+ color: var(--text-muted);
+}
+
+.wind-info {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-left: 4px;
+ padding-left: 6px;
+ border-left: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.wind-arrow {
+ display: inline-block;
+ font-size: 12px;
+ color: var(--accent-cyan);
+ transition: transform 0.3s;
+ text-shadow: 0 0 8px rgba(34, 211, 238, 0.5);
+}
+
+.wind-speed {
+ font-size: 9px;
+ color: var(--text-muted);
+ font-variant-numeric: tabular-nums;
}
@keyframes markerFadeIn {