Spaces:
Sleeping
Sleeping
ok4
Browse files
app.py
CHANGED
@@ -25,43 +25,103 @@ class RainViewerMap:
|
|
25 |
|
26 |
def get_hurricane_data(self):
|
27 |
"""Fetch active hurricane/tropical storm data from NOAA NHC"""
|
|
|
|
|
|
|
|
|
28 |
try:
|
29 |
-
#
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
|
36 |
-
#
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
}
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
'layer_id': layer_id,
|
58 |
-
'geojson': geojson_data
|
59 |
-
})
|
60 |
|
61 |
return active_storms
|
62 |
|
63 |
except Exception as e:
|
64 |
-
|
|
|
|
|
65 |
return []
|
66 |
|
67 |
def get_storm_track_data(self, layer_id):
|
@@ -144,51 +204,94 @@ class RainViewerMap:
|
|
144 |
hurricane_data = self.get_hurricane_data()
|
145 |
|
146 |
for storm_info in hurricane_data:
|
147 |
-
|
148 |
-
layer_id = storm_info['layer_id']
|
149 |
-
geojson_data = storm_info['geojson']
|
150 |
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
|
193 |
def add_storm_tracks(self, folium_map, track_data):
|
194 |
"""Add storm track lines to the map"""
|
@@ -292,8 +395,15 @@ def create_gradio_app():
|
|
292 |
info="Select radar timestamp"
|
293 |
)
|
294 |
|
|
|
|
|
|
|
|
|
|
|
|
|
295 |
update_btn = gr.Button("Update Map", variant="primary")
|
296 |
refresh_times_btn = gr.Button("Refresh Times")
|
|
|
297 |
|
298 |
with gr.Column(scale=2):
|
299 |
map_html = gr.HTML(
|
@@ -312,6 +422,63 @@ def create_gradio_app():
|
|
312 |
new_times = rain_viewer.get_available_times()
|
313 |
return gr.Dropdown(choices=new_times, value=new_times[-1] if new_times else None)
|
314 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
315 |
update_btn.click(
|
316 |
fn=update_map,
|
317 |
inputs=[lat_input, lon_input, zoom_input, show_radar, show_hurricanes, time_dropdown],
|
@@ -323,6 +490,17 @@ def create_gradio_app():
|
|
323 |
outputs=time_dropdown
|
324 |
)
|
325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
326 |
# Auto-update on input changes
|
327 |
for input_component in [lat_input, lon_input, zoom_input, show_radar, show_hurricanes, time_dropdown]:
|
328 |
input_component.change(
|
|
|
25 |
|
26 |
def get_hurricane_data(self):
|
27 |
"""Fetch active hurricane/tropical storm data from NOAA NHC"""
|
28 |
+
active_storms = []
|
29 |
+
error_messages = []
|
30 |
+
|
31 |
+
# Try multiple approaches to get hurricane data
|
32 |
try:
|
33 |
+
# Approach 1: Try the simplified hurricane data endpoint
|
34 |
+
simple_url = "https://www.nhc.noaa.gov/CurrentStorms.json"
|
35 |
+
try:
|
36 |
+
response = requests.get(simple_url, timeout=10)
|
37 |
+
if response.status_code == 200:
|
38 |
+
storms_data = response.json()
|
39 |
+
for storm in storms_data.get('activeStorms', []):
|
40 |
+
active_storms.append({
|
41 |
+
'source': 'NHC CurrentStorms',
|
42 |
+
'data': storm,
|
43 |
+
'type': 'current_storms'
|
44 |
+
})
|
45 |
+
except Exception as e:
|
46 |
+
error_messages.append(f"CurrentStorms.json failed: {e}")
|
47 |
|
48 |
+
# Approach 2: Try NOAA GIS layers for specific storm data
|
49 |
+
if not active_storms:
|
50 |
+
layers_url = f"{self.nhc_api_base}/layers?f=json"
|
51 |
+
try:
|
52 |
+
layers_response = requests.get(layers_url, timeout=15)
|
53 |
+
if layers_response.status_code == 200:
|
54 |
+
layers_data = layers_response.json()
|
55 |
+
|
56 |
+
# Look for Atlantic storms (AT1-AT5) forecast points
|
57 |
+
for layer in layers_data.get('layers', []):
|
58 |
+
layer_name = layer.get('name', '').lower()
|
59 |
+
layer_id = layer.get('id')
|
60 |
+
|
61 |
+
if ('at1' in layer_name or 'at2' in layer_name or 'at3' in layer_name) and 'forecast points' in layer_name:
|
62 |
+
query_url = f"{self.nhc_api_base}/{layer_id}/query"
|
63 |
+
params = {
|
64 |
+
'where': '1=1',
|
65 |
+
'outFields': '*',
|
66 |
+
'f': 'geojson',
|
67 |
+
'returnGeometry': 'true'
|
68 |
+
}
|
69 |
+
|
70 |
+
try:
|
71 |
+
query_response = requests.get(query_url, params=params, timeout=15)
|
72 |
+
if query_response.status_code == 200:
|
73 |
+
geojson_data = query_response.json()
|
74 |
+
if geojson_data.get('features'):
|
75 |
+
active_storms.append({
|
76 |
+
'source': 'NOAA GIS',
|
77 |
+
'layer_name': layer.get('name', ''),
|
78 |
+
'layer_id': layer_id,
|
79 |
+
'geojson': geojson_data,
|
80 |
+
'type': 'gis_layer'
|
81 |
+
})
|
82 |
+
except Exception as e:
|
83 |
+
error_messages.append(f"Layer {layer_id} query failed: {e}")
|
84 |
+
except Exception as e:
|
85 |
+
error_messages.append(f"Layers API failed: {e}")
|
86 |
|
87 |
+
# Approach 3: Add sample data if no real data available (for testing)
|
88 |
+
if not active_storms:
|
89 |
+
sample_storm = {
|
90 |
+
'source': 'Sample Data',
|
91 |
+
'type': 'sample',
|
92 |
+
'geojson': {
|
93 |
+
'type': 'FeatureCollection',
|
94 |
+
'features': [
|
95 |
+
{
|
96 |
+
'type': 'Feature',
|
97 |
+
'geometry': {
|
98 |
+
'type': 'Point',
|
99 |
+
'coordinates': [-80.0, 26.0]
|
100 |
+
},
|
101 |
+
'properties': {
|
102 |
+
'STORMNAME': 'Sample Hurricane',
|
103 |
+
'STORMTYPE': 'Hurricane',
|
104 |
+
'MAXWIND': '75',
|
105 |
+
'MSLP': '980',
|
106 |
+
'FHOUR': 'Current'
|
107 |
+
}
|
108 |
+
}
|
109 |
+
]
|
110 |
}
|
111 |
+
}
|
112 |
+
active_storms.append(sample_storm)
|
113 |
+
error_messages.append("No real storm data available, showing sample data")
|
114 |
+
|
115 |
+
# Store error messages for debugging
|
116 |
+
self.storm_cache['errors'] = error_messages
|
117 |
+
self.storm_cache['last_update'] = datetime.now().isoformat()
|
|
|
|
|
|
|
118 |
|
119 |
return active_storms
|
120 |
|
121 |
except Exception as e:
|
122 |
+
error_messages.append(f"General error fetching hurricane data: {e}")
|
123 |
+
self.storm_cache['errors'] = error_messages
|
124 |
+
print(f"Hurricane data fetch error: {e}")
|
125 |
return []
|
126 |
|
127 |
def get_storm_track_data(self, layer_id):
|
|
|
204 |
hurricane_data = self.get_hurricane_data()
|
205 |
|
206 |
for storm_info in hurricane_data:
|
207 |
+
storm_type = storm_info.get('type', 'unknown')
|
|
|
|
|
208 |
|
209 |
+
if storm_type == 'current_storms':
|
210 |
+
# Handle CurrentStorms.json format
|
211 |
+
storm_data = storm_info['data']
|
212 |
+
self.add_current_storms_markers(folium_map, storm_data)
|
213 |
+
|
214 |
+
elif storm_type == 'gis_layer':
|
215 |
+
# Handle GIS layer format
|
216 |
+
geojson_data = storm_info['geojson']
|
217 |
+
layer_name = storm_info.get('layer_name', 'Unknown Layer')
|
218 |
+
|
219 |
+
for feature in geojson_data.get('features', []):
|
220 |
+
if feature['geometry']['type'] == 'Point':
|
221 |
+
coords = feature['geometry']['coordinates']
|
222 |
+
props = feature.get('properties', {})
|
223 |
+
|
224 |
+
# Extract storm information
|
225 |
+
storm_name = props.get('STORMNAME', props.get('STORMNUM', 'Unknown Storm'))
|
226 |
+
storm_type_detail = props.get('STORMTYPE', 'Unknown')
|
227 |
+
max_wind = props.get('MAXWIND', props.get('INTENSITY', 'N/A'))
|
228 |
+
pressure = props.get('MSLP', 'N/A')
|
229 |
+
forecast_hour = props.get('FHOUR', props.get('TAU', 'Current'))
|
230 |
+
|
231 |
+
# Determine marker color based on storm intensity
|
232 |
+
color = self.get_storm_color(max_wind)
|
233 |
+
|
234 |
+
# Create popup with storm details
|
235 |
+
popup_text = f"""
|
236 |
+
<b>{storm_name}</b><br>
|
237 |
+
Type: {storm_type_detail}<br>
|
238 |
+
Max Wind: {max_wind} kt<br>
|
239 |
+
Pressure: {pressure} mb<br>
|
240 |
+
Forecast Hour: {forecast_hour}<br>
|
241 |
+
Source: {layer_name}
|
242 |
+
"""
|
243 |
+
|
244 |
+
# Add marker
|
245 |
+
folium.CircleMarker(
|
246 |
+
location=[coords[1], coords[0]],
|
247 |
+
radius=8,
|
248 |
+
popup=popup_text,
|
249 |
+
color=color,
|
250 |
+
fill=True,
|
251 |
+
fillColor=color,
|
252 |
+
fillOpacity=0.7,
|
253 |
+
weight=2
|
254 |
+
).add_to(folium_map)
|
255 |
+
|
256 |
+
elif storm_type == 'sample':
|
257 |
+
# Handle sample data
|
258 |
+
geojson_data = storm_info['geojson']
|
259 |
+
for feature in geojson_data.get('features', []):
|
260 |
+
if feature['geometry']['type'] == 'Point':
|
261 |
+
coords = feature['geometry']['coordinates']
|
262 |
+
props = feature.get('properties', {})
|
263 |
+
|
264 |
+
storm_name = props.get('STORMNAME', 'Sample Storm')
|
265 |
+
storm_type_detail = props.get('STORMTYPE', 'Sample')
|
266 |
+
max_wind = props.get('MAXWIND', '75')
|
267 |
+
pressure = props.get('MSLP', '980')
|
268 |
+
|
269 |
+
color = self.get_storm_color(max_wind)
|
270 |
+
|
271 |
+
popup_text = f"""
|
272 |
+
<b>{storm_name} (Sample)</b><br>
|
273 |
+
Type: {storm_type_detail}<br>
|
274 |
+
Max Wind: {max_wind} kt<br>
|
275 |
+
Pressure: {pressure} mb<br>
|
276 |
+
Note: This is sample data for demonstration
|
277 |
+
"""
|
278 |
+
|
279 |
+
folium.CircleMarker(
|
280 |
+
location=[coords[1], coords[0]],
|
281 |
+
radius=10,
|
282 |
+
popup=popup_text,
|
283 |
+
color=color,
|
284 |
+
fill=True,
|
285 |
+
fillColor=color,
|
286 |
+
fillOpacity=0.7,
|
287 |
+
weight=3
|
288 |
+
).add_to(folium_map)
|
289 |
+
|
290 |
+
def add_current_storms_markers(self, folium_map, storm_data):
|
291 |
+
"""Add markers for storms from CurrentStorms.json format"""
|
292 |
+
# This method handles the specific format from NHC CurrentStorms.json
|
293 |
+
# Implementation depends on the actual JSON structure
|
294 |
+
pass
|
295 |
|
296 |
def add_storm_tracks(self, folium_map, track_data):
|
297 |
"""Add storm track lines to the map"""
|
|
|
395 |
info="Select radar timestamp"
|
396 |
)
|
397 |
|
398 |
+
gr.Markdown("### Hurricane Information")
|
399 |
+
storm_list = gr.HTML(
|
400 |
+
value="Loading storm data...",
|
401 |
+
label="Active Storms"
|
402 |
+
)
|
403 |
+
|
404 |
update_btn = gr.Button("Update Map", variant="primary")
|
405 |
refresh_times_btn = gr.Button("Refresh Times")
|
406 |
+
refresh_storms_btn = gr.Button("Refresh Storms")
|
407 |
|
408 |
with gr.Column(scale=2):
|
409 |
map_html = gr.HTML(
|
|
|
422 |
new_times = rain_viewer.get_available_times()
|
423 |
return gr.Dropdown(choices=new_times, value=new_times[-1] if new_times else None)
|
424 |
|
425 |
+
def get_storm_list_html():
|
426 |
+
"""Generate HTML for storm list display"""
|
427 |
+
hurricane_data = rain_viewer.get_hurricane_data()
|
428 |
+
|
429 |
+
if not hurricane_data:
|
430 |
+
return "<p>No active storms currently detected.</p>"
|
431 |
+
|
432 |
+
html_content = "<div style='font-family: Arial, sans-serif;'>"
|
433 |
+
html_content += "<h4>Active Storms:</h4>"
|
434 |
+
|
435 |
+
for i, storm_info in enumerate(hurricane_data):
|
436 |
+
storm_type = storm_info.get('type', 'unknown')
|
437 |
+
source = storm_info.get('source', 'Unknown')
|
438 |
+
|
439 |
+
html_content += f"<div style='margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px;'>"
|
440 |
+
html_content += f"<strong>Storm #{i+1}</strong> - Source: {source}<br>"
|
441 |
+
|
442 |
+
if storm_type == 'gis_layer':
|
443 |
+
layer_name = storm_info.get('layer_name', 'Unknown')
|
444 |
+
geojson = storm_info.get('geojson', {})
|
445 |
+
feature_count = len(geojson.get('features', []))
|
446 |
+
html_content += f"Layer: {layer_name}<br>"
|
447 |
+
html_content += f"Data points: {feature_count}<br>"
|
448 |
+
|
449 |
+
# Show first feature details if available
|
450 |
+
features = geojson.get('features', [])
|
451 |
+
if features:
|
452 |
+
first_feature = features[0]
|
453 |
+
props = first_feature.get('properties', {})
|
454 |
+
storm_name = props.get('STORMNAME', props.get('STORMNUM', 'N/A'))
|
455 |
+
max_wind = props.get('MAXWIND', props.get('INTENSITY', 'N/A'))
|
456 |
+
pressure = props.get('MSLP', 'N/A')
|
457 |
+
|
458 |
+
html_content += f"Name: {storm_name}<br>"
|
459 |
+
html_content += f"Max Wind: {max_wind} kt<br>"
|
460 |
+
html_content += f"Pressure: {pressure} mb<br>"
|
461 |
+
|
462 |
+
elif storm_type == 'sample':
|
463 |
+
html_content += "<em>Sample data for demonstration</em><br>"
|
464 |
+
html_content += "Name: Sample Hurricane<br>"
|
465 |
+
html_content += "Max Wind: 75 kt<br>"
|
466 |
+
html_content += "Pressure: 980 mb<br>"
|
467 |
+
|
468 |
+
html_content += "</div>"
|
469 |
+
|
470 |
+
# Add error information if available
|
471 |
+
errors = rain_viewer.storm_cache.get('errors', [])
|
472 |
+
if errors:
|
473 |
+
html_content += "<div style='margin-top: 15px; padding: 10px; background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px;'>"
|
474 |
+
html_content += "<strong>Debug Info:</strong><br>"
|
475 |
+
for error in errors[-3:]: # Show last 3 errors
|
476 |
+
html_content += f"• {error}<br>"
|
477 |
+
html_content += "</div>"
|
478 |
+
|
479 |
+
html_content += "</div>"
|
480 |
+
return html_content
|
481 |
+
|
482 |
update_btn.click(
|
483 |
fn=update_map,
|
484 |
inputs=[lat_input, lon_input, zoom_input, show_radar, show_hurricanes, time_dropdown],
|
|
|
490 |
outputs=time_dropdown
|
491 |
)
|
492 |
|
493 |
+
refresh_storms_btn.click(
|
494 |
+
fn=get_storm_list_html,
|
495 |
+
outputs=storm_list
|
496 |
+
)
|
497 |
+
|
498 |
+
# Initialize storm list on load
|
499 |
+
demo.load(
|
500 |
+
fn=get_storm_list_html,
|
501 |
+
outputs=storm_list
|
502 |
+
)
|
503 |
+
|
504 |
# Auto-update on input changes
|
505 |
for input_component in [lat_input, lon_input, zoom_input, show_radar, show_hurricanes, time_dropdown]:
|
506 |
input_component.change(
|