nakas commited on
Commit
2ccd08d
·
1 Parent(s): 0a133a5
Files changed (8) hide show
  1. .DS_Store +0 -0
  2. Dockerfile +0 -23
  3. README.md +0 -12
  4. app.py +0 -1067
  5. app.py.backup +0 -692
  6. index.html +0 -377
  7. packages.txt +0 -2
  8. 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