Spaces:
Sleeping
Sleeping
#!/usr/bin/env python3 | |
""" | |
ECMWF Real Wind Particle Visualization with Dual Layers | |
Downloads current ECMWF 10m and 100m wind data and visualizes with particles | |
""" | |
import gradio as gr | |
import folium | |
from branca.element import Element | |
import json | |
import sys | |
import requests | |
import numpy as np | |
import xarray as xr | |
import tempfile | |
import os | |
from datetime import datetime, timedelta | |
import warnings | |
warnings.filterwarnings('ignore') | |
# Import ECMWF OpenData client | |
try: | |
from ecmwf.opendata import Client as OpenDataClient | |
OPENDATA_AVAILABLE = True | |
except ImportError: | |
OPENDATA_AVAILABLE = False | |
def log_step(step, message): | |
"""Log each step with clear formatting""" | |
print(f"π STEP {step}: {message}") | |
sys.stdout.flush() | |
class ECMWFWindDataProcessor: | |
"""Process real ECMWF wind data for particle visualization""" | |
def __init__(self): | |
self.temp_dir = tempfile.mkdtemp() | |
self.client = None | |
if OPENDATA_AVAILABLE: | |
try: | |
self.client = OpenDataClient() | |
except: | |
self.client = None | |
# AWS S3 direct access URLs for ECMWF open data | |
self.aws_base_url = "https://ecmwf-forecasts.s3.eu-central-1.amazonaws.com" | |
def get_latest_forecast_info(self): | |
"""Get the latest available forecast run information""" | |
try: | |
# ECMWF runs at 00, 06, 12, 18 UTC | |
now = datetime.utcnow() | |
# Find the most recent model run (data available 7-9 hours after run time) | |
for hours_back in range(4, 24, 6): # Check recent runs | |
test_time = now - timedelta(hours=hours_back) | |
# Round to nearest 6-hour cycle | |
run_hour = (test_time.hour // 6) * 6 | |
run_time = test_time.replace(hour=run_hour, minute=0, second=0, microsecond=0) | |
date_str = run_time.strftime("%Y%m%d") | |
time_str = f"{run_hour:02d}" | |
return date_str, time_str, run_time | |
# Fallback | |
return now.strftime("%Y%m%d"), "12", now | |
except Exception as e: | |
# Emergency fallback | |
now = datetime.utcnow() | |
return now.strftime("%Y%m%d"), "12", now | |
def download_wind_component(self, parameter="10u", step=0, max_retries=3): | |
"""Download ECMWF wind component data (10u, 10v, 100u, 100v)""" | |
date_str, time_str, run_time = self.get_latest_forecast_info() | |
# Method 1: Try ecmwf-opendata client (most reliable) | |
if OPENDATA_AVAILABLE and self.client: | |
try: | |
filename = os.path.join(self.temp_dir, f'ecmwf_{parameter}_{step}h_{datetime.now().strftime("%Y%m%d_%H%M%S")}.grib') | |
log_step("DOWNLOAD", f"Downloading {parameter} component via ECMWF client...") | |
self.client.retrieve( | |
type="fc", # forecast | |
param=parameter, # 10u, 10v, 100u, 100v | |
step=step, # forecast hour | |
target=filename | |
) | |
if os.path.exists(filename) and os.path.getsize(filename) > 1000: | |
log_step("SUCCESS", f"Downloaded {parameter} component ({os.path.getsize(filename)} bytes)") | |
return filename, f"β ECMWF {parameter} data downloaded successfully!\\nRun: {date_str} {time_str}z, Step: +{step}h" | |
except Exception as e: | |
log_step("ERROR", f"Client method failed: {str(e)}") | |
return None, f"β Unable to download ECMWF {parameter} data" | |
def extract_wind_data_from_grib(self, filename, parameter): | |
"""Extract wind data from GRIB file and return as array""" | |
try: | |
log_step("EXTRACT", f"Processing GRIB file for {parameter}...") | |
# Open the GRIB file with xarray | |
try: | |
ds = xr.open_dataset(filename, engine='cfgrib', backend_kwargs={'indexpath': ''}) | |
except: | |
ds = xr.open_dataset(filename, engine='cfgrib') | |
# Find the right variable | |
data_vars = list(ds.data_vars.keys()) | |
if not data_vars: | |
return None, None, None, "No data variables found in file" | |
data_var = data_vars[0] | |
data = ds[data_var] | |
# Handle coordinates | |
if 'latitude' in ds.coords: | |
lats = ds.latitude.values | |
lons = ds.longitude.values | |
elif 'lat' in ds.coords: | |
lats = ds.lat.values | |
lons = ds.lon.values | |
else: | |
return None, None, None, "Could not find latitude/longitude coordinates" | |
# Get the data values (select first time step if multiple) | |
if 'time' in data.dims and len(data.time) > 1: | |
values = data.isel(time=0).values | |
elif 'valid_time' in data.dims: | |
values = data.isel(valid_time=0).values | |
else: | |
values = data.values | |
# Handle 3D data (select first level if needed) | |
if values.ndim > 2: | |
values = values[0] | |
log_step("SUCCESS", f"Extracted {parameter}: {values.shape} grid, lat range: {lats.min():.1f} to {lats.max():.1f}") | |
ds.close() | |
return lats, lons, values, "Success" | |
except Exception as e: | |
return None, None, None, f"Error extracting data: {str(e)}" | |
def convert_to_wind_json(self, u_lats, u_lons, u_values, v_lats, v_lons, v_values, wind_level="10m"): | |
"""Convert ECMWF wind components to leaflet-velocity JSON format""" | |
try: | |
log_step("CONVERT", f"Converting ECMWF data to wind visualization format for {wind_level}...") | |
# Ensure grids match | |
if not (np.array_equal(u_lats, v_lats) and np.array_equal(u_lons, v_lons)): | |
log_step("WARNING", "U and V grids don't match exactly, using U grid as reference") | |
# Use U component grid as reference | |
lats = u_lats | |
lons = u_lons | |
# Ensure lats are in descending order (North to South) for leaflet-velocity | |
if lats[0] < lats[-1]: | |
lats = lats[::-1] | |
u_values = u_values[::-1, :] | |
v_values = v_values[::-1, :] | |
# Convert to lists and flatten in row-major order | |
u_data = u_values.flatten().tolist() | |
v_data = v_values.flatten().tolist() | |
# Replace any NaN values with 0 | |
u_data = [0.0 if np.isnan(x) else float(x) for x in u_data] | |
v_data = [0.0 if np.isnan(x) else float(x) for x in v_data] | |
# Create grid info | |
ny, nx = u_values.shape | |
lo1 = float(lons[0]) | |
lo2 = float(lons[-1]) | |
la1 = float(lats[0]) # North (highest) | |
la2 = float(lats[-1]) # South (lowest) | |
dx = float(lons[1] - lons[0]) | |
dy = float(lats[0] - lats[1]) # Should be positive since lats are descending | |
current_time = datetime.utcnow() | |
ref_time = current_time.strftime("%Y-%m-%d %H:00:00") | |
# Create leaflet-velocity compatible JSON structure | |
wind_data = [ | |
{ | |
"header": { | |
"discipline": 0, | |
"parameterCategory": 2, | |
"parameterNumber": 2, | |
"parameterName": "UGRD", | |
"parameterNumberName": "eastward_wind", | |
"nx": nx, | |
"ny": ny, | |
"lo1": lo1, | |
"la1": la1, | |
"lo2": lo2, | |
"la2": la2, | |
"dx": dx, | |
"dy": dy, | |
"refTime": ref_time | |
}, | |
"data": u_data | |
}, | |
{ | |
"header": { | |
"discipline": 0, | |
"parameterCategory": 2, | |
"parameterNumber": 3, | |
"parameterName": "VGRD", | |
"parameterNumberName": "northward_wind", | |
"nx": nx, | |
"ny": ny, | |
"lo1": lo1, | |
"la1": la1, | |
"lo2": lo2, | |
"la2": la2, | |
"dx": dx, | |
"dy": dy, | |
"refTime": ref_time | |
}, | |
"data": v_data | |
} | |
] | |
log_step("SUCCESS", f"Converted {wind_level} to wind JSON: {nx}x{ny} grid, {len(u_data)} points each") | |
return wind_data, f"Successfully converted ECMWF {wind_level} data to wind visualization format" | |
except Exception as e: | |
return None, f"Error converting data: {str(e)}" | |
def fetch_real_ecmwf_wind_data(): | |
"""Download and process real ECMWF 10m and 100m wind data""" | |
log_step("WIND-1", "π Fetching REAL ECMWF wind data (10m and 100m)...") | |
processor = ECMWFWindDataProcessor() | |
try: | |
# Download 10m wind components | |
log_step("WIND-2", "Downloading 10m U wind component...") | |
u10_file, u10_msg = processor.download_wind_component("10u", step=0) | |
log_step("WIND-3", "Downloading 10m V wind component...") | |
v10_file, v10_msg = processor.download_wind_component("10v", step=0) | |
# Download 100m wind components | |
log_step("WIND-4", "Downloading 100m U wind component...") | |
u100_file, u100_msg = processor.download_wind_component("100u", step=0) | |
log_step("WIND-5", "Downloading 100m V wind component...") | |
v100_file, v100_msg = processor.download_wind_component("100v", step=0) | |
# Process 10m data | |
wind_data_10m = None | |
if u10_file and v10_file: | |
log_step("WIND-6", "Processing 10m wind data...") | |
u10_lats, u10_lons, u10_values, u10_status = processor.extract_wind_data_from_grib(u10_file, "10u") | |
v10_lats, v10_lons, v10_values, v10_status = processor.extract_wind_data_from_grib(v10_file, "10v") | |
if u10_values is not None and v10_values is not None: | |
wind_data_10m, convert_msg = processor.convert_to_wind_json( | |
u10_lats, u10_lons, u10_values, v10_lats, v10_lons, v10_values, "10m" | |
) | |
# Process 100m data | |
wind_data_100m = None | |
if u100_file and v100_file: | |
log_step("WIND-7", "Processing 100m wind data...") | |
u100_lats, u100_lons, u100_values, u100_status = processor.extract_wind_data_from_grib(u100_file, "100u") | |
v100_lats, v100_lons, v100_values, v100_status = processor.extract_wind_data_from_grib(v100_file, "100v") | |
if u100_values is not None and v100_values is not None: | |
wind_data_100m, convert_msg = processor.convert_to_wind_json( | |
u100_lats, u100_lons, u100_values, v100_lats, v100_lons, v100_values, "100m" | |
) | |
# Return real data if available, otherwise fallback | |
if wind_data_10m is None: | |
wind_data_10m = generate_synthetic_wind_data("10m") | |
if wind_data_100m is None: | |
wind_data_100m = generate_synthetic_wind_data("100m") | |
log_step("WIND-8", f"β SUCCESS: Wind data ready!") | |
return wind_data_10m, wind_data_100m | |
except Exception as e: | |
log_step("WIND-ERROR", f"Failed to fetch real wind data: {str(e)}") | |
log_step("WIND-FALLBACK", "Falling back to synthetic data...") | |
# Fallback to synthetic data | |
return generate_synthetic_wind_data("10m"), generate_synthetic_wind_data("100m") | |
def generate_synthetic_wind_data(wind_level="10m"): | |
"""Generate synthetic wind data as fallback""" | |
log_step("GEN-1", f"Generating synthetic {wind_level} wind data...") | |
# Basic global grid | |
nx, ny = 72, 36 | |
lon_min, lon_max = -180, 175 | |
lat_min, lat_max = -85, 85 | |
lons = np.linspace(lon_min, lon_max, nx) | |
lats = np.linspace(lat_max, lat_min, ny) | |
u_data = [] | |
v_data = [] | |
# Adjust wind strength based on level | |
strength_multiplier = 1.5 if wind_level == "100m" else 1.0 | |
for j, lat in enumerate(lats): | |
for i, lon in enumerate(lons): | |
# Simple wind pattern with different strength for different levels | |
u = (10 * np.sin(np.radians(lon/2)) + np.random.normal(0, 3)) * strength_multiplier | |
v = (5 * np.cos(np.radians(lat)) + np.random.normal(0, 2)) * strength_multiplier | |
u_data.append(round(u, 2)) | |
v_data.append(round(v, 2)) | |
current_time = datetime.utcnow() | |
ref_time = current_time.strftime("%Y-%m-%d %H:00:00") | |
wind_data = [ | |
{ | |
"header": { | |
"discipline": 0, | |
"parameterCategory": 2, | |
"parameterNumber": 2, | |
"parameterName": "UGRD", | |
"parameterNumberName": "eastward_wind", | |
"nx": nx, | |
"ny": ny, | |
"lo1": lon_min, | |
"la1": lat_max, | |
"lo2": lon_max, | |
"la2": lat_min, | |
"dx": 5.0, | |
"dy": 5.0, | |
"refTime": ref_time | |
}, | |
"data": u_data | |
}, | |
{ | |
"header": { | |
"discipline": 0, | |
"parameterCategory": 2, | |
"parameterNumber": 3, | |
"parameterName": "VGRD", | |
"parameterNumberName": "northward_wind", | |
"nx": nx, | |
"ny": ny, | |
"lo1": lon_min, | |
"la1": lat_max, | |
"lo2": lon_max, | |
"la2": lat_min, | |
"dx": 5.0, | |
"dy": 5.0, | |
"refTime": ref_time | |
}, | |
"data": v_data | |
} | |
] | |
log_step("GEN-2", f"Generated synthetic {wind_level} wind data: {len(u_data)} points") | |
return wind_data | |
def create_wind_map(region="global"): | |
"""Create Leaflet-Velocity wind map with real ECMWF data""" | |
# Set map parameters based on region | |
if region == "global": | |
center = [20, 0] | |
zoom = 2 | |
elif region == "north_america": | |
center = [40, -100] | |
zoom = 3 | |
elif region == "europe": | |
center = [50, 10] | |
zoom = 4 | |
else: | |
center = [20, 0] | |
zoom = 2 | |
# Create map | |
m = folium.Map( | |
location=center, | |
tiles="CartoDB dark_matter", | |
zoom_start=zoom, | |
control_scale=True, | |
width='100%', | |
height='600px' | |
) | |
# Add light theme option | |
folium.TileLayer( | |
tiles="CartoDB positron", | |
name="Light Theme", | |
control=True | |
).add_to(m) | |
# Fetch real ECMWF wind data for both levels | |
log_step(5, "Fetching real ECMWF wind data...") | |
wind_data_10m, wind_data_100m = fetch_real_ecmwf_wind_data() | |
log_step(6, f"Wind data ready: 10m={len(wind_data_10m)} components, 100m={len(wind_data_100m)} components") | |
# Add Leaflet-Velocity from CDN | |
velocity_css = """ | |
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |
""" | |
m.get_root().html.add_child(Element(velocity_css)) | |
velocity_js = """ | |
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
<script src="https://unpkg.com/leaflet-velocity@1.8.0/dist/leaflet-velocity.min.js"></script> | |
""" | |
m.get_root().html.add_child(Element(velocity_js)) | |
# Get map variable name | |
map_id = m.get_name() | |
# Add wind visualization with embedded data | |
js_code = f""" | |
<script> | |
setTimeout(function() {{ | |
console.log("Initializing wind particles..."); | |
var map = {map_id}; | |
var windData10m = {json.dumps(wind_data_10m)}; | |
var windData100m = {json.dumps(wind_data_100m)}; | |
console.log("10m Wind data loaded:", windData10m); | |
console.log("100m Wind data loaded:", windData100m); | |
// Variables to track velocity layers | |
var current10mLayer = null; | |
var current100mLayer = null; | |
var show10m = true; | |
var show100m = false; | |
// Function to get theme-appropriate color scale | |
function getColorScale(windType) {{ | |
// Check if we're on light theme by looking at current tile layer | |
var currentTileLayer = 'dark'; // default to dark | |
map.eachLayer(function(layer) {{ | |
if (layer.options && layer.options.tiles) {{ | |
if (layer.options.tiles.includes('positron') || layer.options.tiles.includes('light')) {{ | |
currentTileLayer = 'light'; | |
}} | |
}} | |
// Also check the layer name/attribution | |
if (layer.options && layer.options.name === 'Light Theme') {{ | |
if (map.hasLayer(layer)) {{ | |
currentTileLayer = 'light'; | |
}} | |
}} | |
}}); | |
console.log("Current theme detected:", currentTileLayer); | |
// Different color schemes for 10m (blue) and 100m (red) winds | |
if (windType === '100m') {{ | |
// Red color scheme for 100m winds | |
if (currentTileLayer === 'light') {{ | |
// DARK red colors for light theme (maximum contrast on white) | |
return [ | |
"#4c0000", "#660000", "#800000", "#990000", "#b30000", | |
"#cc0000", "#e60000", "#ff0000", "#ff3333", "#ff6666", "#ff9999" | |
]; | |
}} else {{ | |
// LIGHT red colors for dark theme (maximum visibility on black) | |
return [ | |
"#ff9999", "#ff6666", "#ff3333", "#ff0000", "#e60000", | |
"#cc0000", "#b30000", "#990000", "#800000", "#660000", "#ffcccc" | |
]; | |
}} | |
}} else {{ | |
// Blue color scheme for 10m winds | |
if (currentTileLayer === 'light') {{ | |
// DARK blue colors for light theme (maximum contrast on white) | |
return [ | |
"#000066", "#000080", "#000099", "#0000b3", "#0000cc", | |
"#0000e6", "#0000ff", "#3333ff", "#6666ff", "#9999ff", "#ccccff" | |
]; | |
}} else {{ | |
// LIGHT blue colors for dark theme (maximum visibility on black) | |
return [ | |
"#ccccff", "#9999ff", "#6666ff", "#3333ff", "#0000ff", | |
"#0000e6", "#0000cc", "#0000b3", "#000099", "#000080", "#ffffff" | |
]; | |
}} | |
}} | |
}} | |
// Check if L.velocityLayer exists | |
if (typeof L.velocityLayer === 'undefined') {{ | |
console.error("Leaflet-Velocity not loaded properly"); | |
return; | |
}} | |
// Function to create 10m wind layer | |
function create10mLayer() {{ | |
return L.velocityLayer({{ | |
data: windData10m, | |
displayValues: true, | |
displayOptions: {{ | |
velocityType: "10m Wind", | |
position: "bottomright", | |
emptyString: "No wind data", | |
speedUnit: "m/s", | |
angleConvention: "bearingCW", | |
showCardinal: true | |
}}, | |
velocityScale: 0.02, | |
opacity: 0.9, | |
maxVelocity: 20, | |
particleMultiplier: 0.01, | |
lineWidth: 2, | |
colorScale: getColorScale('10m'), | |
// Smaller tails and speed for low winds | |
velocityAgeScale: [ | |
[0, 20], // Very slow winds = very short tails | |
[3, 40], // Low winds = short tails | |
[8, 80], // Moderate winds = medium tails | |
[15, 120], // Fast winds = longer tails | |
[25, 160] // Very fast winds = longest tails | |
] | |
}}); | |
}} | |
// Function to create 100m wind layer | |
function create100mLayer() {{ | |
return L.velocityLayer({{ | |
data: windData100m, | |
displayValues: true, | |
displayOptions: {{ | |
velocityType: "100m Wind", | |
position: "bottomleft", | |
emptyString: "No wind data", | |
speedUnit: "m/s", | |
angleConvention: "bearingCW", | |
showCardinal: true | |
}}, | |
velocityScale: 0.025, | |
opacity: 0.8, | |
maxVelocity: 30, | |
particleMultiplier: 0.008, | |
lineWidth: 2.5, | |
colorScale: getColorScale('100m'), | |
// Different tail scaling for stronger 100m winds | |
velocityAgeScale: [ | |
[0, 30], // Very slow winds = short tails | |
[5, 60], // Low winds = medium tails | |
[12, 100], // Moderate winds = longer tails | |
[20, 140], // Fast winds = long tails | |
[35, 180] // Very fast winds = longest tails | |
] | |
}}); | |
}} | |
try {{ | |
// Create initial 10m layer | |
if (show10m) {{ | |
current10mLayer = create10mLayer(); | |
current10mLayer.addTo(map); | |
console.log("β 10m wind particles added successfully!"); | |
}} | |
// Optionally create 100m layer | |
if (show100m) {{ | |
current100mLayer = create100mLayer(); | |
current100mLayer.addTo(map); | |
console.log("β 100m wind particles added successfully!"); | |
}} | |
// Function to toggle 10m wind layer | |
function toggle10mLayer() {{ | |
show10m = !show10m; | |
if (show10m) {{ | |
if (current10mLayer) map.removeLayer(current10mLayer); | |
current10mLayer = create10mLayer(); | |
current10mLayer.addTo(map); | |
console.log("β 10m wind layer enabled"); | |
}} else {{ | |
if (current10mLayer) {{ | |
map.removeLayer(current10mLayer); | |
current10mLayer = null; | |
console.log("β 10m wind layer disabled"); | |
}} | |
}} | |
}} | |
// Function to toggle 100m wind layer | |
function toggle100mLayer() {{ | |
show100m = !show100m; | |
if (show100m) {{ | |
if (current100mLayer) map.removeLayer(current100mLayer); | |
current100mLayer = create100mLayer(); | |
current100mLayer.addTo(map); | |
console.log("β 100m wind layer enabled"); | |
}} else {{ | |
if (current100mLayer) {{ | |
map.removeLayer(current100mLayer); | |
current100mLayer = null; | |
console.log("β 100m wind layer disabled"); | |
}} | |
}} | |
}} | |
// Function to reload particles with new theme colors | |
function reloadParticlesWithTheme() {{ | |
console.log("π¨ Theme changed, updating particle colors..."); | |
// Reload 10m layer if active | |
if (show10m && current10mLayer) {{ | |
map.removeLayer(current10mLayer); | |
current10mLayer = create10mLayer(); | |
current10mLayer.addTo(map); | |
}} | |
// Reload 100m layer if active | |
if (show100m && current100mLayer) {{ | |
map.removeLayer(current100mLayer); | |
current100mLayer = create100mLayer(); | |
current100mLayer.addTo(map); | |
}} | |
console.log("β‘ Particle colors updated for theme!"); | |
}} | |
// Immediate particle reload function for pan/zoom events | |
function immediateParticleReload() {{ | |
console.log("β‘ PAN/ZOOM END: Immediate particle reload..."); | |
// Remove old particles immediately | |
if (show10m && current10mLayer) {{ | |
console.log("β‘ Removing old 10m particles immediately..."); | |
map.removeLayer(current10mLayer); | |
console.log("β‘ Creating new 10m particles at full density..."); | |
current10mLayer = create10mLayer(); | |
current10mLayer.addTo(map); | |
}} | |
if (show100m && current100mLayer) {{ | |
console.log("β‘ Removing old 100m particles immediately..."); | |
map.removeLayer(current100mLayer); | |
console.log("β‘ Creating new 100m particles at full density..."); | |
current100mLayer = create100mLayer(); | |
current100mLayer.addTo(map); | |
}} | |
console.log("β‘ New particles loaded immediately - complete!"); | |
}} | |
// Event handlers for map interaction - immediate reload on mouse release | |
map.on('moveend', immediateParticleReload); // Pan end | |
map.on('zoomend', immediateParticleReload); // Zoom end | |
map.on('dragend', immediateParticleReload); // Drag end | |
// Event handler for theme changes | |
map.on('baselayerchange', function() {{ | |
setTimeout(reloadParticlesWithTheme, 100); // Small delay to ensure theme change is complete | |
}}); | |
// Add custom controls for wind layer toggles | |
var windControlDiv = L.DomUtil.create('div', 'wind-controls'); | |
windControlDiv.style.position = 'absolute'; | |
windControlDiv.style.top = '10px'; | |
windControlDiv.style.right = '10px'; | |
windControlDiv.style.zIndex = '1000'; | |
windControlDiv.style.background = 'rgba(255,255,255,0.9)'; | |
windControlDiv.style.padding = '10px'; | |
windControlDiv.style.borderRadius = '5px'; | |
windControlDiv.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; | |
windControlDiv.style.fontFamily = 'Arial, sans-serif'; | |
windControlDiv.style.fontSize = '12px'; | |
var button10m = L.DomUtil.create('button', 'wind-toggle-10m', windControlDiv); | |
button10m.innerHTML = 'πͺοΈ 10m Winds (ON)'; | |
button10m.style.display = 'block'; | |
button10m.style.width = '120px'; | |
button10m.style.margin = '2px 0'; | |
button10m.style.padding = '5px'; | |
button10m.style.border = 'none'; | |
button10m.style.borderRadius = '3px'; | |
button10m.style.background = '#3b82f6'; | |
button10m.style.color = 'white'; | |
button10m.style.cursor = 'pointer'; | |
button10m.style.fontSize = '11px'; | |
var button100m = L.DomUtil.create('button', 'wind-toggle-100m', windControlDiv); | |
button100m.innerHTML = 'π 100m Winds (OFF)'; | |
button100m.style.display = 'block'; | |
button100m.style.width = '120px'; | |
button100m.style.margin = '2px 0'; | |
button100m.style.padding = '5px'; | |
button100m.style.border = 'none'; | |
button100m.style.borderRadius = '3px'; | |
button100m.style.background = '#6b7280'; | |
button100m.style.color = 'white'; | |
button100m.style.cursor = 'pointer'; | |
button100m.style.fontSize = '11px'; | |
// Event handlers for toggle buttons | |
L.DomEvent.on(button10m, 'click', function() {{ | |
toggle10mLayer(); | |
button10m.innerHTML = show10m ? 'πͺοΈ 10m Winds (ON)' : 'πͺοΈ 10m Winds (OFF)'; | |
button10m.style.background = show10m ? '#3b82f6' : '#6b7280'; | |
}}); | |
L.DomEvent.on(button100m, 'click', function() {{ | |
toggle100mLayer(); | |
button100m.innerHTML = show100m ? 'π 100m Winds (ON)' : 'π 100m Winds (OFF)'; | |
button100m.style.background = show100m ? '#dc2626' : '#6b7280'; | |
}}); | |
// Add controls to map | |
map.getContainer().appendChild(windControlDiv); | |
console.log("β Toggle controls added to map!"); | |
// Force a map update to trigger particle rendering | |
setTimeout(function() {{ | |
map.invalidateSize(); | |
console.log("Map size invalidated to trigger particles"); | |
}}, 1000); | |
}} catch (error) {{ | |
console.error("Error creating velocity layer:", error); | |
}} | |
}}, 2000); | |
</script> | |
""" | |
m.get_root().html.add_child(Element(js_code)) | |
# Add layer control | |
folium.LayerControl().add_to(m) | |
return m._repr_html_() | |
def update_visualization(region): | |
"""Update wind visualization for selected region""" | |
try: | |
print(f"π Creating wind visualization for {region}") | |
map_html = create_wind_map(region) | |
success_msg = f"β Wind particles loaded for {region.replace('_', ' ').title()}" | |
print(success_msg) | |
return map_html, success_msg | |
except Exception as e: | |
error_msg = f"β Error: {str(e)}" | |
print(error_msg) | |
return f"<div style='padding: 20px; color: red;'>Error: {str(e)}</div>", error_msg | |
# Create Gradio interface | |
with gr.Blocks(title="Wind Particle Visualization") as app: | |
gr.Markdown(""" | |
# πͺοΈ UPDATED Wind Particle Visualization - v2.0 | |
**Dual-layer wind visualization with 10m and 100m winds** | |
ποΈ **Look for toggle controls in the top-right corner of the map!** | |
πͺοΈ **10m winds (blue)** and π **100m winds (red)** available | |
*Last updated: August 15, 2025 - 2:50 AM* | |
""") | |
with gr.Row(): | |
with gr.Column(scale=1): | |
region = gr.Radio( | |
choices=["global", "north_america", "europe"], | |
value="global", | |
label="πΊοΈ Region" | |
) | |
update_btn = gr.Button("πͺοΈ Update Visualization", variant="primary") | |
status = gr.Textbox( | |
label="Status", | |
lines=2, | |
value="π Loading wind particles..." | |
) | |
gr.Markdown(""" | |
### β¨ Features: | |
- **Dual wind levels**: 10m (blue) and 100m (red) wind visualization | |
- **Toggle controls**: Independent on/off buttons for each wind level | |
- **Adaptive colors**: Lighter colors on dark theme, darker on light theme | |
- **Dynamic tails**: Smaller tails and slower speed for low winds | |
- **Theme switching**: Colors update instantly when changing themes | |
### πͺοΈ Wind Levels: | |
- **10m winds** (surface): Blue particles, standard speed | |
- **100m winds** (altitude): Red particles, stronger/faster | |
### π― Controls: | |
- **Top-right corner**: Toggle buttons for wind layers | |
- **Layer control**: Switch between dark/light map themes | |
- **Wind displays**: Bottom corners show current wind data | |
### π― Troubleshooting: | |
- **Wait 2-3 seconds** for particles to appear | |
- **Use toggle buttons** to enable/disable wind layers | |
- **Check browser console** (F12) for error messages | |
- Particles appear as **flowing lines** with theme-appropriate colors | |
""") | |
with gr.Column(scale=3): | |
wind_map = gr.HTML( | |
label="Wind Particle Animation", | |
value="<div style='padding: 40px; text-align: center;'>π Loading wind particle visualization...</div>" | |
) | |
# Event handlers | |
update_btn.click( | |
update_visualization, | |
inputs=[region], | |
outputs=[wind_map, status] | |
) | |
region.change( | |
update_visualization, | |
inputs=[region], | |
outputs=[wind_map, status] | |
) | |
# Auto-load on startup | |
app.load( | |
lambda: update_visualization("global"), | |
outputs=[wind_map, status] | |
) | |
if __name__ == "__main__": | |
print("π Starting UPDATED Wind Particle Visualization with Dual Layers...") | |
app.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False | |
) |