feat: Introduce initial web application structure with dark theme styling, data collection, and client-side logic.
This commit is contained in:
@@ -658,8 +658,10 @@ class WeatherDataCollector:
|
|||||||
if obs_list:
|
if obs_list:
|
||||||
obs = obs_list[0] if isinstance(obs_list, list) else obs_list
|
obs = obs_list[0] if isinstance(obs_list, list) else obs_list
|
||||||
temp = obs.get("sicaklik")
|
temp = obs.get("sicaklik")
|
||||||
|
wind_speed = obs.get("ruzgarHiz")
|
||||||
|
wind_dir = obs.get("ruzgarYon")
|
||||||
if temp is not None and temp > -9000:
|
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:
|
except:
|
||||||
pass
|
pass
|
||||||
return None, None
|
return None, None
|
||||||
@@ -668,9 +670,9 @@ class WeatherDataCollector:
|
|||||||
station_temps = {}
|
station_temps = {}
|
||||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||||
fetch_results = list(executor.map(fetch_single_station, target_ist_nos))
|
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:
|
if ist_no is not None:
|
||||||
station_temps[ist_no] = temp
|
station_temps[ist_no] = data
|
||||||
|
|
||||||
# 4. 组装最终结果
|
# 4. 组装最终结果
|
||||||
for ist_no, temp in station_temps.items():
|
for ist_no, temp in station_temps.items():
|
||||||
@@ -696,7 +698,9 @@ class WeatherDataCollector:
|
|||||||
"name": display_name,
|
"name": display_name,
|
||||||
"lat": lat,
|
"lat": lat,
|
||||||
"lon": lon,
|
"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
|
"istNo": ist_no
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+63
-22
@@ -70,18 +70,18 @@ function initMap() {
|
|||||||
|
|
||||||
// Initialize Heatmap layer (requires Leaflet.heat)
|
// Initialize Heatmap layer (requires Leaflet.heat)
|
||||||
heatLayer = L.heatLayer([], {
|
heatLayer = L.heatLayer([], {
|
||||||
radius: 70,
|
radius: 160, // Large radius for field blending
|
||||||
blur: 50,
|
blur: 50, // Lower blur for more defined radar-like edges
|
||||||
maxZoom: 10,
|
max: 1.0, // We will pass normalized intensities [0, 1]
|
||||||
gradient: {
|
gradient: {
|
||||||
0.0: "blue",
|
0.0: "#3b82f6", // Blue (Coldest in localized set)
|
||||||
0.2: "cyan",
|
0.2: "#06b6d4", // Cyan
|
||||||
0.4: "lime",
|
0.4: "#10b981", // Green
|
||||||
0.6: "yellow",
|
0.6: "#facc15", // Yellow
|
||||||
0.8: "orange",
|
0.8: "#f97316", // Orange
|
||||||
1.0: "red",
|
1.0: "#ef4444", // Red (Hottest in localized set)
|
||||||
},
|
},
|
||||||
opacity: 0.5,
|
opacity: 0.45,
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
// Close panel and clear selection when clicking on empty map space
|
// 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
|
// These are the "Ankara-style" local station markers
|
||||||
if (zoom < 7) {
|
if (zoom < 7) {
|
||||||
if (map.hasLayer(nearbyLayerGroup)) map.removeLayer(nearbyLayerGroup);
|
if (map.hasLayer(nearbyLayerGroup)) map.removeLayer(nearbyLayerGroup);
|
||||||
if (map.hasLayer(heatLayer)) map.removeLayer(heatLayer);
|
if (heatLayer && map.hasLayer(heatLayer)) map.removeLayer(heatLayer);
|
||||||
} else {
|
} else {
|
||||||
if (!map.hasLayer(nearbyLayerGroup)) map.addLayer(nearbyLayerGroup);
|
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)
|
// 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.
|
// 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)
|
// Skip if it is the exact same marker as main (though coordinates might slightly differ)
|
||||||
const sym = data.temp_symbol || "°C";
|
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 = `
|
||||||
|
<div class="wind-info">
|
||||||
|
<span class="wind-arrow" style="transform: rotate(${rot}deg)">↑</span>
|
||||||
|
<span class="wind-speed">${speed}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const iconHtml = `
|
const iconHtml = `
|
||||||
<div class="nearby-marker">
|
<div class="nearby-marker">
|
||||||
${st.name}: <span class="nearby-temp">${st.temp}</span><span class="nearby-unit">${sym}</span>
|
<span class="nearby-name">${st.name}</span>
|
||||||
|
<span class="nearby-temp">${st.temp}</span><span class="nearby-unit">${sym}</span>
|
||||||
|
${windHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const icon = L.divIcon({
|
const icon = L.divIcon({
|
||||||
html: iconHtml,
|
html: iconHtml,
|
||||||
className: "",
|
className: "",
|
||||||
iconSize: null,
|
iconSize: null,
|
||||||
iconAnchor: [-5, 5],
|
iconAnchor: [0, 0],
|
||||||
});
|
});
|
||||||
|
|
||||||
const marker = L.marker([st.lat, st.lon], { icon }).addTo(nearbyLayerGroup);
|
const marker = L.marker([st.lat, st.lon], { icon }).addTo(nearbyLayerGroup);
|
||||||
@@ -327,16 +342,42 @@ function renderNearbyStations(data) {
|
|||||||
|
|
||||||
// Update Heatmap
|
// Update Heatmap
|
||||||
if (heatLayer) {
|
if (heatLayer) {
|
||||||
const heatData = data.mgm_nearby.map((st) => [
|
const allStations = [...data.mgm_nearby];
|
||||||
st.lat,
|
|
||||||
st.lon,
|
|
||||||
st.temp || 10,
|
|
||||||
]);
|
|
||||||
// Add current city center to heat data
|
|
||||||
if (data.lat && data.lon && data.current?.temp != null) {
|
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) {
|
if (latLngs.length > 1) {
|
||||||
|
|||||||
+43
-11
@@ -825,26 +825,58 @@ body {
|
|||||||
|
|
||||||
/* ── Nearby Markers ── */
|
/* ── Nearby Markers ── */
|
||||||
.nearby-marker {
|
.nearby-marker {
|
||||||
background: rgba(15, 23, 42, 0.85); /* 深蓝玻璃背景 */
|
display: flex;
|
||||||
color: var(--accent-cyan); /* 亮青色 */
|
align-items: center;
|
||||||
border: 1px solid rgba(34, 211, 238, 0.3); /* 青色边框 */
|
gap: 6px;
|
||||||
border-radius: 6px;
|
background: rgba(15, 23, 42, 0.9);
|
||||||
padding: 3px 8px;
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 10px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
box-shadow:
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
0 0 15px rgba(34, 211, 238, 0.1),
|
|
||||||
0 4px 6px rgba(0, 0, 0, 0.3);
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: markerFadeIn 0.5s ease-out;
|
}
|
||||||
|
|
||||||
|
.nearby-name {
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nearby-temp {
|
.nearby-temp {
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #fff;
|
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 {
|
@keyframes markerFadeIn {
|
||||||
|
|||||||
Reference in New Issue
Block a user