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 {