Spaces:
Sleeping
Add folium/leaflet map visualization with grayscale smoke rendering
Browse filesβ’ Implement FoliumSmokeRenderer for professional smoke visualization
β’ Add grayscale styling (light/medium/heavy smoke in shades of gray)
β’ Support polygon-based and heat map visualization modes
β’ Include multiple base layers (OSM, Satellite, Clean)
β’ Add interactive popups with smoke density information
β’ Integrate map type selection in UI (Plotly vs Folium)
β’ Create comprehensive legend and layer controls
β’ Support graceful fallback when folium unavailable
π Leaflet maps perfect for satellite-style smoke plume visualization
π¨ Professional grayscale rendering matches traditional weather imagery
π± Mobile-friendly responsive design with touch controls
π€ Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app.py +256 -5
- requirements.txt +1 -1
- test_folium.py +153 -0
|
@@ -53,6 +53,221 @@ except ImportError as e:
|
|
| 53 |
HMS_LIBS_AVAILABLE = False
|
| 54 |
print(f"HMS POLYGON LIBRARIES NOT AVAILABLE: {e}")
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
class HMSSmokePolygonGenerator:
|
| 57 |
"""Generate HMS-style smoke plume polygons from HRRR-Smoke data"""
|
| 58 |
|
|
@@ -902,6 +1117,18 @@ class HRRRSmokeApp:
|
|
| 902 |
height=600
|
| 903 |
)
|
| 904 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 905 |
except Exception as e:
|
| 906 |
print(f"Contour plot failed, trying scatter: {e}")
|
| 907 |
|
|
@@ -957,7 +1184,12 @@ class HRRRSmokeApp:
|
|
| 957 |
font=dict(size=16)
|
| 958 |
)
|
| 959 |
|
| 960 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 961 |
|
| 962 |
except Exception as e:
|
| 963 |
print(f"Smoke map creation error: {e}")
|
|
@@ -1201,6 +1433,16 @@ with gr.Blocks(title="HRRR Smoke Forecast", theme=gr.themes.Soft()) as app:
|
|
| 1201 |
info="Generate NOAA HMS-style smoke plume polygons"
|
| 1202 |
)
|
| 1203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
update_btn = gr.Button("π Get Smoke Forecast", variant="primary", size="lg")
|
| 1205 |
|
| 1206 |
gr.HTML("""
|
|
@@ -1215,6 +1457,8 @@ with gr.Blocks(title="HRRR Smoke Forecast", theme=gr.themes.Soft()) as app:
|
|
| 1215 |
<li>πΊοΈ <strong>High resolution:</strong> 3km grid spacing</li>
|
| 1216 |
<li>π <strong>HMS-style polygons:</strong> NOAA-compatible plume boundaries</li>
|
| 1217 |
<li>π¦ <strong>KMZ export:</strong> Google Earth compatible files</li>
|
|
|
|
|
|
|
| 1218 |
</ul>
|
| 1219 |
<p style="font-size: 0.8em; margin-top: 1rem; opacity: 0.9;">
|
| 1220 |
<strong>Model:</strong> NOAA HRRR-Smoke provides operational smoke forecasts
|
|
@@ -1242,7 +1486,7 @@ with gr.Blocks(title="HRRR Smoke Forecast", theme=gr.themes.Soft()) as app:
|
|
| 1242 |
)
|
| 1243 |
|
| 1244 |
def update_display_wrapper(*args):
|
| 1245 |
-
"""Wrapper to handle KMZ file display"""
|
| 1246 |
result = smoke_app.update_smoke_display(*args)
|
| 1247 |
|
| 1248 |
if len(result) == 4 and result[3] is not None:
|
|
@@ -1255,21 +1499,28 @@ with gr.Blocks(title="HRRR Smoke Forecast", theme=gr.themes.Soft()) as app:
|
|
| 1255 |
# Event handlers
|
| 1256 |
update_btn.click(
|
| 1257 |
fn=update_display_wrapper,
|
| 1258 |
-
inputs=[location, forecast_hour, parameter, detail_level, min_threshold, show_polygons],
|
| 1259 |
outputs=[status_text, smoke_map, narrative_forecast, kmz_download]
|
| 1260 |
)
|
| 1261 |
|
| 1262 |
# Auto-update when parameter changes
|
| 1263 |
parameter.change(
|
| 1264 |
fn=update_display_wrapper,
|
| 1265 |
-
inputs=[location, forecast_hour, parameter, detail_level, min_threshold, show_polygons],
|
| 1266 |
outputs=[status_text, smoke_map, narrative_forecast, kmz_download]
|
| 1267 |
)
|
| 1268 |
|
| 1269 |
# Update when polygon option changes
|
| 1270 |
show_polygons.change(
|
| 1271 |
fn=update_display_wrapper,
|
| 1272 |
-
inputs=[location, forecast_hour, parameter, detail_level, min_threshold, show_polygons],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1273 |
outputs=[status_text, smoke_map, narrative_forecast, kmz_download]
|
| 1274 |
)
|
| 1275 |
|
|
|
|
| 53 |
HMS_LIBS_AVAILABLE = False
|
| 54 |
print(f"HMS POLYGON LIBRARIES NOT AVAILABLE: {e}")
|
| 55 |
|
| 56 |
+
# Import folium for leaflet maps
|
| 57 |
+
try:
|
| 58 |
+
import folium
|
| 59 |
+
from folium.plugins import HeatMap
|
| 60 |
+
FOLIUM_AVAILABLE = True
|
| 61 |
+
print("FOLIUM AVAILABLE")
|
| 62 |
+
except ImportError as e:
|
| 63 |
+
FOLIUM_AVAILABLE = False
|
| 64 |
+
print(f"FOLIUM NOT AVAILABLE: {e}")
|
| 65 |
+
|
| 66 |
+
class FoliumSmokeRenderer:
|
| 67 |
+
"""Render smoke plumes on folium/leaflet maps with grayscale styling"""
|
| 68 |
+
|
| 69 |
+
def __init__(self):
|
| 70 |
+
# Grayscale smoke styling based on density
|
| 71 |
+
self.grayscale_styles = {
|
| 72 |
+
'light': {
|
| 73 |
+
'fillColor': '#E8E8E8', # Light gray
|
| 74 |
+
'color': '#CCCCCC', # Border
|
| 75 |
+
'weight': 1,
|
| 76 |
+
'fillOpacity': 0.4,
|
| 77 |
+
'opacity': 0.6
|
| 78 |
+
},
|
| 79 |
+
'medium': {
|
| 80 |
+
'fillColor': '#AAAAAA', # Medium gray
|
| 81 |
+
'color': '#888888', # Border
|
| 82 |
+
'weight': 2,
|
| 83 |
+
'fillOpacity': 0.6,
|
| 84 |
+
'opacity': 0.8
|
| 85 |
+
},
|
| 86 |
+
'heavy': {
|
| 87 |
+
'fillColor': '#666666', # Dark gray
|
| 88 |
+
'color': '#444444', # Border
|
| 89 |
+
'weight': 2,
|
| 90 |
+
'fillOpacity': 0.8,
|
| 91 |
+
'opacity': 1.0
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
def create_folium_map(self, polygons, center_lat=39.5, center_lon=-98.5, zoom_start=5):
|
| 96 |
+
"""Create folium map with grayscale smoke polygons"""
|
| 97 |
+
if not FOLIUM_AVAILABLE:
|
| 98 |
+
print("Folium not available")
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
# Create base map with satellite imagery
|
| 103 |
+
m = folium.Map(
|
| 104 |
+
location=[center_lat, center_lon],
|
| 105 |
+
zoom_start=zoom_start,
|
| 106 |
+
tiles='OpenStreetMap' # Start with OSM, add satellite option
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# Add satellite imagery option
|
| 110 |
+
folium.TileLayer(
|
| 111 |
+
tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
| 112 |
+
attr='Esri',
|
| 113 |
+
name='Satellite',
|
| 114 |
+
overlay=False,
|
| 115 |
+
control=True
|
| 116 |
+
).add_to(m)
|
| 117 |
+
|
| 118 |
+
# Add CartoDB Positron for clean background
|
| 119 |
+
folium.TileLayer(
|
| 120 |
+
tiles='CartoDB positron',
|
| 121 |
+
name='Clean',
|
| 122 |
+
overlay=False,
|
| 123 |
+
control=True
|
| 124 |
+
).add_to(m)
|
| 125 |
+
|
| 126 |
+
# Add smoke polygons with grayscale styling
|
| 127 |
+
if polygons:
|
| 128 |
+
for i, poly_data in enumerate(polygons):
|
| 129 |
+
try:
|
| 130 |
+
# Get polygon coordinates
|
| 131 |
+
coords = list(poly_data['geometry'].exterior.coords)
|
| 132 |
+
# Convert to lat/lon pairs for folium (note: folium expects [lat, lon])
|
| 133 |
+
folium_coords = [[lat, lon] for lon, lat in coords]
|
| 134 |
+
|
| 135 |
+
# Get styling for density category
|
| 136 |
+
style = self.grayscale_styles.get(
|
| 137 |
+
poly_data['density_category'],
|
| 138 |
+
self.grayscale_styles['medium']
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# Create popup with smoke information
|
| 142 |
+
popup_html = f"""
|
| 143 |
+
<div style="font-family: Arial, sans-serif; width: 200px;">
|
| 144 |
+
<h4 style="margin: 0; color: #333;">π¬οΈ Smoke Plume</h4>
|
| 145 |
+
<hr style="margin: 5px 0;">
|
| 146 |
+
<p style="margin: 3px 0;"><b>Density:</b> {poly_data['description']}</p>
|
| 147 |
+
<p style="margin: 3px 0;"><b>Concentration:</b> {poly_data['density_value']:.1f} Β΅g/mΒ³</p>
|
| 148 |
+
<p style="margin: 3px 0;"><b>Area:</b> {poly_data['area_deg2']:.4f} degΒ²</p>
|
| 149 |
+
</div>
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
# Add polygon to map
|
| 153 |
+
folium.Polygon(
|
| 154 |
+
locations=folium_coords,
|
| 155 |
+
popup=folium.Popup(popup_html, max_width=300),
|
| 156 |
+
tooltip=f"{poly_data['description']}: {poly_data['density_value']:.1f} Β΅g/mΒ³",
|
| 157 |
+
**style
|
| 158 |
+
).add_to(m)
|
| 159 |
+
|
| 160 |
+
except Exception as e:
|
| 161 |
+
print(f"Error adding polygon {i} to folium map: {e}")
|
| 162 |
+
continue
|
| 163 |
+
|
| 164 |
+
# Add layer control
|
| 165 |
+
folium.LayerControl().add_to(m)
|
| 166 |
+
|
| 167 |
+
# Add legend
|
| 168 |
+
self._add_smoke_legend(m)
|
| 169 |
+
|
| 170 |
+
return m
|
| 171 |
+
|
| 172 |
+
except Exception as e:
|
| 173 |
+
print(f"Folium map creation error: {e}")
|
| 174 |
+
return None
|
| 175 |
+
|
| 176 |
+
def _add_smoke_legend(self, folium_map):
|
| 177 |
+
"""Add smoke density legend to the map"""
|
| 178 |
+
legend_html = '''
|
| 179 |
+
<div style="position: fixed;
|
| 180 |
+
top: 10px; right: 10px; width: 150px; height: 120px;
|
| 181 |
+
background-color: white; border:2px solid grey; z-index:9999;
|
| 182 |
+
font-size:14px; padding: 10px; box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
|
| 183 |
+
">
|
| 184 |
+
<h4 style="margin: 0 0 10px 0; color: #333;">π¬οΈ Smoke Density</h4>
|
| 185 |
+
<div style="margin: 5px 0;">
|
| 186 |
+
<span style="display: inline-block; width: 20px; height: 15px;
|
| 187 |
+
background-color: #E8E8E8; border: 1px solid #CCC; margin-right: 5px;"></span>
|
| 188 |
+
<span style="font-size: 12px;">Light (0.5-15 Β΅g/mΒ³)</span>
|
| 189 |
+
</div>
|
| 190 |
+
<div style="margin: 5px 0;">
|
| 191 |
+
<span style="display: inline-block; width: 20px; height: 15px;
|
| 192 |
+
background-color: #AAAAAA; border: 1px solid #888; margin-right: 5px;"></span>
|
| 193 |
+
<span style="font-size: 12px;">Medium (15-35 Β΅g/mΒ³)</span>
|
| 194 |
+
</div>
|
| 195 |
+
<div style="margin: 5px 0;">
|
| 196 |
+
<span style="display: inline-block; width: 20px; height: 15px;
|
| 197 |
+
background-color: #666666; border: 1px solid #444; margin-right: 5px;"></span>
|
| 198 |
+
<span style="font-size: 12px;">Heavy (35+ Β΅g/mΒ³)</span>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
'''
|
| 202 |
+
|
| 203 |
+
folium_map.get_root().html.add_child(folium.Element(legend_html))
|
| 204 |
+
|
| 205 |
+
def create_gradient_smoke_map(self, lat2d, lon2d, smoke_values, center_lat=39.5, center_lon=-98.5, zoom_start=5):
|
| 206 |
+
"""Create folium map with gradient-based smoke visualization"""
|
| 207 |
+
if not FOLIUM_AVAILABLE:
|
| 208 |
+
print("Folium not available")
|
| 209 |
+
return None
|
| 210 |
+
|
| 211 |
+
try:
|
| 212 |
+
# Create base map
|
| 213 |
+
m = folium.Map(
|
| 214 |
+
location=[center_lat, center_lon],
|
| 215 |
+
zoom_start=zoom_start,
|
| 216 |
+
tiles='CartoDB positron'
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Add satellite imagery option
|
| 220 |
+
folium.TileLayer(
|
| 221 |
+
tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
| 222 |
+
attr='Esri',
|
| 223 |
+
name='Satellite',
|
| 224 |
+
overlay=False,
|
| 225 |
+
control=True
|
| 226 |
+
).add_to(m)
|
| 227 |
+
|
| 228 |
+
# Create heat map data from smoke values
|
| 229 |
+
heat_data = []
|
| 230 |
+
|
| 231 |
+
# Sample the grid to create heat points
|
| 232 |
+
ny, nx = smoke_values.shape
|
| 233 |
+
step = max(1, min(ny, nx) // 50) # Limit to ~50x50 points for performance
|
| 234 |
+
|
| 235 |
+
for i in range(0, ny, step):
|
| 236 |
+
for j in range(0, nx, step):
|
| 237 |
+
if not np.isnan(smoke_values[i, j]) and smoke_values[i, j] > 0.5:
|
| 238 |
+
lat = lat2d[i, j] if lat2d.ndim == 2 else lat2d[i]
|
| 239 |
+
lon = lon2d[i, j] if lon2d.ndim == 2 else lon2d[j]
|
| 240 |
+
|
| 241 |
+
if not (np.isnan(lat) or np.isnan(lon)):
|
| 242 |
+
# Normalize smoke value for heat intensity
|
| 243 |
+
intensity = min(1.0, smoke_values[i, j] / 100.0)
|
| 244 |
+
heat_data.append([lat, lon, intensity])
|
| 245 |
+
|
| 246 |
+
# Add heat map if we have data
|
| 247 |
+
if heat_data and FOLIUM_AVAILABLE:
|
| 248 |
+
HeatMap(
|
| 249 |
+
heat_data,
|
| 250 |
+
min_opacity=0.2,
|
| 251 |
+
max_zoom=18,
|
| 252 |
+
radius=15,
|
| 253 |
+
blur=10,
|
| 254 |
+
gradient={
|
| 255 |
+
0.0: 'rgba(0,0,0,0)', # Transparent
|
| 256 |
+
0.2: 'rgba(128,128,128,0.3)', # Light gray
|
| 257 |
+
0.5: 'rgba(96,96,96,0.6)', # Medium gray
|
| 258 |
+
1.0: 'rgba(64,64,64,0.9)' # Dark gray
|
| 259 |
+
}
|
| 260 |
+
).add_to(m)
|
| 261 |
+
|
| 262 |
+
# Add layer control
|
| 263 |
+
folium.LayerControl().add_to(m)
|
| 264 |
+
|
| 265 |
+
return m
|
| 266 |
+
|
| 267 |
+
except Exception as e:
|
| 268 |
+
print(f"Gradient smoke map creation error: {e}")
|
| 269 |
+
return None
|
| 270 |
+
|
| 271 |
class HMSSmokePolygonGenerator:
|
| 272 |
"""Generate HMS-style smoke plume polygons from HRRR-Smoke data"""
|
| 273 |
|
|
|
|
| 1117 |
height=600
|
| 1118 |
)
|
| 1119 |
|
| 1120 |
+
# Return appropriate map type
|
| 1121 |
+
if map_type == 'folium' and polygons:
|
| 1122 |
+
# Create folium map with grayscale smoke polygons
|
| 1123 |
+
folium_map = self.folium_renderer.create_folium_map(polygons)
|
| 1124 |
+
if folium_map:
|
| 1125 |
+
return folium_map
|
| 1126 |
+
else:
|
| 1127 |
+
# Fallback to plotly if folium fails
|
| 1128 |
+
return fig
|
| 1129 |
+
else:
|
| 1130 |
+
return fig
|
| 1131 |
+
|
| 1132 |
except Exception as e:
|
| 1133 |
print(f"Contour plot failed, trying scatter: {e}")
|
| 1134 |
|
|
|
|
| 1184 |
font=dict(size=16)
|
| 1185 |
)
|
| 1186 |
|
| 1187 |
+
# Return appropriate map type
|
| 1188 |
+
if map_type == 'folium':
|
| 1189 |
+
# Return empty folium map for errors in folium mode
|
| 1190 |
+
return folium.Map(location=[39.5, -98.5], zoom_start=5)
|
| 1191 |
+
else:
|
| 1192 |
+
return fig
|
| 1193 |
|
| 1194 |
except Exception as e:
|
| 1195 |
print(f"Smoke map creation error: {e}")
|
|
|
|
| 1433 |
info="Generate NOAA HMS-style smoke plume polygons"
|
| 1434 |
)
|
| 1435 |
|
| 1436 |
+
map_type = gr.Radio(
|
| 1437 |
+
choices=[
|
| 1438 |
+
("Interactive (Plotly)", "plotly"),
|
| 1439 |
+
("Leaflet/Grayscale (Folium)", "folium")
|
| 1440 |
+
],
|
| 1441 |
+
value="plotly",
|
| 1442 |
+
label="Map Type",
|
| 1443 |
+
info="Choose visualization style"
|
| 1444 |
+
)
|
| 1445 |
+
|
| 1446 |
update_btn = gr.Button("π Get Smoke Forecast", variant="primary", size="lg")
|
| 1447 |
|
| 1448 |
gr.HTML("""
|
|
|
|
| 1457 |
<li>πΊοΈ <strong>High resolution:</strong> 3km grid spacing</li>
|
| 1458 |
<li>π <strong>HMS-style polygons:</strong> NOAA-compatible plume boundaries</li>
|
| 1459 |
<li>π¦ <strong>KMZ export:</strong> Google Earth compatible files</li>
|
| 1460 |
+
<li>π <strong>Leaflet maps:</strong> Grayscale smoke visualization</li>
|
| 1461 |
+
<li>π¨ <strong>Multiple views:</strong> Interactive and satellite-ready styles</li>
|
| 1462 |
</ul>
|
| 1463 |
<p style="font-size: 0.8em; margin-top: 1rem; opacity: 0.9;">
|
| 1464 |
<strong>Model:</strong> NOAA HRRR-Smoke provides operational smoke forecasts
|
|
|
|
| 1486 |
)
|
| 1487 |
|
| 1488 |
def update_display_wrapper(*args):
|
| 1489 |
+
"""Wrapper to handle KMZ file display and map type"""
|
| 1490 |
result = smoke_app.update_smoke_display(*args)
|
| 1491 |
|
| 1492 |
if len(result) == 4 and result[3] is not None:
|
|
|
|
| 1499 |
# Event handlers
|
| 1500 |
update_btn.click(
|
| 1501 |
fn=update_display_wrapper,
|
| 1502 |
+
inputs=[location, forecast_hour, parameter, detail_level, min_threshold, show_polygons, map_type],
|
| 1503 |
outputs=[status_text, smoke_map, narrative_forecast, kmz_download]
|
| 1504 |
)
|
| 1505 |
|
| 1506 |
# Auto-update when parameter changes
|
| 1507 |
parameter.change(
|
| 1508 |
fn=update_display_wrapper,
|
| 1509 |
+
inputs=[location, forecast_hour, parameter, detail_level, min_threshold, show_polygons, map_type],
|
| 1510 |
outputs=[status_text, smoke_map, narrative_forecast, kmz_download]
|
| 1511 |
)
|
| 1512 |
|
| 1513 |
# Update when polygon option changes
|
| 1514 |
show_polygons.change(
|
| 1515 |
fn=update_display_wrapper,
|
| 1516 |
+
inputs=[location, forecast_hour, parameter, detail_level, min_threshold, show_polygons, map_type],
|
| 1517 |
+
outputs=[status_text, smoke_map, narrative_forecast, kmz_download]
|
| 1518 |
+
)
|
| 1519 |
+
|
| 1520 |
+
# Update when map type changes
|
| 1521 |
+
map_type.change(
|
| 1522 |
+
fn=update_display_wrapper,
|
| 1523 |
+
inputs=[location, forecast_hour, parameter, detail_level, min_threshold, show_polygons, map_type],
|
| 1524 |
outputs=[status_text, smoke_map, narrative_forecast, kmz_download]
|
| 1525 |
)
|
| 1526 |
|
|
@@ -15,7 +15,7 @@ dask>=2023.1.0
|
|
| 15 |
netcdf4>=1.6.0
|
| 16 |
cfgrib>=0.9.10
|
| 17 |
eccodes>=1.5.0
|
| 18 |
-
folium>=0.
|
| 19 |
shapely>=2.0.0
|
| 20 |
geopandas>=0.13.0
|
| 21 |
scikit-image>=0.20.0
|
|
|
|
| 15 |
netcdf4>=1.6.0
|
| 16 |
cfgrib>=0.9.10
|
| 17 |
eccodes>=1.5.0
|
| 18 |
+
folium>=0.14.0
|
| 19 |
shapely>=2.0.0
|
| 20 |
geopandas>=0.13.0
|
| 21 |
scikit-image>=0.20.0
|
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test folium/leaflet map functionality
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
def test_folium_availability():
|
| 7 |
+
"""Test if folium is available and basic functionality works"""
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
import folium
|
| 11 |
+
from folium.plugins import HeatMap
|
| 12 |
+
print("β
Folium and HeatMap available")
|
| 13 |
+
FOLIUM_AVAILABLE = True
|
| 14 |
+
except ImportError as e:
|
| 15 |
+
print(f"β Folium not available: {e}")
|
| 16 |
+
FOLIUM_AVAILABLE = False
|
| 17 |
+
return False
|
| 18 |
+
|
| 19 |
+
# Test basic map creation
|
| 20 |
+
try:
|
| 21 |
+
m = folium.Map(location=[39.5, -98.5], zoom_start=5)
|
| 22 |
+
print("β
Basic folium map creation works")
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"β Basic map creation failed: {e}")
|
| 25 |
+
return False
|
| 26 |
+
|
| 27 |
+
# Test polygon creation
|
| 28 |
+
try:
|
| 29 |
+
# Sample polygon coordinates (roughly Colorado)
|
| 30 |
+
coords = [
|
| 31 |
+
[40.0, -109.0],
|
| 32 |
+
[40.0, -102.0],
|
| 33 |
+
[37.0, -102.0],
|
| 34 |
+
[37.0, -109.0],
|
| 35 |
+
[40.0, -109.0] # Close polygon
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
folium.Polygon(
|
| 39 |
+
locations=coords,
|
| 40 |
+
popup="Test Smoke Plume",
|
| 41 |
+
tooltip="Test polygon",
|
| 42 |
+
fillColor='#AAAAAA',
|
| 43 |
+
color='#888888',
|
| 44 |
+
weight=2,
|
| 45 |
+
fillOpacity=0.6,
|
| 46 |
+
opacity=0.8
|
| 47 |
+
).add_to(m)
|
| 48 |
+
|
| 49 |
+
print("β
Polygon creation works")
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"β Polygon creation failed: {e}")
|
| 52 |
+
return False
|
| 53 |
+
|
| 54 |
+
# Test heat map
|
| 55 |
+
try:
|
| 56 |
+
heat_data = [
|
| 57 |
+
[40.0, -105.0, 0.8], # Denver area with intensity
|
| 58 |
+
[39.7, -104.9, 0.9], # Higher intensity nearby
|
| 59 |
+
[39.5, -105.2, 0.6] # Lower intensity
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
HeatMap(
|
| 63 |
+
heat_data,
|
| 64 |
+
min_opacity=0.2,
|
| 65 |
+
radius=15,
|
| 66 |
+
gradient={
|
| 67 |
+
0.0: 'rgba(0,0,0,0)',
|
| 68 |
+
0.5: 'rgba(128,128,128,0.6)',
|
| 69 |
+
1.0: 'rgba(64,64,64,0.9)'
|
| 70 |
+
}
|
| 71 |
+
).add_to(m)
|
| 72 |
+
|
| 73 |
+
print("β
Heat map creation works")
|
| 74 |
+
except Exception as e:
|
| 75 |
+
print(f"β Heat map creation failed: {e}")
|
| 76 |
+
return False
|
| 77 |
+
|
| 78 |
+
# Test layer control
|
| 79 |
+
try:
|
| 80 |
+
folium.LayerControl().add_to(m)
|
| 81 |
+
print("β
Layer control works")
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"β Layer control failed: {e}")
|
| 84 |
+
return False
|
| 85 |
+
|
| 86 |
+
# Test tile layers
|
| 87 |
+
try:
|
| 88 |
+
folium.TileLayer(
|
| 89 |
+
tiles='CartoDB positron',
|
| 90 |
+
name='Clean',
|
| 91 |
+
overlay=False,
|
| 92 |
+
control=True
|
| 93 |
+
).add_to(m)
|
| 94 |
+
print("β
Tile layer creation works")
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"β Tile layer creation failed: {e}")
|
| 97 |
+
return False
|
| 98 |
+
|
| 99 |
+
print("π¬οΈ Folium smoke visualization test completed successfully!")
|
| 100 |
+
print("All core folium features are working properly:")
|
| 101 |
+
print(" β Map creation")
|
| 102 |
+
print(" β Polygon rendering with grayscale styling")
|
| 103 |
+
print(" β Heat map visualization")
|
| 104 |
+
print(" β Layer controls")
|
| 105 |
+
print(" β Multiple tile layer support")
|
| 106 |
+
|
| 107 |
+
return True
|
| 108 |
+
|
| 109 |
+
def test_grayscale_styling():
|
| 110 |
+
"""Test grayscale color scheme for smoke"""
|
| 111 |
+
|
| 112 |
+
grayscale_styles = {
|
| 113 |
+
'light': {
|
| 114 |
+
'fillColor': '#E8E8E8', # Light gray
|
| 115 |
+
'color': '#CCCCCC', # Border
|
| 116 |
+
'weight': 1,
|
| 117 |
+
'fillOpacity': 0.4,
|
| 118 |
+
'opacity': 0.6
|
| 119 |
+
},
|
| 120 |
+
'medium': {
|
| 121 |
+
'fillColor': '#AAAAAA', # Medium gray
|
| 122 |
+
'color': '#888888', # Border
|
| 123 |
+
'weight': 2,
|
| 124 |
+
'fillOpacity': 0.6,
|
| 125 |
+
'opacity': 0.8
|
| 126 |
+
},
|
| 127 |
+
'heavy': {
|
| 128 |
+
'fillColor': '#666666', # Dark gray
|
| 129 |
+
'color': '#444444', # Border
|
| 130 |
+
'weight': 2,
|
| 131 |
+
'fillOpacity': 0.8,
|
| 132 |
+
'opacity': 1.0
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
print("π¨ Grayscale Smoke Styling Test:")
|
| 137 |
+
for category, style in grayscale_styles.items():
|
| 138 |
+
print(f" {category.capitalize():6s}: Fill={style['fillColor']}, Border={style['color']}, Opacity={style['fillOpacity']}")
|
| 139 |
+
|
| 140 |
+
print("β
Grayscale styling configuration validated")
|
| 141 |
+
return True
|
| 142 |
+
|
| 143 |
+
if __name__ == "__main__":
|
| 144 |
+
print("π¬οΈ Testing Folium/Leaflet Smoke Visualization")
|
| 145 |
+
print("=" * 50)
|
| 146 |
+
|
| 147 |
+
folium_works = test_folium_availability()
|
| 148 |
+
grayscale_works = test_grayscale_styling()
|
| 149 |
+
|
| 150 |
+
if folium_works and grayscale_works:
|
| 151 |
+
print("\nπ All tests passed! Folium smoke visualization is ready.")
|
| 152 |
+
else:
|
| 153 |
+
print("\nβ οΈ Some tests failed. Check dependencies or implementation.")
|