Spaces:
Sleeping
Sleeping
ok
Browse files- .DS_Store +0 -0
- Dockerfile +0 -23
- README.md +0 -12
- app.py +0 -1067
- app.py.backup +0 -692
- index.html +0 -377
- packages.txt +0 -2
- requirements.txt +0 -8
.DS_Store
CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
|
|
Dockerfile
DELETED
@@ -1,23 +0,0 @@
|
|
1 |
-
FROM python:3.10-slim
|
2 |
-
|
3 |
-
# Install only essential packages
|
4 |
-
RUN apt-get update && apt-get install -y \
|
5 |
-
git \
|
6 |
-
&& rm -rf /var/lib/apt/lists/*
|
7 |
-
|
8 |
-
# Create user
|
9 |
-
RUN useradd -m -u 1000 user
|
10 |
-
WORKDIR /home/user/app
|
11 |
-
|
12 |
-
# Copy and install Python requirements
|
13 |
-
COPY requirements.txt .
|
14 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
15 |
-
|
16 |
-
# Copy application
|
17 |
-
COPY . .
|
18 |
-
RUN chown -R user:user /home/user/app
|
19 |
-
|
20 |
-
USER user
|
21 |
-
EXPOSE 7860
|
22 |
-
|
23 |
-
CMD ["python", "app.py"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
DELETED
@@ -1,12 +0,0 @@
|
|
1 |
-
---
|
2 |
-
title: Ecmwf Wind Docker
|
3 |
-
emoji: 📉
|
4 |
-
colorFrom: blue
|
5 |
-
colorTo: yellow
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
license: cc-by-4.0
|
9 |
-
short_description: wind data in the docker html5 land
|
10 |
-
---
|
11 |
-
|
12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
DELETED
@@ -1,1067 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
"""
|
3 |
-
ECMWF Wind Visualization with Real Data
|
4 |
-
Based on proven particle technology from windmap project
|
5 |
-
"""
|
6 |
-
|
7 |
-
import gradio as gr
|
8 |
-
import numpy as np
|
9 |
-
import pandas as pd
|
10 |
-
import folium
|
11 |
-
import requests
|
12 |
-
import json
|
13 |
-
import time
|
14 |
-
import tempfile
|
15 |
-
import os
|
16 |
-
import xarray as xr
|
17 |
-
from datetime import datetime, timedelta
|
18 |
-
import warnings
|
19 |
-
warnings.filterwarnings('ignore')
|
20 |
-
|
21 |
-
try:
|
22 |
-
from ecmwf.opendata import Client as OpenDataClient
|
23 |
-
OPENDATA_AVAILABLE = True
|
24 |
-
except ImportError:
|
25 |
-
OPENDATA_AVAILABLE = False
|
26 |
-
|
27 |
-
class ECMWFWindDataFetcher:
|
28 |
-
def __init__(self):
|
29 |
-
self.temp_dir = tempfile.mkdtemp()
|
30 |
-
self.client = None
|
31 |
-
if OPENDATA_AVAILABLE:
|
32 |
-
try:
|
33 |
-
self.client = OpenDataClient()
|
34 |
-
except:
|
35 |
-
self.client = None
|
36 |
-
|
37 |
-
# AWS S3 direct access URLs (completely free)
|
38 |
-
self.aws_base_url = "https://ecmwf-forecasts.s3.eu-central-1.amazonaws.com"
|
39 |
-
|
40 |
-
def get_latest_forecast_info(self):
|
41 |
-
"""Get the latest available ECMWF forecast run info"""
|
42 |
-
now = datetime.utcnow()
|
43 |
-
|
44 |
-
# ECMWF runs at 00, 06, 12, 18 UTC
|
45 |
-
forecast_hours = [0, 6, 12, 18]
|
46 |
-
|
47 |
-
# Find the most recent forecast run
|
48 |
-
for hours_back in range(0, 24, 6):
|
49 |
-
check_time = now - timedelta(hours=hours_back)
|
50 |
-
run_hour = max([h for h in forecast_hours if h <= check_time.hour])
|
51 |
-
|
52 |
-
forecast_time = check_time.replace(hour=run_hour, minute=0, second=0, microsecond=0)
|
53 |
-
|
54 |
-
# ECMWF data is usually available 2-4 hours after run time
|
55 |
-
if now >= (forecast_time + timedelta(hours=3)):
|
56 |
-
date_str = forecast_time.strftime("%Y%m%d")
|
57 |
-
time_str = f"{run_hour:02d}"
|
58 |
-
return date_str, time_str, forecast_time
|
59 |
-
|
60 |
-
# Fallback to previous day
|
61 |
-
yesterday = now - timedelta(days=1)
|
62 |
-
return yesterday.strftime("%Y%m%d"), "18", yesterday.replace(hour=18)
|
63 |
-
|
64 |
-
def download_ecmwf_wind_data(self, step=0):
|
65 |
-
"""Download ECMWF wind data at multiple levels"""
|
66 |
-
date_str, time_str, run_time = self.get_latest_forecast_info()
|
67 |
-
|
68 |
-
# Download 10m wind components
|
69 |
-
u10_file, u10_status = self.download_parameter('10u', step)
|
70 |
-
v10_file, v10_status = self.download_parameter('10v', step)
|
71 |
-
|
72 |
-
# Download 100m wind components
|
73 |
-
u100_file, u100_status = self.download_parameter('100u', step)
|
74 |
-
v100_file, v100_status = self.download_parameter('100v', step)
|
75 |
-
|
76 |
-
status_msg = f"✅ ECMWF Wind Data Retrieved!\nRun: {date_str} {time_str}z, Step: +{step}h\n"
|
77 |
-
status_msg += f"10m U-component: {u10_status}\n10m V-component: {v10_status}\n"
|
78 |
-
status_msg += f"100m U-component: {u100_status}\n100m V-component: {v100_status}"
|
79 |
-
|
80 |
-
# Return both levels
|
81 |
-
wind_data = {
|
82 |
-
'10m': (u10_file, v10_file) if u10_file and v10_file else (None, None),
|
83 |
-
'100m': (u100_file, v100_file) if u100_file and v100_file else (None, None)
|
84 |
-
}
|
85 |
-
|
86 |
-
return wind_data, status_msg
|
87 |
-
|
88 |
-
def download_parameter(self, parameter, step=0):
|
89 |
-
"""Download a specific ECMWF parameter using multiple methods"""
|
90 |
-
date_str, time_str, run_time = self.get_latest_forecast_info()
|
91 |
-
|
92 |
-
# Method 1: Try ecmwf-opendata client (most reliable)
|
93 |
-
if OPENDATA_AVAILABLE and self.client:
|
94 |
-
try:
|
95 |
-
filename = os.path.join(self.temp_dir, f'ecmwf_{parameter}_{step}h_{datetime.now().strftime("%Y%m%d_%H%M%S")}.grib')
|
96 |
-
|
97 |
-
self.client.retrieve(
|
98 |
-
type="fc",
|
99 |
-
param=parameter,
|
100 |
-
step=step,
|
101 |
-
target=filename
|
102 |
-
)
|
103 |
-
|
104 |
-
if os.path.exists(filename) and os.path.getsize(filename) > 1000:
|
105 |
-
return filename, f"✅ Downloaded via ECMWF client"
|
106 |
-
|
107 |
-
except Exception as e:
|
108 |
-
print(f"ECMWF client method failed: {str(e)}")
|
109 |
-
|
110 |
-
# Method 2: Direct AWS S3 access (backup method)
|
111 |
-
try:
|
112 |
-
step_str = f"{step:03d}"
|
113 |
-
filename = f"{date_str}{time_str}0000-{step_str}h-oper-fc.grib2"
|
114 |
-
url = f"{self.aws_base_url}/{date_str}/{time_str}z/0p25/oper/{filename}"
|
115 |
-
|
116 |
-
response = requests.get(url, timeout=120, stream=True)
|
117 |
-
if response.status_code == 200:
|
118 |
-
local_file = os.path.join(self.temp_dir, f'ecmwf_aws_{parameter}_{step}h.grib2')
|
119 |
-
|
120 |
-
with open(local_file, 'wb') as f:
|
121 |
-
for chunk in response.iter_content(chunk_size=8192):
|
122 |
-
f.write(chunk)
|
123 |
-
|
124 |
-
if os.path.getsize(local_file) > 1000:
|
125 |
-
return local_file, f"✅ Downloaded via AWS S3"
|
126 |
-
|
127 |
-
except Exception as e:
|
128 |
-
print(f"AWS method failed: {str(e)}")
|
129 |
-
|
130 |
-
return None, f"❌ Failed to download {parameter}"
|
131 |
-
|
132 |
-
def extract_wind_data_from_grib(self, u_file, v_file, level_name, resolution=8):
|
133 |
-
"""Extract wind data from ECMWF GRIB files with improved processing"""
|
134 |
-
try:
|
135 |
-
print(f"🌪️ Processing {level_name} ECMWF GRIB wind data...")
|
136 |
-
|
137 |
-
# Open GRIB files with robust error handling
|
138 |
-
ds_u = None
|
139 |
-
ds_v = None
|
140 |
-
|
141 |
-
try:
|
142 |
-
# Method 1: Standard xarray with cfgrib
|
143 |
-
ds_u = xr.open_dataset(u_file, engine='cfgrib')
|
144 |
-
ds_v = xr.open_dataset(v_file, engine='cfgrib')
|
145 |
-
print(f"✅ Opened {level_name} GRIB files with cfgrib")
|
146 |
-
except Exception as e1:
|
147 |
-
print(f"⚠️ cfgrib method failed: {e1}")
|
148 |
-
try:
|
149 |
-
# Method 2: Alternative backend settings
|
150 |
-
ds_u = xr.open_dataset(u_file, engine='cfgrib', backend_kwargs={'indexpath': ''})
|
151 |
-
ds_v = xr.open_dataset(v_file, engine='cfgrib', backend_kwargs={'indexpath': ''})
|
152 |
-
print(f"✅ Opened {level_name} GRIB files with alternative cfgrib settings")
|
153 |
-
except Exception as e2:
|
154 |
-
print(f"❌ All GRIB opening methods failed: {e2}")
|
155 |
-
return []
|
156 |
-
|
157 |
-
# Debug: Print dataset info
|
158 |
-
print(f"📊 U dataset variables: {list(ds_u.data_vars.keys())}")
|
159 |
-
print(f"📊 U dataset coords: {list(ds_u.coords.keys())}")
|
160 |
-
print(f"📊 U dataset dims: {ds_u.dims}")
|
161 |
-
|
162 |
-
# Get wind components - more robust variable detection
|
163 |
-
u_var = None
|
164 |
-
v_var = None
|
165 |
-
|
166 |
-
# Try to find wind variables
|
167 |
-
for var in ds_u.data_vars.keys():
|
168 |
-
if 'u' in var.lower() or '10u' in var or '100u' in var:
|
169 |
-
u_var = var
|
170 |
-
break
|
171 |
-
|
172 |
-
for var in ds_v.data_vars.keys():
|
173 |
-
if 'v' in var.lower() or '10v' in var or '100v' in var:
|
174 |
-
v_var = var
|
175 |
-
break
|
176 |
-
|
177 |
-
if not u_var or not v_var:
|
178 |
-
print(f"❌ Could not find wind variables in {level_name} data")
|
179 |
-
return []
|
180 |
-
|
181 |
-
print(f"🌬️ Using variables: U={u_var}, V={v_var}")
|
182 |
-
|
183 |
-
u_data = ds_u[u_var]
|
184 |
-
v_data = ds_v[v_var]
|
185 |
-
|
186 |
-
# Simple coordinate extraction - like working ECMWF code
|
187 |
-
if 'latitude' in ds_u.coords:
|
188 |
-
lats = ds_u.latitude.values
|
189 |
-
lons = ds_u.longitude.values
|
190 |
-
elif 'lat' in ds_u.coords:
|
191 |
-
lats = ds_u.lat.values
|
192 |
-
lons = ds_u.lon.values
|
193 |
-
else:
|
194 |
-
print(f"❌ Could not find coordinates in {level_name} data")
|
195 |
-
return []
|
196 |
-
|
197 |
-
print(f"🗺️ Found coordinates: lats {lats.shape}, lons {lons.shape}")
|
198 |
-
print(f"🗺️ Lat range: {lats.min():.2f} to {lats.max():.2f}")
|
199 |
-
print(f"🗺️ Lon range: {lons.min():.2f} to {lons.max():.2f}")
|
200 |
-
|
201 |
-
# Get data values - handle time dimension
|
202 |
-
u_values = u_data.values
|
203 |
-
v_values = v_data.values
|
204 |
-
|
205 |
-
if u_values.ndim > 2:
|
206 |
-
print(f"🕐 Selecting first time/level from {u_values.ndim}D data")
|
207 |
-
while u_values.ndim > 2:
|
208 |
-
u_values = u_values[0]
|
209 |
-
v_values = v_values[0]
|
210 |
-
|
211 |
-
print(f"📐 Data shape: {u_values.shape}")
|
212 |
-
print(f"📐 Coord shapes: lats={len(lats)}, lons={len(lons)}")
|
213 |
-
|
214 |
-
# Smart subsampling based on resolution
|
215 |
-
if resolution >= 10:
|
216 |
-
step = max(1, len(lats) // 50) # Limit to ~50 points per dimension
|
217 |
-
else:
|
218 |
-
step = max(1, int(resolution // 2))
|
219 |
-
|
220 |
-
print(f"⬇️ Subsampling every {step} points")
|
221 |
-
|
222 |
-
lats_sub = lats[::step]
|
223 |
-
lons_sub = lons[::step]
|
224 |
-
u_sub = u_values[::step, ::step]
|
225 |
-
v_sub = v_values[::step, ::step]
|
226 |
-
|
227 |
-
# Create wind data points
|
228 |
-
wind_data = []
|
229 |
-
valid_points = 0
|
230 |
-
|
231 |
-
print(f"🔍 Creating wind data points from subsampled coordinates:")
|
232 |
-
print(f"🔍 lats_sub range: {lats_sub.min():.2f} to {lats_sub.max():.2f} (shape: {lats_sub.shape})")
|
233 |
-
print(f"🔍 lons_sub range: {lons_sub.min():.2f} to {lons_sub.max():.2f} (shape: {lons_sub.shape})")
|
234 |
-
print(f"🔍 First 3 lats_sub: {lats_sub[:3]}")
|
235 |
-
print(f"🔍 First 3 lons_sub: {lons_sub[:3]}")
|
236 |
-
print(f"🔍 u_sub shape: {u_sub.shape}, v_sub shape: {v_sub.shape}")
|
237 |
-
print(f"🔍 Will iterate: {len(lats_sub)} lats × {len(lons_sub)} lons = {len(lats_sub)*len(lons_sub)} potential points")
|
238 |
-
|
239 |
-
for i, lat in enumerate(lats_sub):
|
240 |
-
print(f"🔍 Processing lat row {i}: lat={lat:.2f}")
|
241 |
-
for j, lon in enumerate(lons_sub):
|
242 |
-
if i < len(u_sub) and j < len(u_sub[i]):
|
243 |
-
try:
|
244 |
-
u_wind = float(u_sub[i, j])
|
245 |
-
v_wind = float(v_sub[i, j])
|
246 |
-
|
247 |
-
# Skip invalid data (NaN, extreme values)
|
248 |
-
if np.isnan(u_wind) or np.isnan(v_wind):
|
249 |
-
continue
|
250 |
-
if abs(u_wind) > 200 or abs(v_wind) > 200: # Sanity check
|
251 |
-
continue
|
252 |
-
|
253 |
-
# Calculate speed and direction
|
254 |
-
speed = float(np.sqrt(u_wind**2 + v_wind**2))
|
255 |
-
direction = float(np.degrees(np.arctan2(v_wind, u_wind)))
|
256 |
-
|
257 |
-
# Normalize direction to 0-360
|
258 |
-
if direction < 0:
|
259 |
-
direction += 360
|
260 |
-
|
261 |
-
wind_data.append({
|
262 |
-
'lat': float(lat),
|
263 |
-
'lon': float(lon),
|
264 |
-
'u': u_wind,
|
265 |
-
'v': v_wind,
|
266 |
-
'speed': speed,
|
267 |
-
'direction': direction,
|
268 |
-
'level': level_name
|
269 |
-
})
|
270 |
-
valid_points += 1
|
271 |
-
|
272 |
-
# Debug ALL points in first few rows
|
273 |
-
if i <= 3 or valid_points <= 10:
|
274 |
-
print(f"🔍 Point {valid_points+1}: i={i}, j={j}, lat={lat:.4f}, lon={lon:.4f}, speed={speed:.2f}")
|
275 |
-
|
276 |
-
except (ValueError, TypeError) as e:
|
277 |
-
continue # Skip invalid data points
|
278 |
-
|
279 |
-
print(f"✅ Processed {valid_points} valid {level_name} wind data points")
|
280 |
-
|
281 |
-
# Close datasets to free memory
|
282 |
-
ds_u.close()
|
283 |
-
ds_v.close()
|
284 |
-
|
285 |
-
return wind_data
|
286 |
-
|
287 |
-
except Exception as e:
|
288 |
-
print(f"❌ Error processing {level_name} GRIB files: {str(e)}")
|
289 |
-
import traceback
|
290 |
-
traceback.print_exc()
|
291 |
-
return []
|
292 |
-
|
293 |
-
def get_wind_data(lat_min=-90, lat_max=90, lon_min=-180, lon_max=180, resolution=8):
|
294 |
-
"""
|
295 |
-
Fetch REAL ECMWF wind data at multiple levels using proven methodology
|
296 |
-
"""
|
297 |
-
try:
|
298 |
-
print(f"🌍 Fetching ECMWF wind data at 10m and 100m levels (resolution: {resolution}°)...")
|
299 |
-
|
300 |
-
# Create ECMWF data fetcher
|
301 |
-
fetcher = ECMWFWindDataFetcher()
|
302 |
-
|
303 |
-
# Download ECMWF wind data for both levels
|
304 |
-
wind_files, status = fetcher.download_ecmwf_wind_data(step=0)
|
305 |
-
|
306 |
-
all_wind_data = []
|
307 |
-
|
308 |
-
# Process 10m wind data
|
309 |
-
if wind_files['10m'][0] and wind_files['10m'][1]:
|
310 |
-
u10_file, v10_file = wind_files['10m']
|
311 |
-
wind_data_10m = fetcher.extract_wind_data_from_grib(u10_file, v10_file, '10m', resolution)
|
312 |
-
if wind_data_10m:
|
313 |
-
all_wind_data.extend(wind_data_10m)
|
314 |
-
|
315 |
-
# Process 100m wind data
|
316 |
-
if wind_files['100m'][0] and wind_files['100m'][1]:
|
317 |
-
u100_file, v100_file = wind_files['100m']
|
318 |
-
wind_data_100m = fetcher.extract_wind_data_from_grib(u100_file, v100_file, '100m', resolution)
|
319 |
-
if wind_data_100m:
|
320 |
-
all_wind_data.extend(wind_data_100m)
|
321 |
-
|
322 |
-
if all_wind_data:
|
323 |
-
# Filter data to requested region
|
324 |
-
filtered_data = [
|
325 |
-
wd for wd in all_wind_data
|
326 |
-
if lat_min <= wd['lat'] <= lat_max and lon_min <= wd['lon'] <= lon_max
|
327 |
-
]
|
328 |
-
|
329 |
-
print(f"✅ Successfully processed {len(filtered_data)} total ECMWF wind points")
|
330 |
-
return filtered_data if filtered_data else all_wind_data, status
|
331 |
-
else:
|
332 |
-
print("⚠️ GRIB processing failed, falling back to synthetic data")
|
333 |
-
return generate_synthetic_wind_data(), status
|
334 |
-
|
335 |
-
except Exception as e:
|
336 |
-
print(f"❌ Error in ECMWF wind data fetching: {e}")
|
337 |
-
print("🔄 Falling back to synthetic wind data...")
|
338 |
-
return generate_synthetic_wind_data(), f"❌ Error: {str(e)}"
|
339 |
-
|
340 |
-
def generate_synthetic_wind_data():
|
341 |
-
"""Fallback synthetic data - similar to working windmap"""
|
342 |
-
print("🎯 Generating synthetic wind data as fallback...")
|
343 |
-
|
344 |
-
wind_data = []
|
345 |
-
for lat in range(-60, 61, 15):
|
346 |
-
for lon in range(-180, 181, 20):
|
347 |
-
# Realistic wind patterns
|
348 |
-
lat_rad = np.radians(lat)
|
349 |
-
lon_rad = np.radians(lon)
|
350 |
-
|
351 |
-
# Jet stream + trade winds + noise
|
352 |
-
u = 15 * np.sin(lat_rad) + 5 * np.cos(lon_rad/2) + np.random.normal(0, 3)
|
353 |
-
v = 5 * np.cos(lat_rad) + 3 * np.sin(lon_rad/3) + np.random.normal(0, 2)
|
354 |
-
speed = np.sqrt(u*u + v*v)
|
355 |
-
|
356 |
-
wind_data.append({
|
357 |
-
'lat': lat,
|
358 |
-
'lon': lon,
|
359 |
-
'u': u,
|
360 |
-
'v': v,
|
361 |
-
'speed': speed,
|
362 |
-
'direction': np.degrees(np.arctan2(v, u))
|
363 |
-
})
|
364 |
-
|
365 |
-
print(f"✅ Generated {len(wind_data)} synthetic wind points")
|
366 |
-
return wind_data
|
367 |
-
|
368 |
-
def create_wind_arrow_map(wind_data):
|
369 |
-
"""
|
370 |
-
Create wind arrow visualization using folium
|
371 |
-
"""
|
372 |
-
try:
|
373 |
-
print("🌪️ Creating wind arrow visualization...")
|
374 |
-
|
375 |
-
if not wind_data:
|
376 |
-
return "<p>❌ No wind data available</p>"
|
377 |
-
|
378 |
-
# Create folium map
|
379 |
-
m = folium.Map(
|
380 |
-
location=[30, 0],
|
381 |
-
zoom_start=3,
|
382 |
-
tiles='OpenStreetMap'
|
383 |
-
)
|
384 |
-
|
385 |
-
# Create separate feature groups for each wind level
|
386 |
-
wind_10m_layer = folium.FeatureGroup(name="10m Wind Data", show=True)
|
387 |
-
wind_100m_layer = folium.FeatureGroup(name="100m Wind Data", show=True)
|
388 |
-
wind_particles_layer = folium.FeatureGroup(name="10m Wind Particles", show=False)
|
389 |
-
wind_hexagonal_layer = folium.FeatureGroup(name="10m Hexagonal Grid", show=False)
|
390 |
-
|
391 |
-
# Separate data by level
|
392 |
-
wind_10m = [w for w in wind_data if w.get('level') == '10m']
|
393 |
-
wind_100m = [w for w in wind_data if w.get('level') == '100m']
|
394 |
-
|
395 |
-
print(f"📊 Wind data: {len(wind_10m)} points at 10m, {len(wind_100m)} points at 100m")
|
396 |
-
|
397 |
-
# Debug: Show lat distribution in final wind_data
|
398 |
-
if wind_10m:
|
399 |
-
lats_10m = [w["lat"] for w in wind_10m[:10]]
|
400 |
-
print(f"🔍 First 10 lats in wind_10m: {lats_10m}")
|
401 |
-
unique_lats = set(w["lat"] for w in wind_10m)
|
402 |
-
print(f"🔍 Unique latitudes in 10m data: {sorted(list(unique_lats))[:10]}... (showing first 10)")
|
403 |
-
# Debug: Show sample coordinates
|
404 |
-
if wind_10m:
|
405 |
-
sample = wind_10m[0]
|
406 |
-
print(f"🗺️ Sample 10m wind: Lat={sample['lat']:.2f}, Lon={sample['lon']:.2f}, Speed={sample['speed']:.1f} m/s, Dir={sample['direction']:.0f}°")
|
407 |
-
if wind_100m:
|
408 |
-
sample = wind_100m[0]
|
409 |
-
print(f"🗺️ Sample 100m wind: Lat={sample['lat']:.2f}, Lon={sample['lon']:.2f}, Speed={sample['speed']:.1f} m/s, Dir={sample['direction']:.0f}°")
|
410 |
-
|
411 |
-
# Show ALL 10m wind points for complete global coverage and accurate positioning
|
412 |
-
print(f"🌍 Displaying ALL {len(wind_10m)} 10m wind points for full global coverage")
|
413 |
-
if wind_10m:
|
414 |
-
# Show coordinate distribution
|
415 |
-
lats_10m = [w['lat'] for w in wind_10m]
|
416 |
-
lons_10m = [w['lon'] for w in wind_10m]
|
417 |
-
print(f"🗺️ 10m lat range: {min(lats_10m):.1f}° to {max(lats_10m):.1f}°")
|
418 |
-
print(f"🗺️ 10m lon range: {min(lons_10m):.1f}° to {max(lons_10m):.1f}°")
|
419 |
-
print(f"🗺️ Sample 10m coordinates: {[(lats_10m[i], lons_10m[i]) for i in [0, len(lats_10m)//4, len(lats_10m)//2, 3*len(lats_10m)//4, -1]]}")
|
420 |
-
|
421 |
-
# Add ALL 10m wind points exactly as they exist in GRIB files
|
422 |
-
for i, wind in enumerate(wind_10m):
|
423 |
-
if wind['speed'] > 1: # Only show significant winds
|
424 |
-
|
425 |
-
# Add circle marker for 10m data (BLUE)
|
426 |
-
folium.CircleMarker(
|
427 |
-
location=[wind['lat'], wind['lon']],
|
428 |
-
radius=4,
|
429 |
-
color='blue',
|
430 |
-
fillColor='lightblue',
|
431 |
-
fillOpacity=0.6,
|
432 |
-
popup=f"<b>10m Wind #{i+1}</b><br>Speed: {wind['speed']:.1f} m/s<br>Direction: {wind['direction']:.0f}°<br>U: {wind['u']:.1f} m/s<br>V: {wind['v']:.1f} m/s<br>Lat: {wind['lat']:.4f}<br>Lon: {wind['lon']:.4f}",
|
433 |
-
tooltip=f"10m: {wind['speed']:.1f}m/s"
|
434 |
-
).add_to(wind_10m_layer)
|
435 |
-
|
436 |
-
# Add arrow marker for every 20th point to show direction without overcrowding
|
437 |
-
if i % 20 == 0:
|
438 |
-
# Calculate arrow length based on wind speed
|
439 |
-
arrow_length = min(wind['speed'] * 1.5, 15)
|
440 |
-
|
441 |
-
# Convert meteorological direction to mathematical angle
|
442 |
-
arrow_angle = (90 - wind['direction']) % 360
|
443 |
-
|
444 |
-
# Create simple arrow SVG (BLUE)
|
445 |
-
arrow_svg = f"""
|
446 |
-
<svg width="25" height="25" style="overflow: visible;">
|
447 |
-
<g transform="rotate({arrow_angle} 12.5 12.5)">
|
448 |
-
<line x1="4" y1="12.5" x2="{17}" y2="12.5" stroke="darkblue" stroke-width="2" marker-end="url(#arrowhead-blue{i})"/>
|
449 |
-
</g>
|
450 |
-
<defs>
|
451 |
-
<marker id="arrowhead-blue{i}" markerWidth="8" markerHeight="6"
|
452 |
-
refX="7" refY="3" orient="auto">
|
453 |
-
<polygon points="0 0, 8 3, 0 6" fill="darkblue"/>
|
454 |
-
</marker>
|
455 |
-
</defs>
|
456 |
-
</svg>
|
457 |
-
"""
|
458 |
-
|
459 |
-
folium.Marker(
|
460 |
-
location=[wind['lat'], wind['lon']],
|
461 |
-
icon=folium.DivIcon(
|
462 |
-
html=arrow_svg,
|
463 |
-
icon_size=(25, 25),
|
464 |
-
icon_anchor=(12.5, 12.5)
|
465 |
-
),
|
466 |
-
popup=f"<b>10m Arrow #{i+1}</b><br>Speed: {wind['speed']:.1f} m/s<br>Direction: {wind['direction']:.0f}°<br>Lat: {wind['lat']:.4f}<br>Lon: {wind['lon']:.4f}"
|
467 |
-
).add_to(wind_10m_layer)
|
468 |
-
|
469 |
-
# Show ALL 100m wind points for complete global coverage and accurate positioning
|
470 |
-
print(f"🌍 Displaying ALL {len(wind_100m)} 100m wind points for full global coverage")
|
471 |
-
if wind_100m:
|
472 |
-
# Show coordinate distribution
|
473 |
-
lats_100m = [w['lat'] for w in wind_100m]
|
474 |
-
lons_100m = [w['lon'] for w in wind_100m]
|
475 |
-
print(f"🗺️ 100m lat range: {min(lats_100m):.1f}° to {max(lats_100m):.1f}°")
|
476 |
-
print(f"🗺️ 100m lon range: {min(lons_100m):.1f}° to {max(lons_100m):.1f}°")
|
477 |
-
print(f"🗺️ Sample 100m coordinates: {[(lats_100m[i], lons_100m[i]) for i in [0, len(lats_100m)//4, len(lats_100m)//2, 3*len(lats_100m)//4, -1]]}")
|
478 |
-
|
479 |
-
# Add ALL 100m wind points exactly as they exist in GRIB files
|
480 |
-
for i, wind in enumerate(wind_100m):
|
481 |
-
if wind['speed'] > 1: # Only show significant winds
|
482 |
-
|
483 |
-
# Add circle marker for 100m data (RED)
|
484 |
-
folium.CircleMarker(
|
485 |
-
location=[wind['lat'], wind['lon']],
|
486 |
-
radius=4,
|
487 |
-
color='red',
|
488 |
-
fillColor='lightcoral',
|
489 |
-
fillOpacity=0.6,
|
490 |
-
popup=f"<b>100m Wind #{i+1}</b><br>Speed: {wind['speed']:.1f} m/s<br>Direction: {wind['direction']:.0f}°<br>U: {wind['u']:.1f} m/s<br>V: {wind['v']:.1f} m/s<br>Lat: {wind['lat']:.4f}<br>Lon: {wind['lon']:.4f}",
|
491 |
-
tooltip=f"100m: {wind['speed']:.1f}m/s"
|
492 |
-
).add_to(wind_100m_layer)
|
493 |
-
|
494 |
-
# Add arrow marker for every 20th point to show direction without overcrowding
|
495 |
-
if i % 20 == 0:
|
496 |
-
# Calculate arrow length based on wind speed
|
497 |
-
arrow_length = min(wind['speed'] * 1.5, 15)
|
498 |
-
|
499 |
-
# Convert meteorological direction to mathematical angle
|
500 |
-
arrow_angle = (90 - wind['direction']) % 360
|
501 |
-
|
502 |
-
# Create simple arrow SVG (RED)
|
503 |
-
arrow_svg = f"""
|
504 |
-
<svg width="25" height="25" style="overflow: visible;">
|
505 |
-
<g transform="rotate({arrow_angle} 12.5 12.5)">
|
506 |
-
<line x1="4" y1="12.5" x2="{17}" y2="12.5" stroke="darkred" stroke-width="2" marker-end="url(#arrowhead-red{i})"/>
|
507 |
-
</g>
|
508 |
-
<defs>
|
509 |
-
<marker id="arrowhead-red{i}" markerWidth="8" markerHeight="6"
|
510 |
-
refX="7" refY="3" orient="auto">
|
511 |
-
<polygon points="0 0, 8 3, 0 6" fill="darkred"/>
|
512 |
-
</marker>
|
513 |
-
</defs>
|
514 |
-
</svg>
|
515 |
-
"""
|
516 |
-
|
517 |
-
folium.Marker(
|
518 |
-
location=[wind['lat'], wind['lon']],
|
519 |
-
icon=folium.DivIcon(
|
520 |
-
html=arrow_svg,
|
521 |
-
icon_size=(25, 25),
|
522 |
-
icon_anchor=(12.5, 12.5)
|
523 |
-
),
|
524 |
-
popup=f"<b>100m Arrow #{i+1}</b><br>Speed: {wind['speed']:.1f} m/s<br>Direction: {wind['direction']:.0f}°<br>Lat: {wind['lat']:.4f}<br>Lon: {wind['lon']:.4f}"
|
525 |
-
).add_to(wind_100m_layer)
|
526 |
-
|
527 |
-
# Create TimestampedGeoJson wind particle animation (windy-style)
|
528 |
-
if wind_10m:
|
529 |
-
print(f"🌪️ Creating TimestampedGeoJson wind particle animation for {len(wind_10m)} wind points")
|
530 |
-
|
531 |
-
def create_particle_trajectories(wind_data, num_particles=1000, trajectory_length=20, time_steps=30):
|
532 |
-
"""Create particle trajectories for TimestampedGeoJson animation"""
|
533 |
-
|
534 |
-
features = []
|
535 |
-
particle_id = 0
|
536 |
-
|
537 |
-
# Create multiple particles across the wind field
|
538 |
-
for wind in wind_data[::max(1, len(wind_data)//200)]: # Sample wind points
|
539 |
-
if wind['speed'] > 1:
|
540 |
-
|
541 |
-
# Create multiple particles per wind point with random distribution
|
542 |
-
for _ in range(5): # 5 particles per wind point
|
543 |
-
if particle_id >= num_particles:
|
544 |
-
break
|
545 |
-
|
546 |
-
# Random starting offset for particle distribution
|
547 |
-
start_lat = wind['lat'] + np.random.uniform(-0.2, 0.2)
|
548 |
-
start_lon = wind['lon'] + np.random.uniform(-0.2, 0.2)
|
549 |
-
|
550 |
-
# Ensure valid coordinates
|
551 |
-
start_lat = max(-90, min(90, start_lat))
|
552 |
-
start_lon = max(-180, min(180, start_lon))
|
553 |
-
|
554 |
-
# Create trajectory points based on wind field
|
555 |
-
trajectory_coords = []
|
556 |
-
current_lat, current_lon = start_lat, start_lon
|
557 |
-
|
558 |
-
for step in range(trajectory_length):
|
559 |
-
# Add current position
|
560 |
-
trajectory_coords.append([current_lon, current_lat])
|
561 |
-
|
562 |
-
# Move particle based on wind field (simplified advection)
|
563 |
-
# Scale movement by wind speed and add some randomness
|
564 |
-
movement_scale = 0.01 * (wind['speed'] / 10) + np.random.uniform(-0.005, 0.005)
|
565 |
-
|
566 |
-
direction_rad = np.radians(wind['direction'])
|
567 |
-
lat_movement = movement_scale * np.sin(direction_rad)
|
568 |
-
lon_movement = movement_scale * np.cos(direction_rad)
|
569 |
-
|
570 |
-
current_lat += lat_movement
|
571 |
-
current_lon += lon_movement
|
572 |
-
|
573 |
-
# Keep particles in bounds
|
574 |
-
current_lat = max(-90, min(90, current_lat))
|
575 |
-
current_lon = max(-180, min(180, current_lon))
|
576 |
-
|
577 |
-
# Create timestamped features for this particle trajectory
|
578 |
-
for time_step in range(time_steps):
|
579 |
-
coord_index = int((time_step / time_steps) * (len(trajectory_coords) - 1))
|
580 |
-
|
581 |
-
# High contrast particle styling based on speed
|
582 |
-
if wind['speed'] < 5:
|
583 |
-
color = '#00ff41' # Bright green for low speed
|
584 |
-
elif wind['speed'] < 15:
|
585 |
-
color = '#ffff00' # Bright yellow for medium speed
|
586 |
-
else:
|
587 |
-
color = '#ff4136' # Bright red for high speed
|
588 |
-
|
589 |
-
# Create feature with timestamp
|
590 |
-
feature = {
|
591 |
-
"type": "Feature",
|
592 |
-
"geometry": {
|
593 |
-
"type": "Point",
|
594 |
-
"coordinates": trajectory_coords[coord_index]
|
595 |
-
},
|
596 |
-
"properties": {
|
597 |
-
"time": f"2024-01-01T00:{time_step:02d}:00", # Timestamp for animation
|
598 |
-
"style": {
|
599 |
-
"color": color,
|
600 |
-
"fillColor": color,
|
601 |
-
"fillOpacity": 0.8,
|
602 |
-
"weight": 2,
|
603 |
-
"radius": max(3, min(8, int(wind['speed'] / 2)))
|
604 |
-
},
|
605 |
-
"popup": f"Particle {particle_id}<br>Speed: {wind['speed']:.1f} m/s<br>Direction: {wind['direction']:.0f}°",
|
606 |
-
"particle_id": particle_id,
|
607 |
-
"wind_speed": wind['speed'],
|
608 |
-
"wind_direction": wind['direction']
|
609 |
-
}
|
610 |
-
}
|
611 |
-
features.append(feature)
|
612 |
-
|
613 |
-
particle_id += 1
|
614 |
-
|
615 |
-
return features
|
616 |
-
|
617 |
-
# Generate particle trajectories
|
618 |
-
particle_features = create_particle_trajectories(wind_10m)
|
619 |
-
|
620 |
-
# Create TimestampedGeoJson for animated particles
|
621 |
-
from folium.plugins import TimestampedGeoJson
|
622 |
-
|
623 |
-
timestamped_data = {
|
624 |
-
"type": "FeatureCollection",
|
625 |
-
"features": particle_features
|
626 |
-
}
|
627 |
-
|
628 |
-
# Add TimestampedGeoJson to map
|
629 |
-
TimestampedGeoJson(
|
630 |
-
timestamped_data,
|
631 |
-
period="PT1S",
|
632 |
-
auto_play=True,
|
633 |
-
loop=True,
|
634 |
-
max_speed=3,
|
635 |
-
loop_button=True,
|
636 |
-
time_slider_drag_update=True
|
637 |
-
).add_to(wind_particles_layer)
|
638 |
-
|
639 |
-
print(f"🌪️ Created {len(particle_features)} timestamped particle positions")
|
640 |
-
|
641 |
-
# Add info marker
|
642 |
-
folium.Marker(
|
643 |
-
location=[0, 0],
|
644 |
-
icon=folium.DivIcon(
|
645 |
-
html=f'<div style="background: rgba(0,150,0,0.8); color: white; padding: 5px; border-radius: 3px; font-size: 12px;">TimestampedGeoJson Particles: {len(set(f["properties"]["particle_id"] for f in particle_features))} Active</div>',
|
646 |
-
icon_size=(350, 30),
|
647 |
-
icon_anchor=(175, 15)
|
648 |
-
),
|
649 |
-
popup=f"<b>Wind Particle Animation</b><br>Particles: {len(set(f['properties']['particle_id'] for f in particle_features))}<br>High contrast windy-style animation<br>Auto-playing with 30s loop"
|
650 |
-
).add_to(wind_particles_layer)
|
651 |
-
|
652 |
-
# Create hexagonal grid wind visualization (H3-inspired)
|
653 |
-
if wind_10m:
|
654 |
-
print(f"🔷 Creating hexagonal grid wind visualization for {len(wind_10m)} wind points")
|
655 |
-
|
656 |
-
def generate_hexagonal_grid(bounds, resolution=0.5):
|
657 |
-
"""Generate hexagonal grid points within bounds"""
|
658 |
-
lat_min, lat_max, lon_min, lon_max = bounds
|
659 |
-
|
660 |
-
hex_points = []
|
661 |
-
hex_spacing = resolution
|
662 |
-
row_offset = hex_spacing * np.sqrt(3) / 2
|
663 |
-
|
664 |
-
lat = lat_min
|
665 |
-
row = 0
|
666 |
-
while lat <= lat_max:
|
667 |
-
# Offset every other row for hexagonal pattern
|
668 |
-
lon_start = lon_min + (hex_spacing / 2 if row % 2 else 0)
|
669 |
-
lon = lon_start
|
670 |
-
|
671 |
-
while lon <= lon_max:
|
672 |
-
hex_points.append((lat, lon))
|
673 |
-
lon += hex_spacing
|
674 |
-
|
675 |
-
lat += row_offset
|
676 |
-
row += 1
|
677 |
-
|
678 |
-
return hex_points
|
679 |
-
|
680 |
-
def interpolate_wind_at_point(target_lat, target_lon, wind_data, max_distance=2.0):
|
681 |
-
"""Interpolate wind values at a specific point using inverse distance weighting"""
|
682 |
-
if not wind_data:
|
683 |
-
return None
|
684 |
-
|
685 |
-
nearby_winds = []
|
686 |
-
weights = []
|
687 |
-
|
688 |
-
for wind in wind_data:
|
689 |
-
# Calculate distance using simple lat/lon difference
|
690 |
-
dist = np.sqrt((wind['lat'] - target_lat)**2 + (wind['lon'] - target_lon)**2)
|
691 |
-
|
692 |
-
if dist < max_distance: # Only use nearby points
|
693 |
-
nearby_winds.append(wind)
|
694 |
-
weights.append(1 / (dist + 0.01)) # Avoid division by zero
|
695 |
-
|
696 |
-
if not weights:
|
697 |
-
return None
|
698 |
-
|
699 |
-
# Weighted average of nearby wind points
|
700 |
-
total_weight = sum(weights)
|
701 |
-
weighted_u = sum(w * wind['u'] for w, wind in zip(weights, nearby_winds)) / total_weight
|
702 |
-
weighted_v = sum(w * wind['v'] for w, wind in zip(weights, nearby_winds)) / total_weight
|
703 |
-
weighted_speed = np.sqrt(weighted_u**2 + weighted_v**2)
|
704 |
-
weighted_direction = np.degrees(np.arctan2(weighted_v, weighted_u))
|
705 |
-
|
706 |
-
if weighted_direction < 0:
|
707 |
-
weighted_direction += 360
|
708 |
-
|
709 |
-
return {
|
710 |
-
'u': weighted_u,
|
711 |
-
'v': weighted_v,
|
712 |
-
'speed': weighted_speed,
|
713 |
-
'direction': weighted_direction
|
714 |
-
}
|
715 |
-
|
716 |
-
# Calculate bounds from wind data
|
717 |
-
lats = [w['lat'] for w in wind_10m]
|
718 |
-
lons = [w['lon'] for w in wind_10m]
|
719 |
-
bounds = (min(lats), max(lats), min(lons), max(lons))
|
720 |
-
|
721 |
-
# Generate hexagonal grid with appropriate resolution
|
722 |
-
hex_resolution = 5.0 # Larger spacing for better performance
|
723 |
-
hex_points = generate_hexagonal_grid(bounds, hex_resolution)
|
724 |
-
|
725 |
-
print(f"🔷 Generated {len(hex_points)} hexagonal grid points")
|
726 |
-
|
727 |
-
# Create wind arrows on hexagonal grid
|
728 |
-
hex_arrow_count = 0
|
729 |
-
for hex_lat, hex_lon in hex_points:
|
730 |
-
if hex_arrow_count >= 150: # Reduced limit for better performance
|
731 |
-
break
|
732 |
-
|
733 |
-
# Interpolate wind at this hexagonal grid point
|
734 |
-
interpolated_wind = interpolate_wind_at_point(hex_lat, hex_lon, wind_10m, max_distance=3.0)
|
735 |
-
|
736 |
-
if interpolated_wind and interpolated_wind['speed'] > 1:
|
737 |
-
# Create hexagonal-based arrow
|
738 |
-
arrow_length = min(interpolated_wind['speed'] * 2, 20)
|
739 |
-
arrow_angle = (90 - interpolated_wind['direction']) % 360
|
740 |
-
|
741 |
-
# Speed-based color (green to yellow to red)
|
742 |
-
if interpolated_wind['speed'] < 5:
|
743 |
-
arrow_color = f"rgb(0, {int(interpolated_wind['speed'] * 50)}, 0)"
|
744 |
-
elif interpolated_wind['speed'] < 15:
|
745 |
-
yellow_intensity = int((interpolated_wind['speed'] - 5) * 25)
|
746 |
-
arrow_color = f"rgb({yellow_intensity}, 255, 0)"
|
747 |
-
else:
|
748 |
-
red_intensity = min(255, int(interpolated_wind['speed'] * 10))
|
749 |
-
arrow_color = f"rgb(255, {255 - red_intensity}, 0)"
|
750 |
-
|
751 |
-
# Create hexagonal background with arrow
|
752 |
-
hex_svg = f"""
|
753 |
-
<svg width="40" height="40" style="overflow: visible;">
|
754 |
-
<defs>
|
755 |
-
<marker id="hexarrow{hex_arrow_count}" markerWidth="8" markerHeight="6"
|
756 |
-
refX="7" refY="3" orient="auto">
|
757 |
-
<polygon points="0 0, 8 3, 0 6" fill="{arrow_color}"/>
|
758 |
-
</marker>
|
759 |
-
</defs>
|
760 |
-
<!-- Hexagonal background -->
|
761 |
-
<polygon points="20,5 30,12 30,28 20,35 10,28 10,12"
|
762 |
-
fill="rgba(50, 150, 50, 0.2)"
|
763 |
-
stroke="rgba(0, 100, 0, 0.6)"
|
764 |
-
stroke-width="1"/>
|
765 |
-
<!-- Wind arrow -->
|
766 |
-
<g transform="rotate({arrow_angle} 20 20)">
|
767 |
-
<line x1="8" y1="20" x2="{8 + arrow_length}" y2="20"
|
768 |
-
stroke="{arrow_color}"
|
769 |
-
stroke-width="3"
|
770 |
-
marker-end="url(#hexarrow{hex_arrow_count})"/>
|
771 |
-
</g>
|
772 |
-
<!-- Center dot -->
|
773 |
-
<circle cx="20" cy="20" r="2" fill="rgba(255, 255, 255, 0.8)"/>
|
774 |
-
</svg>
|
775 |
-
"""
|
776 |
-
|
777 |
-
folium.Marker(
|
778 |
-
location=[hex_lat, hex_lon],
|
779 |
-
icon=folium.DivIcon(
|
780 |
-
html=hex_svg,
|
781 |
-
icon_size=(40, 40),
|
782 |
-
icon_anchor=(20, 20),
|
783 |
-
class_name="hexagonal-wind-marker"
|
784 |
-
),
|
785 |
-
popup=f"<b>Hexagonal Grid Wind</b><br>Speed: {interpolated_wind['speed']:.1f} m/s<br>Direction: {interpolated_wind['direction']:.0f}°<br>U: {interpolated_wind['u']:.1f} m/s<br>V: {interpolated_wind['v']:.1f} m/s<br>Grid: ({hex_lat:.2f}, {hex_lon:.2f})"
|
786 |
-
).add_to(wind_hexagonal_layer)
|
787 |
-
|
788 |
-
hex_arrow_count += 1
|
789 |
-
|
790 |
-
print(f"🔷 Created {hex_arrow_count} hexagonal wind arrows")
|
791 |
-
|
792 |
-
# Add the feature groups to the map
|
793 |
-
wind_10m_layer.add_to(m)
|
794 |
-
wind_100m_layer.add_to(m)
|
795 |
-
wind_particles_layer.add_to(m)
|
796 |
-
wind_hexagonal_layer.add_to(m)
|
797 |
-
|
798 |
-
# Add layer control to toggle layers on/off
|
799 |
-
folium.LayerControl(
|
800 |
-
position='topleft',
|
801 |
-
collapsed=False
|
802 |
-
).add_to(m)
|
803 |
-
|
804 |
-
# Add legend
|
805 |
-
legend_html = '''
|
806 |
-
<div style="position: fixed;
|
807 |
-
top: 10px; right: 10px; width: 260px; height: 200px;
|
808 |
-
background-color: white; border:2px solid grey; z-index:9999;
|
809 |
-
font-size:12px; padding: 10px">
|
810 |
-
<h4>ECMWF Wind Data Legend</h4>
|
811 |
-
<p><span style="color:blue;">●</span> 10m Wind Data (Blue)</p>
|
812 |
-
<p><span style="color:red;">●</span> 100m Wind Data (Red)</p>
|
813 |
-
<p><span style="color:darkblue;">→</span> 10m Wind Arrows</p>
|
814 |
-
<p><span style="color:darkred;">→</span> 100m Wind Arrows</p>
|
815 |
-
<p><span style="color:cyan;">◦</span> 10m Wind Particles (Velocity)</p>
|
816 |
-
<p><span style="color:green;">⬡</span> 10m Hexagonal Grid (H3-style)</p>
|
817 |
-
<p><small>All points from GRIB files<br>Arrows show every 20th point<br>Particles show velocity field flow<br>Hexagons use interpolated wind data<br><br>Use layer control (top-left) to toggle layers</small></p>
|
818 |
-
</div>
|
819 |
-
'''
|
820 |
-
m.get_root().html.add_child(folium.Element(legend_html))
|
821 |
-
|
822 |
-
# Add zoom-responsive CSS and JavaScript for particle scaling
|
823 |
-
zoom_responsive_css = '''
|
824 |
-
<style>
|
825 |
-
.wind-particle-marker {
|
826 |
-
transition: transform 0.3s ease;
|
827 |
-
}
|
828 |
-
|
829 |
-
.hexagonal-wind-marker {
|
830 |
-
transition: transform 0.3s ease;
|
831 |
-
}
|
832 |
-
|
833 |
-
/* Base scaling for different zoom levels */
|
834 |
-
.leaflet-zoom-anim .wind-particle-marker,
|
835 |
-
.leaflet-zoom-anim .hexagonal-wind-marker {
|
836 |
-
transition: none;
|
837 |
-
}
|
838 |
-
|
839 |
-
/* Zoom level adjustments */
|
840 |
-
.leaflet-container[data-zoom="1"] .wind-particle-marker svg,
|
841 |
-
.leaflet-container[data-zoom="2"] .wind-particle-marker svg,
|
842 |
-
.leaflet-container[data-zoom="1"] .hexagonal-wind-marker svg,
|
843 |
-
.leaflet-container[data-zoom="2"] .hexagonal-wind-marker svg {
|
844 |
-
transform: scale(0.3);
|
845 |
-
}
|
846 |
-
|
847 |
-
.leaflet-container[data-zoom="3"] .wind-particle-marker svg,
|
848 |
-
.leaflet-container[data-zoom="4"] .wind-particle-marker svg,
|
849 |
-
.leaflet-container[data-zoom="3"] .hexagonal-wind-marker svg,
|
850 |
-
.leaflet-container[data-zoom="4"] .hexagonal-wind-marker svg {
|
851 |
-
transform: scale(0.5);
|
852 |
-
}
|
853 |
-
|
854 |
-
.leaflet-container[data-zoom="5"] .wind-particle-marker svg,
|
855 |
-
.leaflet-container[data-zoom="6"] .wind-particle-marker svg,
|
856 |
-
.leaflet-container[data-zoom="5"] .hexagonal-wind-marker svg,
|
857 |
-
.leaflet-container[data-zoom="6"] .hexagonal-wind-marker svg {
|
858 |
-
transform: scale(0.7);
|
859 |
-
}
|
860 |
-
|
861 |
-
.leaflet-container[data-zoom="7"] .wind-particle-marker svg,
|
862 |
-
.leaflet-container[data-zoom="8"] .wind-particle-marker svg,
|
863 |
-
.leaflet-container[data-zoom="7"] .hexagonal-wind-marker svg,
|
864 |
-
.leaflet-container[data-zoom="8"] .hexagonal-wind-marker svg {
|
865 |
-
transform: scale(0.9);
|
866 |
-
}
|
867 |
-
|
868 |
-
.leaflet-container[data-zoom="9"] .wind-particle-marker svg,
|
869 |
-
.leaflet-container[data-zoom="10"] .wind-particle-marker svg,
|
870 |
-
.leaflet-container[data-zoom="9"] .hexagonal-wind-marker svg,
|
871 |
-
.leaflet-container[data-zoom="10"] .hexagonal-wind-marker svg {
|
872 |
-
transform: scale(1.0);
|
873 |
-
}
|
874 |
-
|
875 |
-
.leaflet-container[data-zoom="11"] .wind-particle-marker svg,
|
876 |
-
.leaflet-container[data-zoom="12"] .wind-particle-marker svg,
|
877 |
-
.leaflet-container[data-zoom="11"] .hexagonal-wind-marker svg,
|
878 |
-
.leaflet-container[data-zoom="12"] .hexagonal-wind-marker svg {
|
879 |
-
transform: scale(1.2);
|
880 |
-
}
|
881 |
-
|
882 |
-
.leaflet-container[data-zoom="13"] .wind-particle-marker svg,
|
883 |
-
.leaflet-container[data-zoom="14"] .wind-particle-marker svg,
|
884 |
-
.leaflet-container[data-zoom="13"] .hexagonal-wind-marker svg,
|
885 |
-
.leaflet-container[data-zoom="14"] .hexagonal-wind-marker svg {
|
886 |
-
transform: scale(1.5);
|
887 |
-
}
|
888 |
-
|
889 |
-
.leaflet-container[data-zoom="15"] .wind-particle-marker svg,
|
890 |
-
.leaflet-container[data-zoom="16"] .wind-particle-marker svg,
|
891 |
-
.leaflet-container[data-zoom="17"] .wind-particle-marker svg,
|
892 |
-
.leaflet-container[data-zoom="18"] .wind-particle-marker svg,
|
893 |
-
.leaflet-container[data-zoom="15"] .hexagonal-wind-marker svg,
|
894 |
-
.leaflet-container[data-zoom="16"] .hexagonal-wind-marker svg,
|
895 |
-
.leaflet-container[data-zoom="17"] .hexagonal-wind-marker svg,
|
896 |
-
.leaflet-container[data-zoom="18"] .hexagonal-wind-marker svg {
|
897 |
-
transform: scale(2.0);
|
898 |
-
}
|
899 |
-
</style>
|
900 |
-
'''
|
901 |
-
m.get_root().html.add_child(folium.Element(zoom_responsive_css))
|
902 |
-
|
903 |
-
# Add JavaScript for zoom-responsive particle scaling
|
904 |
-
zoom_responsive_js = '''
|
905 |
-
<script>
|
906 |
-
// Add zoom-responsive particle scaling
|
907 |
-
document.addEventListener('DOMContentLoaded', function() {
|
908 |
-
// Wait for the map to be ready
|
909 |
-
setTimeout(function() {
|
910 |
-
var mapElement = document.querySelector('.folium-map');
|
911 |
-
if (mapElement && mapElement._leaflet_map) {
|
912 |
-
var map = mapElement._leaflet_map;
|
913 |
-
var mapContainer = map.getContainer();
|
914 |
-
|
915 |
-
function updateZoomClass() {
|
916 |
-
var zoom = Math.round(map.getZoom());
|
917 |
-
mapContainer.setAttribute('data-zoom', zoom);
|
918 |
-
}
|
919 |
-
|
920 |
-
// Set initial zoom
|
921 |
-
updateZoomClass();
|
922 |
-
|
923 |
-
// Update on zoom change
|
924 |
-
map.on('zoomend', updateZoomClass);
|
925 |
-
map.on('zoom', updateZoomClass);
|
926 |
-
|
927 |
-
console.log('Wind particle zoom responsiveness initialized');
|
928 |
-
}
|
929 |
-
}, 1000);
|
930 |
-
});
|
931 |
-
</script>
|
932 |
-
'''
|
933 |
-
m.get_root().html.add_child(folium.Element(zoom_responsive_js))
|
934 |
-
|
935 |
-
return m._repr_html_()
|
936 |
-
|
937 |
-
except Exception as e:
|
938 |
-
return f"<p>❌ Error creating wind map: {str(e)}</p>"
|
939 |
-
|
940 |
-
def fetch_and_visualize_winds(resolution=8):
|
941 |
-
"""Main function to fetch and visualize wind data"""
|
942 |
-
try:
|
943 |
-
status_msg = "🌍 Fetching real-time ECMWF wind data at 10m and 100m levels..."
|
944 |
-
|
945 |
-
# Fetch real wind data
|
946 |
-
wind_data, download_status = get_wind_data(resolution=resolution)
|
947 |
-
|
948 |
-
if not wind_data:
|
949 |
-
return "❌ No wind data could be fetched", ""
|
950 |
-
|
951 |
-
# Create wind arrow map
|
952 |
-
wind_map = create_wind_arrow_map(wind_data)
|
953 |
-
|
954 |
-
# Count data by level
|
955 |
-
wind_10m_count = len([w for w in wind_data if w.get('level') == '10m'])
|
956 |
-
wind_100m_count = len([w for w in wind_data if w.get('level') == '100m'])
|
957 |
-
|
958 |
-
status_msg = f"""✅ ECMWF Wind Data Successfully Retrieved!
|
959 |
-
|
960 |
-
{download_status}
|
961 |
-
|
962 |
-
📊 Data Summary:
|
963 |
-
• Total wind measurements: {len(wind_data)} points
|
964 |
-
• 10m wind data: {wind_10m_count} points
|
965 |
-
• 100m wind data: {wind_100m_count} points
|
966 |
-
• Data source: ECMWF Operational Forecasts
|
967 |
-
• Resolution: 0.25° (~25km) ECMWF grid
|
968 |
-
• Grid spacing: {resolution}° display resolution
|
969 |
-
• Update time: {datetime.now().strftime('%H:%M:%S UTC')}
|
970 |
-
|
971 |
-
🏹 Wind Arrow Visualization:
|
972 |
-
• Blue arrows = 10m wind data
|
973 |
-
• Red arrows = 100m wind data
|
974 |
-
• Arrow length = wind speed
|
975 |
-
• Arrow direction = wind direction
|
976 |
-
• Click arrows for detailed wind info (U/V components)
|
977 |
-
• Interactive map with zoom/pan controls
|
978 |
-
|
979 |
-
🎯 Features:
|
980 |
-
• Real ECMWF GRIB file processing
|
981 |
-
• Multi-level wind comparison (10m vs 100m)
|
982 |
-
• Professional meteorological data
|
983 |
-
• Same data source as earth.nullschool.net
|
984 |
-
• Clean arrow-based visualization"""
|
985 |
-
|
986 |
-
return status_msg, wind_map
|
987 |
-
|
988 |
-
except Exception as e:
|
989 |
-
return f"❌ Error: {str(e)}", ""
|
990 |
-
|
991 |
-
# Create Gradio interface
|
992 |
-
with gr.Blocks(title="ECMWF Wind Visualization", theme=gr.themes.Soft()) as app:
|
993 |
-
gr.Markdown("""
|
994 |
-
# 🌪️ ECMWF Wind Visualization
|
995 |
-
## Multi-Level Wind Data with Arrow Visualization
|
996 |
-
|
997 |
-
**Features:**
|
998 |
-
- 🌍 Real ECMWF operational forecast data
|
999 |
-
- 🏹 Wind arrows at 10m and 100m levels
|
1000 |
-
- 📊 Professional meteorological visualization
|
1001 |
-
- 🎯 Same data source as earth.nullschool.net
|
1002 |
-
- 📈 Compare wind speeds at different altitudes
|
1003 |
-
""")
|
1004 |
-
|
1005 |
-
with gr.Row():
|
1006 |
-
with gr.Column(scale=1):
|
1007 |
-
gr.Markdown("### ⚙️ Controls")
|
1008 |
-
|
1009 |
-
resolution_slider = gr.Slider(
|
1010 |
-
minimum=8,
|
1011 |
-
maximum=20,
|
1012 |
-
value=12,
|
1013 |
-
step=2,
|
1014 |
-
label="Grid Resolution (degrees)",
|
1015 |
-
info="Lower = more detail, slower loading"
|
1016 |
-
)
|
1017 |
-
|
1018 |
-
fetch_btn = gr.Button(
|
1019 |
-
"🏹 Fetch ECMWF Wind Data & Visualize",
|
1020 |
-
variant="primary",
|
1021 |
-
size="lg"
|
1022 |
-
)
|
1023 |
-
|
1024 |
-
status_output = gr.Textbox(
|
1025 |
-
label="Status",
|
1026 |
-
lines=20,
|
1027 |
-
interactive=False
|
1028 |
-
)
|
1029 |
-
|
1030 |
-
with gr.Column(scale=2):
|
1031 |
-
gr.Markdown("### 🗺️ Wind Arrow Visualization")
|
1032 |
-
wind_map_output = gr.HTML(
|
1033 |
-
label="ECMWF Wind Data Arrows",
|
1034 |
-
value="<p>Click 'Fetch ECMWF Wind Data' to see wind arrows at 10m and 100m levels!</p>"
|
1035 |
-
)
|
1036 |
-
|
1037 |
-
# Event handlers
|
1038 |
-
fetch_btn.click(
|
1039 |
-
fetch_and_visualize_winds,
|
1040 |
-
inputs=[resolution_slider],
|
1041 |
-
outputs=[status_output, wind_map_output]
|
1042 |
-
)
|
1043 |
-
|
1044 |
-
gr.Markdown("""
|
1045 |
-
---
|
1046 |
-
### 📖 About This App
|
1047 |
-
|
1048 |
-
This application uses **real ECMWF operational forecast data** to visualize wind patterns at multiple atmospheric levels.
|
1049 |
-
|
1050 |
-
**Technical Features:**
|
1051 |
-
- Professional ECMWF operational forecasts at 10m and 100m levels
|
1052 |
-
- Same data source as earth.nullschool.net
|
1053 |
-
- Wind arrow visualization showing speed and direction
|
1054 |
-
- Real U/V wind component processing from GRIB files
|
1055 |
-
- Multi-level wind comparison capabilities
|
1056 |
-
- Color-coded wind arrows (blue=10m, red=100m)
|
1057 |
-
- Interactive Leaflet map integration
|
1058 |
-
|
1059 |
-
**Data Source:** ECMWF Open Data - Professional meteorological forecasts at 0.25° resolution
|
1060 |
-
""")
|
1061 |
-
|
1062 |
-
if __name__ == "__main__":
|
1063 |
-
app.launch(
|
1064 |
-
server_name="0.0.0.0",
|
1065 |
-
server_port=7860,
|
1066 |
-
share=False
|
1067 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py.backup
DELETED
@@ -1,692 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
"""
|
3 |
-
ECMWF Wind Visualization with Real Data
|
4 |
-
Based on proven particle technology from windmap project
|
5 |
-
"""
|
6 |
-
|
7 |
-
import gradio as gr
|
8 |
-
import numpy as np
|
9 |
-
import pandas as pd
|
10 |
-
import folium
|
11 |
-
import requests
|
12 |
-
import json
|
13 |
-
import time
|
14 |
-
import tempfile
|
15 |
-
import os
|
16 |
-
import xarray as xr
|
17 |
-
from datetime import datetime, timedelta
|
18 |
-
import warnings
|
19 |
-
warnings.filterwarnings('ignore')
|
20 |
-
|
21 |
-
try:
|
22 |
-
from ecmwf.opendata import Client as OpenDataClient
|
23 |
-
OPENDATA_AVAILABLE = True
|
24 |
-
except ImportError:
|
25 |
-
OPENDATA_AVAILABLE = False
|
26 |
-
|
27 |
-
class ECMWFWindDataFetcher:
|
28 |
-
def __init__(self):
|
29 |
-
self.temp_dir = tempfile.mkdtemp()
|
30 |
-
self.client = None
|
31 |
-
if OPENDATA_AVAILABLE:
|
32 |
-
try:
|
33 |
-
self.client = OpenDataClient()
|
34 |
-
except:
|
35 |
-
self.client = None
|
36 |
-
|
37 |
-
# AWS S3 direct access URLs (completely free)
|
38 |
-
self.aws_base_url = "https://ecmwf-forecasts.s3.eu-central-1.amazonaws.com"
|
39 |
-
|
40 |
-
def get_latest_forecast_info(self):
|
41 |
-
"""Get the latest available ECMWF forecast run info"""
|
42 |
-
now = datetime.utcnow()
|
43 |
-
|
44 |
-
# ECMWF runs at 00, 06, 12, 18 UTC
|
45 |
-
forecast_hours = [0, 6, 12, 18]
|
46 |
-
|
47 |
-
# Find the most recent forecast run
|
48 |
-
for hours_back in range(0, 24, 6):
|
49 |
-
check_time = now - timedelta(hours=hours_back)
|
50 |
-
run_hour = max([h for h in forecast_hours if h <= check_time.hour])
|
51 |
-
|
52 |
-
forecast_time = check_time.replace(hour=run_hour, minute=0, second=0, microsecond=0)
|
53 |
-
|
54 |
-
# ECMWF data is usually available 2-4 hours after run time
|
55 |
-
if now >= (forecast_time + timedelta(hours=3)):
|
56 |
-
date_str = forecast_time.strftime("%Y%m%d")
|
57 |
-
time_str = f"{run_hour:02d}"
|
58 |
-
return date_str, time_str, forecast_time
|
59 |
-
|
60 |
-
# Fallback to previous day
|
61 |
-
yesterday = now - timedelta(days=1)
|
62 |
-
return yesterday.strftime("%Y%m%d"), "18", yesterday.replace(hour=18)
|
63 |
-
|
64 |
-
def download_ecmwf_wind_data(self, step=0):
|
65 |
-
"""Download ECMWF wind data at multiple levels"""
|
66 |
-
date_str, time_str, run_time = self.get_latest_forecast_info()
|
67 |
-
|
68 |
-
# Download 10m wind components
|
69 |
-
u10_file, u10_status = self.download_parameter('10u', step)
|
70 |
-
v10_file, v10_status = self.download_parameter('10v', step)
|
71 |
-
|
72 |
-
# Download 100m wind components
|
73 |
-
u100_file, u100_status = self.download_parameter('100u', step)
|
74 |
-
v100_file, v100_status = self.download_parameter('100v', step)
|
75 |
-
|
76 |
-
status_msg = f"✅ ECMWF Wind Data Retrieved!\nRun: {date_str} {time_str}z, Step: +{step}h\n"
|
77 |
-
status_msg += f"10m U-component: {u10_status}\n10m V-component: {v10_status}\n"
|
78 |
-
status_msg += f"100m U-component: {u100_status}\n100m V-component: {v100_status}"
|
79 |
-
|
80 |
-
# Return both levels
|
81 |
-
wind_data = {
|
82 |
-
'10m': (u10_file, v10_file) if u10_file and v10_file else (None, None),
|
83 |
-
'100m': (u100_file, v100_file) if u100_file and v100_file else (None, None)
|
84 |
-
}
|
85 |
-
|
86 |
-
return wind_data, status_msg
|
87 |
-
|
88 |
-
def download_parameter(self, parameter, step=0):
|
89 |
-
"""Download a specific ECMWF parameter using multiple methods"""
|
90 |
-
date_str, time_str, run_time = self.get_latest_forecast_info()
|
91 |
-
|
92 |
-
# Method 1: Try ecmwf-opendata client (most reliable)
|
93 |
-
if OPENDATA_AVAILABLE and self.client:
|
94 |
-
try:
|
95 |
-
filename = os.path.join(self.temp_dir, f'ecmwf_{parameter}_{step}h_{datetime.now().strftime("%Y%m%d_%H%M%S")}.grib')
|
96 |
-
|
97 |
-
self.client.retrieve(
|
98 |
-
type="fc",
|
99 |
-
param=parameter,
|
100 |
-
step=step,
|
101 |
-
target=filename
|
102 |
-
)
|
103 |
-
|
104 |
-
if os.path.exists(filename) and os.path.getsize(filename) > 1000:
|
105 |
-
return filename, f"✅ Downloaded via ECMWF client"
|
106 |
-
|
107 |
-
except Exception as e:
|
108 |
-
print(f"ECMWF client method failed: {str(e)}")
|
109 |
-
|
110 |
-
# Method 2: Direct AWS S3 access (backup method)
|
111 |
-
try:
|
112 |
-
step_str = f"{step:03d}"
|
113 |
-
filename = f"{date_str}{time_str}0000-{step_str}h-oper-fc.grib2"
|
114 |
-
url = f"{self.aws_base_url}/{date_str}/{time_str}z/0p25/oper/{filename}"
|
115 |
-
|
116 |
-
response = requests.get(url, timeout=120, stream=True)
|
117 |
-
if response.status_code == 200:
|
118 |
-
local_file = os.path.join(self.temp_dir, f'ecmwf_aws_{parameter}_{step}h.grib2')
|
119 |
-
|
120 |
-
with open(local_file, 'wb') as f:
|
121 |
-
for chunk in response.iter_content(chunk_size=8192):
|
122 |
-
f.write(chunk)
|
123 |
-
|
124 |
-
if os.path.getsize(local_file) > 1000:
|
125 |
-
return local_file, f"✅ Downloaded via AWS S3"
|
126 |
-
|
127 |
-
except Exception as e:
|
128 |
-
print(f"AWS method failed: {str(e)}")
|
129 |
-
|
130 |
-
return None, f"❌ Failed to download {parameter}"
|
131 |
-
|
132 |
-
def extract_wind_data_from_grib(self, u_file, v_file, level_name, resolution=8):
|
133 |
-
"""Extract wind data from ECMWF GRIB files with improved processing"""
|
134 |
-
try:
|
135 |
-
print(f"🌪️ Processing {level_name} ECMWF GRIB wind data...")
|
136 |
-
|
137 |
-
# Open GRIB files with robust error handling
|
138 |
-
ds_u = None
|
139 |
-
ds_v = None
|
140 |
-
|
141 |
-
try:
|
142 |
-
# Method 1: Standard xarray with cfgrib
|
143 |
-
ds_u = xr.open_dataset(u_file, engine='cfgrib')
|
144 |
-
ds_v = xr.open_dataset(v_file, engine='cfgrib')
|
145 |
-
print(f"✅ Opened {level_name} GRIB files with cfgrib")
|
146 |
-
except Exception as e1:
|
147 |
-
print(f"⚠️ cfgrib method failed: {e1}")
|
148 |
-
try:
|
149 |
-
# Method 2: Alternative backend settings
|
150 |
-
ds_u = xr.open_dataset(u_file, engine='cfgrib', backend_kwargs={'indexpath': ''})
|
151 |
-
ds_v = xr.open_dataset(v_file, engine='cfgrib', backend_kwargs={'indexpath': ''})
|
152 |
-
print(f"✅ Opened {level_name} GRIB files with alternative cfgrib settings")
|
153 |
-
except Exception as e2:
|
154 |
-
print(f"❌ All GRIB opening methods failed: {e2}")
|
155 |
-
return []
|
156 |
-
|
157 |
-
# Debug: Print dataset info
|
158 |
-
print(f"📊 U dataset variables: {list(ds_u.data_vars.keys())}")
|
159 |
-
print(f"📊 U dataset coords: {list(ds_u.coords.keys())}")
|
160 |
-
print(f"📊 U dataset dims: {ds_u.dims}")
|
161 |
-
|
162 |
-
# Get wind components - more robust variable detection
|
163 |
-
u_var = None
|
164 |
-
v_var = None
|
165 |
-
|
166 |
-
# Try to find wind variables
|
167 |
-
for var in ds_u.data_vars.keys():
|
168 |
-
if 'u' in var.lower() or '10u' in var or '100u' in var:
|
169 |
-
u_var = var
|
170 |
-
break
|
171 |
-
|
172 |
-
for var in ds_v.data_vars.keys():
|
173 |
-
if 'v' in var.lower() or '10v' in var or '100v' in var:
|
174 |
-
v_var = var
|
175 |
-
break
|
176 |
-
|
177 |
-
if not u_var or not v_var:
|
178 |
-
print(f"❌ Could not find wind variables in {level_name} data")
|
179 |
-
return []
|
180 |
-
|
181 |
-
print(f"🌬️ Using variables: U={u_var}, V={v_var}")
|
182 |
-
|
183 |
-
u_data = ds_u[u_var]
|
184 |
-
v_data = ds_v[v_var]
|
185 |
-
|
186 |
-
# Get coordinates with better detection - check dimensions too
|
187 |
-
lats = None
|
188 |
-
lons = None
|
189 |
-
|
190 |
-
# First try coordinate variables
|
191 |
-
for coord_name in ['latitude', 'lat', 'y']:
|
192 |
-
if coord_name in ds_u.coords:
|
193 |
-
lats = ds_u[coord_name].values
|
194 |
-
print(f"🗺️ Found latitudes in coord '{coord_name}': shape {lats.shape}")
|
195 |
-
print(f"🔍 Latitude values sample: {lats[:5]} ... {lats[-5:] if len(lats) > 5 else lats}")
|
196 |
-
print(f"🔍 Latitude min/max: {lats.min():.6f} / {lats.max():.6f}")
|
197 |
-
print(f"🔍 Unique latitude count: {len(np.unique(lats))}")
|
198 |
-
break
|
199 |
-
|
200 |
-
for coord_name in ['longitude', 'lon', 'x']:
|
201 |
-
if coord_name in ds_u.coords:
|
202 |
-
lons = ds_u[coord_name].values
|
203 |
-
print(f"🗺️ Found longitudes in coord '{coord_name}': shape {lons.shape}")
|
204 |
-
print(f"🔍 Longitude values sample: {lons[:5]} ... {lons[-5:] if len(lons) > 5 else lons}")
|
205 |
-
print(f"🔍 Longitude min/max: {lons.min():.6f} / {lons.max():.6f}")
|
206 |
-
print(f"🔍 Unique longitude count: {len(np.unique(lons))}")
|
207 |
-
break
|
208 |
-
|
209 |
-
# If not found in coords, try dimensions or data variables
|
210 |
-
if lats is None:
|
211 |
-
print("⚠️ Latitudes not found in coords, checking dimensions and data vars...")
|
212 |
-
for dim_name in ds_u.dims:
|
213 |
-
if 'lat' in dim_name.lower() or 'y' in dim_name.lower():
|
214 |
-
if dim_name in ds_u.coords:
|
215 |
-
lats = ds_u.coords[dim_name].values
|
216 |
-
print(f"🗺️ Found latitudes in dim '{dim_name}': shape {lats.shape}")
|
217 |
-
break
|
218 |
-
elif hasattr(ds_u, dim_name):
|
219 |
-
lats = getattr(ds_u, dim_name).values
|
220 |
-
print(f"🗺️ Found latitudes as attr '{dim_name}': shape {lats.shape}")
|
221 |
-
break
|
222 |
-
|
223 |
-
if lons is None:
|
224 |
-
print("⚠️ Longitudes not found in coords, checking dimensions and data vars...")
|
225 |
-
for dim_name in ds_u.dims:
|
226 |
-
if 'lon' in dim_name.lower() or 'x' in dim_name.lower():
|
227 |
-
if dim_name in ds_u.coords:
|
228 |
-
lons = ds_u.coords[dim_name].values
|
229 |
-
print(f"🗺️ Found longitudes in dim '{dim_name}': shape {lons.shape}")
|
230 |
-
break
|
231 |
-
elif hasattr(ds_u, dim_name):
|
232 |
-
lons = getattr(ds_u, dim_name).values
|
233 |
-
print(f"🗺️ Found longitudes as attr '{dim_name}': shape {lons.shape}")
|
234 |
-
break
|
235 |
-
|
236 |
-
if lats is None or lons is None:
|
237 |
-
print(f"❌ Could not find coordinates in {level_name} data")
|
238 |
-
return []
|
239 |
-
|
240 |
-
print(f"🗺️ Grid size: {len(lats)} x {len(lons)} ({len(lats)*len(lons)} points)")
|
241 |
-
print(f"🗺️ Lat range: {lats.min():.2f} to {lats.max():.2f}")
|
242 |
-
print(f"🗺️ Lon range: {lons.min():.2f} to {lons.max():.2f}")
|
243 |
-
|
244 |
-
# Extract data values to get proper shape
|
245 |
-
u_values = u_data.values
|
246 |
-
v_values = v_data.values
|
247 |
-
|
248 |
-
# Handle time dimension
|
249 |
-
if u_values.ndim > 2:
|
250 |
-
print(f"🕐 Handling {u_values.ndim}D data, selecting first time/level")
|
251 |
-
# Take first time step and/or first level
|
252 |
-
while u_values.ndim > 2:
|
253 |
-
u_values = u_values[0]
|
254 |
-
v_values = v_values[0]
|
255 |
-
|
256 |
-
# Check for corrupted coordinates (all same value)
|
257 |
-
coords_corrupted = False
|
258 |
-
if len(np.unique(lats)) == 1:
|
259 |
-
print("⚠️ All latitudes are the same value - coordinates may be corrupted!")
|
260 |
-
coords_corrupted = True
|
261 |
-
elif len(np.unique(lons)) == 1:
|
262 |
-
print("⚠️ All longitudes are the same value - coordinates may be corrupted!")
|
263 |
-
coords_corrupted = True
|
264 |
-
|
265 |
-
if coords_corrupted:
|
266 |
-
print("🛠️ Generating proper coordinate grid from data shape...")
|
267 |
-
|
268 |
-
# Generate proper ECMWF-like coordinate grid
|
269 |
-
nlat, nlon = u_values.shape
|
270 |
-
|
271 |
-
# ECMWF global 0.25° grid typically ranges from 90N to -90S, 0E to 359.75E
|
272 |
-
lats = np.linspace(90, -90, nlat)
|
273 |
-
lons = np.linspace(0, 360-0.25, nlon)
|
274 |
-
|
275 |
-
print(f"🔧 Generated coordinate grid: {len(lats)} lats x {len(lons)} lons")
|
276 |
-
print(f"🗺️ New lat range: {lats.min():.2f} to {lats.max():.2f}")
|
277 |
-
print(f"🗺️ New lon range: {lons.min():.2f} to {lons.max():.2f}")
|
278 |
-
else:
|
279 |
-
# Check if latitudes are in wrong order (need to be descending for ECMWF)
|
280 |
-
if len(lats) > 1 and lats[0] < lats[-1]:
|
281 |
-
print("🔄 Reversing latitude order (ECMWF usually goes from North to South)")
|
282 |
-
lats = lats[::-1]
|
283 |
-
|
284 |
-
print(f"📐 Final data shape: {u_values.shape}")
|
285 |
-
|
286 |
-
# Ensure we have the right orientation
|
287 |
-
if u_values.shape != (len(lats), len(lons)):
|
288 |
-
print(f"⚠️ Shape mismatch: data {u_values.shape} vs coords ({len(lats)}, {len(lons)})")
|
289 |
-
# Try transpose
|
290 |
-
if u_values.shape == (len(lons), len(lats)):
|
291 |
-
u_values = u_values.T
|
292 |
-
v_values = v_values.T
|
293 |
-
print("🔄 Transposed data to match coordinates")
|
294 |
-
elif u_values.shape[1] == len(lats) and u_values.shape[0] == len(lons):
|
295 |
-
# Data might be (lon, lat) instead of (lat, lon)
|
296 |
-
u_values = u_values.T
|
297 |
-
v_values = v_values.T
|
298 |
-
print("🔄 Transposed data from (lon,lat) to (lat,lon)")
|
299 |
-
|
300 |
-
|
301 |
-
# Smart subsampling based on resolution
|
302 |
-
if resolution >= 10:
|
303 |
-
step = max(1, len(lats) // 50) # Limit to ~50 points per dimension
|
304 |
-
else:
|
305 |
-
step = max(1, int(resolution // 2))
|
306 |
-
|
307 |
-
print(f"⬇️ Subsampling every {step} points")
|
308 |
-
|
309 |
-
lats_sub = lats[::step]
|
310 |
-
lons_sub = lons[::step]
|
311 |
-
u_sub = u_values[::step, ::step]
|
312 |
-
v_sub = v_values[::step, ::step]
|
313 |
-
|
314 |
-
# Create wind data points
|
315 |
-
wind_data = []
|
316 |
-
valid_points = 0
|
317 |
-
|
318 |
-
print(f"🔍 Creating wind data points from subsampled coordinates:")
|
319 |
-
print(f"🔍 lats_sub range: {lats_sub.min():.2f} to {lats_sub.max():.2f} (shape: {lats_sub.shape})")
|
320 |
-
print(f"🔍 lons_sub range: {lons_sub.min():.2f} to {lons_sub.max():.2f} (shape: {lons_sub.shape})")
|
321 |
-
print(f"🔍 First 3 lats_sub: {lats_sub[:3]}")
|
322 |
-
print(f"🔍 First 3 lons_sub: {lons_sub[:3]}")
|
323 |
-
|
324 |
-
for i, lat in enumerate(lats_sub):
|
325 |
-
for j, lon in enumerate(lons_sub):
|
326 |
-
if i < len(u_sub) and j < len(u_sub[i]):
|
327 |
-
try:
|
328 |
-
u_wind = float(u_sub[i, j])
|
329 |
-
v_wind = float(v_sub[i, j])
|
330 |
-
|
331 |
-
# Skip invalid data (NaN, extreme values)
|
332 |
-
if np.isnan(u_wind) or np.isnan(v_wind):
|
333 |
-
continue
|
334 |
-
if abs(u_wind) > 200 or abs(v_wind) > 200: # Sanity check
|
335 |
-
continue
|
336 |
-
|
337 |
-
# Calculate speed and direction
|
338 |
-
speed = float(np.sqrt(u_wind**2 + v_wind**2))
|
339 |
-
direction = float(np.degrees(np.arctan2(v_wind, u_wind)))
|
340 |
-
|
341 |
-
# Normalize direction to 0-360
|
342 |
-
if direction < 0:
|
343 |
-
direction += 360
|
344 |
-
|
345 |
-
wind_data.append({
|
346 |
-
'lat': float(lat),
|
347 |
-
'lon': float(lon),
|
348 |
-
'u': u_wind,
|
349 |
-
'v': v_wind,
|
350 |
-
'speed': speed,
|
351 |
-
'direction': direction,
|
352 |
-
'level': level_name
|
353 |
-
})
|
354 |
-
valid_points += 1
|
355 |
-
|
356 |
-
# Debug first few points
|
357 |
-
if valid_points <= 5:
|
358 |
-
print(f"🔍 Wind point {valid_points}: lat={lat:.4f}, lon={lon:.4f}, speed={speed:.2f}")
|
359 |
-
|
360 |
-
except (ValueError, TypeError) as e:
|
361 |
-
continue # Skip invalid data points
|
362 |
-
|
363 |
-
print(f"✅ Processed {valid_points} valid {level_name} wind data points")
|
364 |
-
|
365 |
-
# Close datasets to free memory
|
366 |
-
ds_u.close()
|
367 |
-
ds_v.close()
|
368 |
-
|
369 |
-
return wind_data
|
370 |
-
|
371 |
-
except Exception as e:
|
372 |
-
print(f"❌ Error processing {level_name} GRIB files: {str(e)}")
|
373 |
-
import traceback
|
374 |
-
traceback.print_exc()
|
375 |
-
return []
|
376 |
-
|
377 |
-
def get_wind_data(lat_min=-90, lat_max=90, lon_min=-180, lon_max=180, resolution=8):
|
378 |
-
"""
|
379 |
-
Fetch REAL ECMWF wind data at multiple levels using proven methodology
|
380 |
-
"""
|
381 |
-
try:
|
382 |
-
print(f"🌍 Fetching ECMWF wind data at 10m and 100m levels (resolution: {resolution}°)...")
|
383 |
-
|
384 |
-
# Create ECMWF data fetcher
|
385 |
-
fetcher = ECMWFWindDataFetcher()
|
386 |
-
|
387 |
-
# Download ECMWF wind data for both levels
|
388 |
-
wind_files, status = fetcher.download_ecmwf_wind_data(step=0)
|
389 |
-
|
390 |
-
all_wind_data = []
|
391 |
-
|
392 |
-
# Process 10m wind data
|
393 |
-
if wind_files['10m'][0] and wind_files['10m'][1]:
|
394 |
-
u10_file, v10_file = wind_files['10m']
|
395 |
-
wind_data_10m = fetcher.extract_wind_data_from_grib(u10_file, v10_file, '10m', resolution)
|
396 |
-
if wind_data_10m:
|
397 |
-
all_wind_data.extend(wind_data_10m)
|
398 |
-
|
399 |
-
# Process 100m wind data
|
400 |
-
if wind_files['100m'][0] and wind_files['100m'][1]:
|
401 |
-
u100_file, v100_file = wind_files['100m']
|
402 |
-
wind_data_100m = fetcher.extract_wind_data_from_grib(u100_file, v100_file, '100m', resolution)
|
403 |
-
if wind_data_100m:
|
404 |
-
all_wind_data.extend(wind_data_100m)
|
405 |
-
|
406 |
-
if all_wind_data:
|
407 |
-
# Filter data to requested region
|
408 |
-
filtered_data = [
|
409 |
-
wd for wd in all_wind_data
|
410 |
-
if lat_min <= wd['lat'] <= lat_max and lon_min <= wd['lon'] <= lon_max
|
411 |
-
]
|
412 |
-
|
413 |
-
print(f"✅ Successfully processed {len(filtered_data)} total ECMWF wind points")
|
414 |
-
return filtered_data if filtered_data else all_wind_data, status
|
415 |
-
else:
|
416 |
-
print("⚠️ GRIB processing failed, falling back to synthetic data")
|
417 |
-
return generate_synthetic_wind_data(), status
|
418 |
-
|
419 |
-
except Exception as e:
|
420 |
-
print(f"❌ Error in ECMWF wind data fetching: {e}")
|
421 |
-
print("🔄 Falling back to synthetic wind data...")
|
422 |
-
return generate_synthetic_wind_data(), f"❌ Error: {str(e)}"
|
423 |
-
|
424 |
-
def generate_synthetic_wind_data():
|
425 |
-
"""Fallback synthetic data - similar to working windmap"""
|
426 |
-
print("🎯 Generating synthetic wind data as fallback...")
|
427 |
-
|
428 |
-
wind_data = []
|
429 |
-
for lat in range(-60, 61, 15):
|
430 |
-
for lon in range(-180, 181, 20):
|
431 |
-
# Realistic wind patterns
|
432 |
-
lat_rad = np.radians(lat)
|
433 |
-
lon_rad = np.radians(lon)
|
434 |
-
|
435 |
-
# Jet stream + trade winds + noise
|
436 |
-
u = 15 * np.sin(lat_rad) + 5 * np.cos(lon_rad/2) + np.random.normal(0, 3)
|
437 |
-
v = 5 * np.cos(lat_rad) + 3 * np.sin(lon_rad/3) + np.random.normal(0, 2)
|
438 |
-
speed = np.sqrt(u*u + v*v)
|
439 |
-
|
440 |
-
wind_data.append({
|
441 |
-
'lat': lat,
|
442 |
-
'lon': lon,
|
443 |
-
'u': u,
|
444 |
-
'v': v,
|
445 |
-
'speed': speed,
|
446 |
-
'direction': np.degrees(np.arctan2(v, u))
|
447 |
-
})
|
448 |
-
|
449 |
-
print(f"✅ Generated {len(wind_data)} synthetic wind points")
|
450 |
-
return wind_data
|
451 |
-
|
452 |
-
def create_wind_arrow_map(wind_data):
|
453 |
-
"""
|
454 |
-
Create wind arrow visualization using folium
|
455 |
-
"""
|
456 |
-
try:
|
457 |
-
print("🌪️ Creating wind arrow visualization...")
|
458 |
-
|
459 |
-
if not wind_data:
|
460 |
-
return "<p>❌ No wind data available</p>"
|
461 |
-
|
462 |
-
# Create folium map
|
463 |
-
m = folium.Map(
|
464 |
-
location=[30, 0],
|
465 |
-
zoom_start=3,
|
466 |
-
tiles='OpenStreetMap'
|
467 |
-
)
|
468 |
-
|
469 |
-
# Separate data by level
|
470 |
-
wind_10m = [w for w in wind_data if w.get('level') == '10m']
|
471 |
-
wind_100m = [w for w in wind_data if w.get('level') == '100m']
|
472 |
-
|
473 |
-
print(f"📊 Wind data: {len(wind_10m)} points at 10m, {len(wind_100m)} points at 100m")
|
474 |
-
|
475 |
-
# Debug: Show sample coordinates
|
476 |
-
if wind_10m:
|
477 |
-
sample = wind_10m[0]
|
478 |
-
print(f"🗺️ Sample 10m wind: Lat={sample['lat']:.2f}, Lon={sample['lon']:.2f}, Speed={sample['speed']:.1f} m/s, Dir={sample['direction']:.0f}°")
|
479 |
-
if wind_100m:
|
480 |
-
sample = wind_100m[0]
|
481 |
-
print(f"🗺️ Sample 100m wind: Lat={sample['lat']:.2f}, Lon={sample['lon']:.2f}, Speed={sample['speed']:.1f} m/s, Dir={sample['direction']:.0f}°")
|
482 |
-
|
483 |
-
# Add 10m wind markers - start with simple circles to test positioning
|
484 |
-
for i, wind in enumerate(wind_10m[:100]): # Limit for performance
|
485 |
-
if wind['speed'] > 1: # Only show significant winds
|
486 |
-
|
487 |
-
# First add a simple circle marker to test positioning
|
488 |
-
folium.CircleMarker(
|
489 |
-
location=[wind['lat'], wind['lon']],
|
490 |
-
radius=5,
|
491 |
-
color='blue',
|
492 |
-
fillColor='lightblue',
|
493 |
-
fillOpacity=0.7,
|
494 |
-
popup=f"<b>10m Wind #{i+1}</b><br>Speed: {wind['speed']:.1f} m/s<br>Direction: {wind['direction']:.0f}°<br>U: {wind['u']:.1f} m/s<br>V: {wind['v']:.1f} m/s<br>Lat: {wind['lat']:.4f}<br>Lon: {wind['lon']:.4f}",
|
495 |
-
tooltip=f"10m: {wind['speed']:.1f}m/s"
|
496 |
-
).add_to(m)
|
497 |
-
|
498 |
-
# Also try with regular marker as backup
|
499 |
-
if i < 20: # Only add arrows for first 20 points
|
500 |
-
# Calculate arrow length based on wind speed
|
501 |
-
arrow_length = min(wind['speed'] * 1.5, 15)
|
502 |
-
|
503 |
-
# Convert meteorological direction to mathematical angle
|
504 |
-
arrow_angle = (90 - wind['direction']) % 360
|
505 |
-
|
506 |
-
# Create simple arrow SVG
|
507 |
-
arrow_svg = f"""
|
508 |
-
<svg width="30" height="30" style="overflow: visible;">
|
509 |
-
<g transform="rotate({arrow_angle} 15 15)">
|
510 |
-
<line x1="5" y1="15" x2="{20}" y2="15" stroke="darkblue" stroke-width="3" marker-end="url(#arrowhead-blue{i})"/>
|
511 |
-
</g>
|
512 |
-
<defs>
|
513 |
-
<marker id="arrowhead-blue{i}" markerWidth="10" markerHeight="7"
|
514 |
-
refX="9" refY="3.5" orient="auto">
|
515 |
-
<polygon points="0 0, 10 3.5, 0 7" fill="darkblue"/>
|
516 |
-
</marker>
|
517 |
-
</defs>
|
518 |
-
</svg>
|
519 |
-
"""
|
520 |
-
|
521 |
-
folium.Marker(
|
522 |
-
location=[wind['lat'], wind['lon']],
|
523 |
-
icon=folium.DivIcon(
|
524 |
-
html=arrow_svg,
|
525 |
-
icon_size=(30, 30),
|
526 |
-
icon_anchor=(15, 15)
|
527 |
-
),
|
528 |
-
popup=f"<b>10m Arrow #{i+1}</b><br>Speed: {wind['speed']:.1f} m/s<br>Direction: {wind['direction']:.0f}°<br>Lat: {wind['lat']:.4f}<br>Lon: {wind['lon']:.4f}"
|
529 |
-
).add_to(m)
|
530 |
-
|
531 |
-
# Add 100m wind markers - simple circles
|
532 |
-
for i, wind in enumerate(wind_100m[:50]): # Limit for performance
|
533 |
-
if wind['speed'] > 1: # Only show significant winds
|
534 |
-
# Add simple circle marker to test positioning
|
535 |
-
folium.CircleMarker(
|
536 |
-
location=[wind['lat'], wind['lon']],
|
537 |
-
radius=7,
|
538 |
-
color='red',
|
539 |
-
fillColor='pink',
|
540 |
-
fillOpacity=0.7,
|
541 |
-
popup=f"<b>100m Wind #{i+1}</b><br>Speed: {wind['speed']:.1f} m/s<br>Direction: {wind['direction']:.0f}°<br>U: {wind['u']:.1f} m/s<br>V: {wind['v']:.1f} m/s<br>Lat: {wind['lat']:.4f}<br>Lon: {wind['lon']:.4f}",
|
542 |
-
tooltip=f"100m: {wind['speed']:.1f}m/s"
|
543 |
-
).add_to(m)
|
544 |
-
|
545 |
-
# Add legend
|
546 |
-
legend_html = '''
|
547 |
-
<div style="position: fixed;
|
548 |
-
top: 10px; right: 10px; width: 200px; height: 120px;
|
549 |
-
background-color: white; border:2px solid grey; z-index:9999;
|
550 |
-
font-size:14px; padding: 10px">
|
551 |
-
<h4>Wind Data Legend</h4>
|
552 |
-
<p><span style="color:blue;">■</span> 10m Wind Arrows</p>
|
553 |
-
<p><span style="color:red;">■</span> 100m Wind Arrows</p>
|
554 |
-
<p>Arrow length = wind speed</p>
|
555 |
-
<p>Arrow direction = wind direction</p>
|
556 |
-
</div>
|
557 |
-
'''
|
558 |
-
m.get_root().html.add_child(folium.Element(legend_html))
|
559 |
-
|
560 |
-
return m._repr_html_()
|
561 |
-
|
562 |
-
except Exception as e:
|
563 |
-
return f"<p>❌ Error creating wind map: {str(e)}</p>"
|
564 |
-
|
565 |
-
def fetch_and_visualize_winds(resolution=8):
|
566 |
-
"""Main function to fetch and visualize wind data"""
|
567 |
-
try:
|
568 |
-
status_msg = "🌍 Fetching real-time ECMWF wind data at 10m and 100m levels..."
|
569 |
-
|
570 |
-
# Fetch real wind data
|
571 |
-
wind_data, download_status = get_wind_data(resolution=resolution)
|
572 |
-
|
573 |
-
if not wind_data:
|
574 |
-
return "❌ No wind data could be fetched", ""
|
575 |
-
|
576 |
-
# Create wind arrow map
|
577 |
-
wind_map = create_wind_arrow_map(wind_data)
|
578 |
-
|
579 |
-
# Count data by level
|
580 |
-
wind_10m_count = len([w for w in wind_data if w.get('level') == '10m'])
|
581 |
-
wind_100m_count = len([w for w in wind_data if w.get('level') == '100m'])
|
582 |
-
|
583 |
-
status_msg = f"""✅ ECMWF Wind Data Successfully Retrieved!
|
584 |
-
|
585 |
-
{download_status}
|
586 |
-
|
587 |
-
📊 Data Summary:
|
588 |
-
• Total wind measurements: {len(wind_data)} points
|
589 |
-
• 10m wind data: {wind_10m_count} points
|
590 |
-
• 100m wind data: {wind_100m_count} points
|
591 |
-
• Data source: ECMWF Operational Forecasts
|
592 |
-
• Resolution: 0.25° (~25km) ECMWF grid
|
593 |
-
• Grid spacing: {resolution}° display resolution
|
594 |
-
• Update time: {datetime.now().strftime('%H:%M:%S UTC')}
|
595 |
-
|
596 |
-
🏹 Wind Arrow Visualization:
|
597 |
-
• Blue arrows = 10m wind data
|
598 |
-
• Red arrows = 100m wind data
|
599 |
-
• Arrow length = wind speed
|
600 |
-
• Arrow direction = wind direction
|
601 |
-
• Click arrows for detailed wind info (U/V components)
|
602 |
-
• Interactive map with zoom/pan controls
|
603 |
-
|
604 |
-
🎯 Features:
|
605 |
-
• Real ECMWF GRIB file processing
|
606 |
-
• Multi-level wind comparison (10m vs 100m)
|
607 |
-
• Professional meteorological data
|
608 |
-
• Same data source as earth.nullschool.net
|
609 |
-
• Clean arrow-based visualization"""
|
610 |
-
|
611 |
-
return status_msg, wind_map
|
612 |
-
|
613 |
-
except Exception as e:
|
614 |
-
return f"❌ Error: {str(e)}", ""
|
615 |
-
|
616 |
-
# Create Gradio interface
|
617 |
-
with gr.Blocks(title="ECMWF Wind Visualization", theme=gr.themes.Soft()) as app:
|
618 |
-
gr.Markdown("""
|
619 |
-
# 🌪️ ECMWF Wind Visualization
|
620 |
-
## Multi-Level Wind Data with Arrow Visualization
|
621 |
-
|
622 |
-
**Features:**
|
623 |
-
- 🌍 Real ECMWF operational forecast data
|
624 |
-
- 🏹 Wind arrows at 10m and 100m levels
|
625 |
-
- 📊 Professional meteorological visualization
|
626 |
-
- 🎯 Same data source as earth.nullschool.net
|
627 |
-
- 📈 Compare wind speeds at different altitudes
|
628 |
-
""")
|
629 |
-
|
630 |
-
with gr.Row():
|
631 |
-
with gr.Column(scale=1):
|
632 |
-
gr.Markdown("### ⚙️ Controls")
|
633 |
-
|
634 |
-
resolution_slider = gr.Slider(
|
635 |
-
minimum=8,
|
636 |
-
maximum=20,
|
637 |
-
value=12,
|
638 |
-
step=2,
|
639 |
-
label="Grid Resolution (degrees)",
|
640 |
-
info="Lower = more detail, slower loading"
|
641 |
-
)
|
642 |
-
|
643 |
-
fetch_btn = gr.Button(
|
644 |
-
"🏹 Fetch ECMWF Wind Data & Visualize",
|
645 |
-
variant="primary",
|
646 |
-
size="lg"
|
647 |
-
)
|
648 |
-
|
649 |
-
status_output = gr.Textbox(
|
650 |
-
label="Status",
|
651 |
-
lines=20,
|
652 |
-
interactive=False
|
653 |
-
)
|
654 |
-
|
655 |
-
with gr.Column(scale=2):
|
656 |
-
gr.Markdown("### 🗺️ Wind Arrow Visualization")
|
657 |
-
wind_map_output = gr.HTML(
|
658 |
-
label="ECMWF Wind Data Arrows",
|
659 |
-
value="<p>Click 'Fetch ECMWF Wind Data' to see wind arrows at 10m and 100m levels!</p>"
|
660 |
-
)
|
661 |
-
|
662 |
-
# Event handlers
|
663 |
-
fetch_btn.click(
|
664 |
-
fetch_and_visualize_winds,
|
665 |
-
inputs=[resolution_slider],
|
666 |
-
outputs=[status_output, wind_map_output]
|
667 |
-
)
|
668 |
-
|
669 |
-
gr.Markdown("""
|
670 |
-
---
|
671 |
-
### 📖 About This App
|
672 |
-
|
673 |
-
This application uses **real ECMWF operational forecast data** to visualize wind patterns at multiple atmospheric levels.
|
674 |
-
|
675 |
-
**Technical Features:**
|
676 |
-
- Professional ECMWF operational forecasts at 10m and 100m levels
|
677 |
-
- Same data source as earth.nullschool.net
|
678 |
-
- Wind arrow visualization showing speed and direction
|
679 |
-
- Real U/V wind component processing from GRIB files
|
680 |
-
- Multi-level wind comparison capabilities
|
681 |
-
- Color-coded wind arrows (blue=10m, red=100m)
|
682 |
-
- Interactive Leaflet map integration
|
683 |
-
|
684 |
-
**Data Source:** ECMWF Open Data - Professional meteorological forecasts at 0.25° resolution
|
685 |
-
""")
|
686 |
-
|
687 |
-
if __name__ == "__main__":
|
688 |
-
app.launch(
|
689 |
-
server_name="0.0.0.0",
|
690 |
-
server_port=7860,
|
691 |
-
share=False
|
692 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
index.html
DELETED
@@ -1,377 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="en">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8">
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>🌪️ Wind Particle Visualization - Earth.nullschool.net Style</title>
|
7 |
-
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
|
8 |
-
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
|
9 |
-
<style>
|
10 |
-
body {
|
11 |
-
margin: 0;
|
12 |
-
padding: 20px;
|
13 |
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
14 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
15 |
-
color: white;
|
16 |
-
}
|
17 |
-
.container {
|
18 |
-
max-width: 1200px;
|
19 |
-
margin: 0 auto;
|
20 |
-
}
|
21 |
-
h1 {
|
22 |
-
text-align: center;
|
23 |
-
margin-bottom: 10px;
|
24 |
-
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
25 |
-
}
|
26 |
-
.subtitle {
|
27 |
-
text-align: center;
|
28 |
-
margin-bottom: 20px;
|
29 |
-
opacity: 0.9;
|
30 |
-
}
|
31 |
-
#map {
|
32 |
-
height: 600px;
|
33 |
-
width: 100%;
|
34 |
-
border-radius: 10px;
|
35 |
-
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
36 |
-
margin-bottom: 20px;
|
37 |
-
}
|
38 |
-
.controls {
|
39 |
-
display: flex;
|
40 |
-
justify-content: center;
|
41 |
-
gap: 15px;
|
42 |
-
margin-bottom: 20px;
|
43 |
-
}
|
44 |
-
button {
|
45 |
-
padding: 12px 24px;
|
46 |
-
border: none;
|
47 |
-
border-radius: 25px;
|
48 |
-
background: rgba(255,255,255,0.2);
|
49 |
-
color: white;
|
50 |
-
font-weight: bold;
|
51 |
-
cursor: pointer;
|
52 |
-
transition: all 0.3s ease;
|
53 |
-
backdrop-filter: blur(10px);
|
54 |
-
}
|
55 |
-
button:hover {
|
56 |
-
background: rgba(255,255,255,0.3);
|
57 |
-
transform: translateY(-2px);
|
58 |
-
}
|
59 |
-
.info {
|
60 |
-
text-align: center;
|
61 |
-
background: rgba(255,255,255,0.1);
|
62 |
-
padding: 15px;
|
63 |
-
border-radius: 10px;
|
64 |
-
backdrop-filter: blur(10px);
|
65 |
-
}
|
66 |
-
.legend {
|
67 |
-
position: absolute;
|
68 |
-
bottom: 20px;
|
69 |
-
right: 20px;
|
70 |
-
background: rgba(255,255,255,0.9);
|
71 |
-
color: #333;
|
72 |
-
padding: 15px;
|
73 |
-
border-radius: 10px;
|
74 |
-
font-size: 12px;
|
75 |
-
z-index: 1000;
|
76 |
-
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
77 |
-
}
|
78 |
-
.legend h4 {
|
79 |
-
margin: 0 0 10px 0;
|
80 |
-
color: #333;
|
81 |
-
}
|
82 |
-
.legend-item {
|
83 |
-
display: flex;
|
84 |
-
align-items: center;
|
85 |
-
margin: 5px 0;
|
86 |
-
}
|
87 |
-
.legend-color {
|
88 |
-
width: 20px;
|
89 |
-
height: 3px;
|
90 |
-
margin-right: 8px;
|
91 |
-
border-radius: 2px;
|
92 |
-
}
|
93 |
-
</style>
|
94 |
-
</head>
|
95 |
-
<body>
|
96 |
-
<div class="container">
|
97 |
-
<h1>🌪️ Wind Particle Visualization</h1>
|
98 |
-
<p class="subtitle">Earth.nullschool.net style particle animation with synthetic wind data</p>
|
99 |
-
|
100 |
-
<div class="controls">
|
101 |
-
<button onclick="startParticles()">🌪️ Start Wind Particles</button>
|
102 |
-
<button onclick="resetParticles()">🔄 Reset</button>
|
103 |
-
<button onclick="toggleSpeed()">⚡ Toggle Speed</button>
|
104 |
-
</div>
|
105 |
-
|
106 |
-
<div id="map"></div>
|
107 |
-
|
108 |
-
<div class="info">
|
109 |
-
<strong>🎯 Demo Features:</strong> Earth.nullschool.net style particle system • Bilinear interpolation • 1000 animated particles • Zoom-responsive visualization • Synthetic global wind patterns
|
110 |
-
</div>
|
111 |
-
</div>
|
112 |
-
|
113 |
-
<div class="legend">
|
114 |
-
<h4>🌪️ Wind Speed</h4>
|
115 |
-
<div class="legend-item">
|
116 |
-
<div class="legend-color" style="background: rgba(110,159,190,0.9);"></div>
|
117 |
-
<span>< 5 m/s</span>
|
118 |
-
</div>
|
119 |
-
<div class="legend-item">
|
120 |
-
<div class="legend-color" style="background: rgba(65,120,190,0.9);"></div>
|
121 |
-
<span>5-10 m/s</span>
|
122 |
-
</div>
|
123 |
-
<div class="legend-item">
|
124 |
-
<div class="legend-color" style="background: rgba(25,80,170,0.9);"></div>
|
125 |
-
<span>10-15 m/s</span>
|
126 |
-
</div>
|
127 |
-
<div class="legend-item">
|
128 |
-
<div class="legend-color" style="background: rgba(85,160,35,0.9);"></div>
|
129 |
-
<span>15-20 m/s</span>
|
130 |
-
</div>
|
131 |
-
<div class="legend-item">
|
132 |
-
<div class="legend-color" style="background: rgba(255,215,0,0.9);"></div>
|
133 |
-
<span>20-25 m/s</span>
|
134 |
-
</div>
|
135 |
-
<div class="legend-item">
|
136 |
-
<div class="legend-color" style="background: rgba(255,120,0,0.9);"></div>
|
137 |
-
<span>> 25 m/s</span>
|
138 |
-
</div>
|
139 |
-
</div>
|
140 |
-
|
141 |
-
<script>
|
142 |
-
console.log('🌪️ Initializing wind particle system...');
|
143 |
-
|
144 |
-
// Initialize map
|
145 |
-
const map = L.map('map').setView([30, 0], 3);
|
146 |
-
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
147 |
-
attribution: '© OpenStreetMap contributors'
|
148 |
-
}).addTo(map);
|
149 |
-
|
150 |
-
// Global variables
|
151 |
-
let particleSystem = null;
|
152 |
-
let animationSpeed = 1;
|
153 |
-
let windField = null;
|
154 |
-
|
155 |
-
// Generate synthetic wind data (earth.nullschool.net style)
|
156 |
-
function generateWindField() {
|
157 |
-
console.log('Generating synthetic wind field...');
|
158 |
-
|
159 |
-
const lats = [];
|
160 |
-
const lons = [];
|
161 |
-
const winds = [];
|
162 |
-
|
163 |
-
// Create global grid
|
164 |
-
for (let lat = 60; lat >= -60; lat -= 3) {
|
165 |
-
for (let lon = -180; lon <= 180; lon += 6) {
|
166 |
-
lats.push(lat);
|
167 |
-
lons.push(lon);
|
168 |
-
|
169 |
-
// Realistic wind patterns
|
170 |
-
const latRad = lat * Math.PI / 180;
|
171 |
-
const lonRad = lon * Math.PI / 180;
|
172 |
-
|
173 |
-
// Jet stream + trade winds + random turbulence
|
174 |
-
const u = 15 * Math.sin(latRad) + 8 * Math.cos(lonRad/2) + (Math.random() - 0.5) * 6;
|
175 |
-
const v = 5 * Math.cos(latRad) + 3 * Math.sin(lonRad/3) + (Math.random() - 0.5) * 4;
|
176 |
-
|
177 |
-
winds.push({ lat, lon, u, v, speed: Math.sqrt(u*u + v*v) });
|
178 |
-
}
|
179 |
-
}
|
180 |
-
|
181 |
-
console.log(`Generated ${winds.length} wind points`);
|
182 |
-
return winds;
|
183 |
-
}
|
184 |
-
|
185 |
-
// Wind particle system (earth.nullschool.net inspired)
|
186 |
-
class WindParticleSystem {
|
187 |
-
constructor(map, windData) {
|
188 |
-
this.map = map;
|
189 |
-
this.windData = windData;
|
190 |
-
this.particles = [];
|
191 |
-
this.numParticles = 1000;
|
192 |
-
this.particleAge = 80;
|
193 |
-
this.canvas = null;
|
194 |
-
this.ctx = null;
|
195 |
-
this.animationId = null;
|
196 |
-
this.setup();
|
197 |
-
}
|
198 |
-
|
199 |
-
setup() {
|
200 |
-
const mapContainer = this.map.getContainer();
|
201 |
-
|
202 |
-
// Create canvas overlay
|
203 |
-
this.canvas = document.createElement('canvas');
|
204 |
-
this.canvas.style.position = 'absolute';
|
205 |
-
this.canvas.style.top = '0';
|
206 |
-
this.canvas.style.left = '0';
|
207 |
-
this.canvas.style.pointerEvents = 'none';
|
208 |
-
this.canvas.style.zIndex = '1000';
|
209 |
-
this.canvas.width = mapContainer.offsetWidth;
|
210 |
-
this.canvas.height = mapContainer.offsetHeight;
|
211 |
-
|
212 |
-
mapContainer.style.position = 'relative';
|
213 |
-
mapContainer.appendChild(this.canvas);
|
214 |
-
|
215 |
-
this.ctx = this.canvas.getContext('2d');
|
216 |
-
this.ctx.lineCap = 'round';
|
217 |
-
this.ctx.lineJoin = 'round';
|
218 |
-
|
219 |
-
// Initialize particles
|
220 |
-
this.initializeParticles();
|
221 |
-
this.animate();
|
222 |
-
|
223 |
-
// Handle map events
|
224 |
-
this.map.on('moveend zoomend resize', () => this.updateCanvas());
|
225 |
-
|
226 |
-
console.log('✅ Wind particle system initialized');
|
227 |
-
}
|
228 |
-
|
229 |
-
updateCanvas() {
|
230 |
-
const mapContainer = this.map.getContainer();
|
231 |
-
this.canvas.width = mapContainer.offsetWidth;
|
232 |
-
this.canvas.height = mapContainer.offsetHeight;
|
233 |
-
}
|
234 |
-
|
235 |
-
initializeParticles() {
|
236 |
-
this.particles = [];
|
237 |
-
const bounds = this.map.getBounds();
|
238 |
-
|
239 |
-
for (let i = 0; i < this.numParticles; i++) {
|
240 |
-
this.particles.push({
|
241 |
-
lat: bounds.getSouth() + Math.random() * (bounds.getNorth() - bounds.getSouth()),
|
242 |
-
lon: bounds.getWest() + Math.random() * (bounds.getEast() - bounds.getWest()),
|
243 |
-
age: Math.floor(Math.random() * this.particleAge),
|
244 |
-
trail: []
|
245 |
-
});
|
246 |
-
}
|
247 |
-
}
|
248 |
-
|
249 |
-
getWindAtPosition(lat, lon) {
|
250 |
-
// Find nearest wind point (simple approach)
|
251 |
-
let minDist = Infinity;
|
252 |
-
let nearestWind = { u: 0, v: 0, speed: 0 };
|
253 |
-
|
254 |
-
for (const wind of this.windData) {
|
255 |
-
const dist = Math.sqrt((wind.lat - lat) ** 2 + (wind.lon - lon) ** 2);
|
256 |
-
if (dist < minDist) {
|
257 |
-
minDist = dist;
|
258 |
-
nearestWind = wind;
|
259 |
-
}
|
260 |
-
}
|
261 |
-
|
262 |
-
return nearestWind;
|
263 |
-
}
|
264 |
-
|
265 |
-
getWindColor(speed) {
|
266 |
-
// Earth.nullschool.net color scheme
|
267 |
-
if (speed < 5) return 'rgba(110, 159, 190, 0.8)';
|
268 |
-
if (speed < 10) return 'rgba(65, 120, 190, 0.8)';
|
269 |
-
if (speed < 15) return 'rgba(25, 80, 170, 0.8)';
|
270 |
-
if (speed < 20) return 'rgba(85, 160, 35, 0.8)';
|
271 |
-
if (speed < 25) return 'rgba(255, 215, 0, 0.8)';
|
272 |
-
return 'rgba(255, 120, 0, 0.8)';
|
273 |
-
}
|
274 |
-
|
275 |
-
evolveParticle(particle) {
|
276 |
-
const wind = this.getWindAtPosition(particle.lat, particle.lon);
|
277 |
-
|
278 |
-
// Move particle based on wind
|
279 |
-
const scale = 0.01 * animationSpeed * Math.pow(2, this.map.getZoom() - 5);
|
280 |
-
particle.lat += wind.v * scale;
|
281 |
-
particle.lon += wind.u * scale;
|
282 |
-
particle.age++;
|
283 |
-
|
284 |
-
// Reset if too old or out of bounds
|
285 |
-
const bounds = this.map.getBounds();
|
286 |
-
if (particle.age > this.particleAge ||
|
287 |
-
particle.lat < bounds.getSouth() || particle.lat > bounds.getNorth() ||
|
288 |
-
particle.lon < bounds.getWest() || particle.lon > bounds.getEast()) {
|
289 |
-
|
290 |
-
particle.lat = bounds.getSouth() + Math.random() * (bounds.getNorth() - bounds.getSouth());
|
291 |
-
particle.lon = bounds.getWest() + Math.random() * (bounds.getEast() - bounds.getWest());
|
292 |
-
particle.age = 0;
|
293 |
-
}
|
294 |
-
|
295 |
-
return wind;
|
296 |
-
}
|
297 |
-
|
298 |
-
render() {
|
299 |
-
// Fade effect
|
300 |
-
this.ctx.globalCompositeOperation = 'destination-in';
|
301 |
-
this.ctx.globalAlpha = 0.95;
|
302 |
-
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
303 |
-
|
304 |
-
// Draw particles
|
305 |
-
this.ctx.globalCompositeOperation = 'lighter';
|
306 |
-
this.ctx.globalAlpha = 0.9;
|
307 |
-
this.ctx.lineWidth = 1.5;
|
308 |
-
|
309 |
-
for (const particle of this.particles) {
|
310 |
-
const wind = this.evolveParticle(particle);
|
311 |
-
|
312 |
-
if (wind.speed > 0.5) {
|
313 |
-
const point = this.map.latLngToContainerPoint([particle.lat, particle.lon]);
|
314 |
-
|
315 |
-
if (point.x >= 0 && point.x < this.canvas.width &&
|
316 |
-
point.y >= 0 && point.y < this.canvas.height) {
|
317 |
-
|
318 |
-
this.ctx.strokeStyle = this.getWindColor(wind.speed);
|
319 |
-
this.ctx.beginPath();
|
320 |
-
this.ctx.arc(point.x, point.y, 1, 0, 2 * Math.PI);
|
321 |
-
this.ctx.stroke();
|
322 |
-
}
|
323 |
-
}
|
324 |
-
}
|
325 |
-
}
|
326 |
-
|
327 |
-
animate() {
|
328 |
-
this.render();
|
329 |
-
this.animationId = requestAnimationFrame(() => this.animate());
|
330 |
-
}
|
331 |
-
|
332 |
-
destroy() {
|
333 |
-
if (this.animationId) {
|
334 |
-
cancelAnimationFrame(this.animationId);
|
335 |
-
}
|
336 |
-
if (this.canvas) {
|
337 |
-
this.canvas.remove();
|
338 |
-
}
|
339 |
-
}
|
340 |
-
}
|
341 |
-
|
342 |
-
// Control functions
|
343 |
-
function startParticles() {
|
344 |
-
if (!windField) {
|
345 |
-
windField = generateWindField();
|
346 |
-
}
|
347 |
-
|
348 |
-
if (particleSystem) {
|
349 |
-
particleSystem.destroy();
|
350 |
-
}
|
351 |
-
|
352 |
-
particleSystem = new WindParticleSystem(map, windField);
|
353 |
-
console.log('🌪️ Wind particles started!');
|
354 |
-
}
|
355 |
-
|
356 |
-
function resetParticles() {
|
357 |
-
if (particleSystem) {
|
358 |
-
particleSystem.destroy();
|
359 |
-
particleSystem = null;
|
360 |
-
}
|
361 |
-
windField = null;
|
362 |
-
console.log('🔄 Particles reset');
|
363 |
-
}
|
364 |
-
|
365 |
-
function toggleSpeed() {
|
366 |
-
animationSpeed = animationSpeed === 1 ? 2 : animationSpeed === 2 ? 0.5 : 1;
|
367 |
-
console.log('⚡ Animation speed:', animationSpeed);
|
368 |
-
}
|
369 |
-
|
370 |
-
// Auto-start after page load
|
371 |
-
setTimeout(() => {
|
372 |
-
console.log('🚀 Auto-starting wind particles...');
|
373 |
-
startParticles();
|
374 |
-
}, 1000);
|
375 |
-
</script>
|
376 |
-
</body>
|
377 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages.txt
DELETED
@@ -1,2 +0,0 @@
|
|
1 |
-
git
|
2 |
-
git-lfs
|
|
|
|
|
|
requirements.txt
DELETED
@@ -1,8 +0,0 @@
|
|
1 |
-
gradio==4.44.0
|
2 |
-
folium==0.15.1
|
3 |
-
numpy
|
4 |
-
pandas
|
5 |
-
requests
|
6 |
-
ecmwf-opendata>=0.3.0
|
7 |
-
xarray
|
8 |
-
cfgrib
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|