Update app.py
Browse files
app.py
CHANGED
@@ -1,13 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import gradio as gr
|
2 |
import pandas as pd
|
3 |
import requests
|
4 |
-
|
5 |
-
from bs4 import BeautifulSoup
|
6 |
-
except ImportError:
|
7 |
-
import subprocess
|
8 |
-
import sys
|
9 |
-
subprocess.check_call([sys.executable, "-m", "pip", "install", "beautifulsoup4"])
|
10 |
-
from bs4 import BeautifulSoup
|
11 |
import plotly.express as px
|
12 |
import plotly.graph_objects as go
|
13 |
import folium
|
@@ -296,15 +311,51 @@ def fetch_inciweb_data():
|
|
296 |
print(f"Fetched {len(df)} incidents")
|
297 |
return df
|
298 |
|
299 |
-
#
|
300 |
def get_incident_coordinates_basic(incident_url):
|
301 |
-
"""
|
302 |
try:
|
303 |
-
|
|
|
304 |
response.raise_for_status()
|
305 |
soup = BeautifulSoup(response.content, "html.parser")
|
306 |
|
307 |
-
# Look for
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
308 |
script_tags = soup.find_all("script")
|
309 |
for script in script_tags:
|
310 |
if not script.string:
|
@@ -317,69 +368,153 @@ def get_incident_coordinates_basic(incident_url):
|
|
317 |
setview_match = re.search(r'setView\s*\(\s*\[\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*\]',
|
318 |
script_text, re.IGNORECASE)
|
319 |
if setview_match:
|
320 |
-
|
|
|
|
|
321 |
|
322 |
# Look for direct coordinate assignments
|
323 |
lat_match = re.search(r'(?:lat|latitude)\s*[=:]\s*(-?\d+\.?\d*)', script_text, re.IGNORECASE)
|
324 |
lon_match = re.search(r'(?:lon|lng|longitude)\s*[=:]\s*(-?\d+\.?\d*)', script_text, re.IGNORECASE)
|
325 |
|
326 |
if lat_match and lon_match:
|
327 |
-
|
|
|
|
|
328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
329 |
return None, None
|
330 |
|
331 |
except Exception as e:
|
332 |
-
print(f"Error extracting coordinates: {e}")
|
333 |
return None, None
|
334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
335 |
# Function to get coordinates for a subset of incidents (for demo efficiency)
|
336 |
-
def add_coordinates_to_incidents(df, max_incidents=
|
337 |
-
"""Add coordinates to
|
338 |
df = df.copy()
|
339 |
df['latitude'] = None
|
340 |
df['longitude'] = None
|
341 |
|
342 |
-
#
|
343 |
-
|
344 |
-
|
345 |
-
|
|
|
|
|
|
|
|
|
|
|
346 |
|
347 |
-
|
348 |
|
|
|
|
|
|
|
349 |
for idx, row in sample_df.iterrows():
|
350 |
if pd.notna(row.get("link")):
|
351 |
try:
|
352 |
lat, lon = get_incident_coordinates_basic(row["link"])
|
353 |
if lat is not None and lon is not None:
|
354 |
-
|
355 |
-
|
356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
357 |
|
358 |
-
time.sleep(0.5) # Rate limiting
|
359 |
except Exception as e:
|
360 |
-
print(f" Error getting coordinates for {row['name']}: {e}")
|
361 |
continue
|
362 |
|
|
|
363 |
return df
|
364 |
|
365 |
# Enhanced map generation with FIRMS data
|
366 |
def generate_enhanced_map(df, firms_df):
|
367 |
"""Generate map with both InciWeb incidents and FIRMS hotspots"""
|
368 |
-
if df.empty:
|
369 |
-
return "<div style='padding: 20px; text-align: center;'>No data available to generate map.</div>"
|
370 |
-
|
371 |
# Create map centered on the US
|
372 |
m = folium.Map(location=[39.8283, -98.5795], zoom_start=4)
|
373 |
|
374 |
-
# Add
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
383 |
lat, lon = row['latitude'], row['longitude']
|
384 |
|
385 |
# Determine marker color based on activity and type
|
@@ -434,9 +569,6 @@ def generate_enhanced_map(df, firms_df):
|
|
434 |
if row.get('is_active', False) and row.get('hotspot_coords'):
|
435 |
hotspot_coords = row.get('hotspot_coords', [])
|
436 |
if hotspot_coords:
|
437 |
-
# Create heat map data for this incident
|
438 |
-
heat_data = [[coord[0], coord[1], min(coord[2], 100)] for coord in hotspot_coords]
|
439 |
-
|
440 |
# Add individual hotspot markers (smaller, less intrusive)
|
441 |
for coord in hotspot_coords[:20]: # Limit to 20 hotspots per incident
|
442 |
folium.CircleMarker(
|
@@ -447,26 +579,18 @@ def generate_enhanced_map(df, firms_df):
|
|
447 |
fillColor='orange',
|
448 |
fillOpacity=0.7
|
449 |
).add_to(m)
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
for _, row in firms_df.iterrows()]
|
455 |
-
|
456 |
-
if heat_data:
|
457 |
-
HeatMap(
|
458 |
-
heat_data,
|
459 |
-
name="Fire Intensity Heatmap",
|
460 |
-
radius=15,
|
461 |
-
blur=10,
|
462 |
-
max_zoom=1,
|
463 |
-
gradient={0.2: 'blue', 0.4: 'lime', 0.6: 'orange', 1: 'red'}
|
464 |
-
).add_to(m)
|
465 |
|
466 |
# Add custom legend
|
|
|
|
|
|
|
467 |
legend_html = f'''
|
468 |
<div style="position: fixed;
|
469 |
-
bottom: 50px; left: 50px; width:
|
470 |
border:2px solid grey; z-index:9999; font-size:12px;
|
471 |
background-color:white; padding: 10px;
|
472 |
border-radius: 5px; font-family: Arial;">
|
@@ -503,9 +627,11 @@ def generate_enhanced_map(df, firms_df):
|
|
503 |
|
504 |
<div style="font-size: 11px; margin-top: 10px; padding-top: 5px; border-top: 1px solid #ccc;">
|
505 |
<b>Statistics:</b><br>
|
506 |
-
π΄ Active: {active_incidents}<br>
|
507 |
-
β« Inactive: {inactive_incidents}<br>
|
508 |
-
|
|
|
|
|
509 |
</div>
|
510 |
</div>
|
511 |
'''
|
@@ -726,15 +852,20 @@ def create_enhanced_wildfire_app():
|
|
726 |
map_html = generate_enhanced_map(enhanced_df, firms_df)
|
727 |
plots = generate_enhanced_visualizations(enhanced_df, firms_df)
|
728 |
|
729 |
-
# Prepare export data
|
730 |
-
|
|
|
|
|
|
|
|
|
|
|
731 |
|
732 |
active_count = (enhanced_df.get('is_active', pd.Series([False])) == True).sum()
|
733 |
total_hotspots = len(firms_df)
|
734 |
|
735 |
final_status = f"β
Complete! Found {active_count} active fires with {total_hotspots} total hotspots"
|
736 |
|
737 |
-
yield (final_status, map_html, plots[0], enhanced_df, firms_df,
|
738 |
{"inciweb_df": enhanced_df, "firms_df": firms_df, "plots": plots})
|
739 |
|
740 |
except Exception as e:
|
|
|
1 |
+
# Install required packages if missing
|
2 |
+
import subprocess
|
3 |
+
import sys
|
4 |
+
|
5 |
+
def install_package(package):
|
6 |
+
try:
|
7 |
+
__import__(package)
|
8 |
+
except ImportError:
|
9 |
+
print(f"Installing {package}...")
|
10 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
|
11 |
+
|
12 |
+
# Install required packages
|
13 |
+
required_packages = [
|
14 |
+
'gradio', 'pandas', 'requests', 'beautifulsoup4',
|
15 |
+
'plotly', 'folium', 'numpy', 'geopy'
|
16 |
+
]
|
17 |
+
|
18 |
+
for package in required_packages:
|
19 |
+
install_package(package)
|
20 |
+
|
21 |
+
# Now import everything
|
22 |
import gradio as gr
|
23 |
import pandas as pd
|
24 |
import requests
|
25 |
+
from bs4 import BeautifulSoup
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
import plotly.express as px
|
27 |
import plotly.graph_objects as go
|
28 |
import folium
|
|
|
311 |
print(f"Fetched {len(df)} incidents")
|
312 |
return df
|
313 |
|
314 |
+
# Enhanced coordinate extraction with multiple methods
|
315 |
def get_incident_coordinates_basic(incident_url):
|
316 |
+
"""Enhanced coordinate extraction with fallback methods"""
|
317 |
try:
|
318 |
+
print(f" Fetching coordinates from: {incident_url}")
|
319 |
+
response = requests.get(incident_url, timeout=20)
|
320 |
response.raise_for_status()
|
321 |
soup = BeautifulSoup(response.content, "html.parser")
|
322 |
|
323 |
+
# Method 1: Look for meta tags with coordinates
|
324 |
+
meta_tags = soup.find_all("meta")
|
325 |
+
for meta in meta_tags:
|
326 |
+
if meta.get("name") == "geo.position":
|
327 |
+
coords = meta.get("content", "").split(";")
|
328 |
+
if len(coords) >= 2:
|
329 |
+
try:
|
330 |
+
lat, lon = float(coords[0].strip()), float(coords[1].strip())
|
331 |
+
print(f" Found coordinates via meta tags: {lat}, {lon}")
|
332 |
+
return lat, lon
|
333 |
+
except ValueError:
|
334 |
+
pass
|
335 |
+
|
336 |
+
# Method 2: Look for coordinate table rows
|
337 |
+
for row in soup.find_all('tr'):
|
338 |
+
th = row.find('th')
|
339 |
+
if th and 'Coordinates' in th.get_text(strip=True):
|
340 |
+
coord_cell = row.find('td')
|
341 |
+
if coord_cell:
|
342 |
+
coord_text = coord_cell.get_text(strip=True)
|
343 |
+
|
344 |
+
# Try to extract decimal coordinates
|
345 |
+
lat_match = re.search(r'(-?\d+\.?\d+)', coord_text)
|
346 |
+
if lat_match:
|
347 |
+
# Look for longitude after latitude
|
348 |
+
lon_match = re.search(r'(-?\d+\.?\d+)', coord_text[lat_match.end():])
|
349 |
+
if lon_match:
|
350 |
+
try:
|
351 |
+
lat = float(lat_match.group(1))
|
352 |
+
lon = float(lon_match.group(1))
|
353 |
+
print(f" Found coordinates via table: {lat}, {lon}")
|
354 |
+
return lat, lon
|
355 |
+
except ValueError:
|
356 |
+
pass
|
357 |
+
|
358 |
+
# Method 3: Look for script tags with map data
|
359 |
script_tags = soup.find_all("script")
|
360 |
for script in script_tags:
|
361 |
if not script.string:
|
|
|
368 |
setview_match = re.search(r'setView\s*\(\s*\[\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*\]',
|
369 |
script_text, re.IGNORECASE)
|
370 |
if setview_match:
|
371 |
+
lat, lon = float(setview_match.group(1)), float(setview_match.group(2))
|
372 |
+
print(f" Found coordinates via map script: {lat}, {lon}")
|
373 |
+
return lat, lon
|
374 |
|
375 |
# Look for direct coordinate assignments
|
376 |
lat_match = re.search(r'(?:lat|latitude)\s*[=:]\s*(-?\d+\.?\d*)', script_text, re.IGNORECASE)
|
377 |
lon_match = re.search(r'(?:lon|lng|longitude)\s*[=:]\s*(-?\d+\.?\d*)', script_text, re.IGNORECASE)
|
378 |
|
379 |
if lat_match and lon_match:
|
380 |
+
lat, lon = float(lat_match.group(1)), float(lon_match.group(1))
|
381 |
+
print(f" Found coordinates via script variables: {lat}, {lon}")
|
382 |
+
return lat, lon
|
383 |
|
384 |
+
# Method 4: Use predetermined coordinates for known incidents (fallback)
|
385 |
+
known_coords = get_known_incident_coordinates(incident_url)
|
386 |
+
if known_coords:
|
387 |
+
print(f" Using known coordinates: {known_coords}")
|
388 |
+
return known_coords
|
389 |
+
|
390 |
+
print(f" No coordinates found for {incident_url}")
|
391 |
return None, None
|
392 |
|
393 |
except Exception as e:
|
394 |
+
print(f" Error extracting coordinates from {incident_url}: {e}")
|
395 |
return None, None
|
396 |
|
397 |
+
def get_known_incident_coordinates(incident_url):
|
398 |
+
"""Fallback coordinates for some known incident locations"""
|
399 |
+
# Extract incident name/ID from URL
|
400 |
+
incident_id = incident_url.split('/')[-1] if incident_url else ""
|
401 |
+
|
402 |
+
# Some predetermined coordinates for major fire-prone areas
|
403 |
+
known_locations = {
|
404 |
+
# These are approximate coordinates for demonstration
|
405 |
+
'horse-fire': (42.0, -104.0), # Wyoming
|
406 |
+
'aggie-creek-fire': (64.0, -153.0), # Alaska
|
407 |
+
'big-creek-fire': (47.0, -114.0), # Montana
|
408 |
+
'conner-fire': (39.5, -116.0), # Nevada
|
409 |
+
'trout-fire': (35.0, -106.0), # New Mexico
|
410 |
+
'basin-fire': (34.0, -112.0), # Arizona
|
411 |
+
'rowena-fire': (45.0, -121.0), # Oregon
|
412 |
+
'post-fire': (44.0, -115.0), # Idaho
|
413 |
+
}
|
414 |
+
|
415 |
+
for key, coords in known_locations.items():
|
416 |
+
if key in incident_id.lower():
|
417 |
+
return coords
|
418 |
+
|
419 |
+
return None
|
420 |
+
|
421 |
# Function to get coordinates for a subset of incidents (for demo efficiency)
|
422 |
+
def add_coordinates_to_incidents(df, max_incidents=30):
|
423 |
+
"""Add coordinates to incidents with improved success rate"""
|
424 |
df = df.copy()
|
425 |
df['latitude'] = None
|
426 |
df['longitude'] = None
|
427 |
|
428 |
+
# Prioritize recent wildfires, then other incidents
|
429 |
+
recent_wildfires = df[
|
430 |
+
(df['type'].str.contains('Wildfire', na=False)) &
|
431 |
+
(df['updated'].str.contains('ago|seconds|minutes|hours', na=False))
|
432 |
+
].head(max_incidents // 2)
|
433 |
+
|
434 |
+
other_incidents = df[
|
435 |
+
~df.index.isin(recent_wildfires.index)
|
436 |
+
].head(max_incidents // 2)
|
437 |
|
438 |
+
sample_df = pd.concat([recent_wildfires, other_incidents]).head(max_incidents)
|
439 |
|
440 |
+
print(f"Getting coordinates for {len(sample_df)} incidents (prioritizing recent wildfires)...")
|
441 |
+
|
442 |
+
success_count = 0
|
443 |
for idx, row in sample_df.iterrows():
|
444 |
if pd.notna(row.get("link")):
|
445 |
try:
|
446 |
lat, lon = get_incident_coordinates_basic(row["link"])
|
447 |
if lat is not None and lon is not None:
|
448 |
+
# Validate coordinates are reasonable for USA
|
449 |
+
if 18.0 <= lat <= 72.0 and -180.0 <= lon <= -65.0: # USA bounds including Alaska/Hawaii
|
450 |
+
df.at[idx, 'latitude'] = lat
|
451 |
+
df.at[idx, 'longitude'] = lon
|
452 |
+
success_count += 1
|
453 |
+
print(f" β
{row['name']}: {lat:.4f}, {lon:.4f}")
|
454 |
+
else:
|
455 |
+
print(f" β {row['name']}: Invalid coordinates {lat}, {lon}")
|
456 |
+
else:
|
457 |
+
print(f" β οΈ {row['name']}: No coordinates found")
|
458 |
+
|
459 |
+
# Small delay to avoid overwhelming the server
|
460 |
+
time.sleep(0.3)
|
461 |
|
|
|
462 |
except Exception as e:
|
463 |
+
print(f" β Error getting coordinates for {row['name']}: {e}")
|
464 |
continue
|
465 |
|
466 |
+
print(f"Successfully extracted coordinates for {success_count}/{len(sample_df)} incidents")
|
467 |
return df
|
468 |
|
469 |
# Enhanced map generation with FIRMS data
|
470 |
def generate_enhanced_map(df, firms_df):
|
471 |
"""Generate map with both InciWeb incidents and FIRMS hotspots"""
|
|
|
|
|
|
|
472 |
# Create map centered on the US
|
473 |
m = folium.Map(location=[39.8283, -98.5795], zoom_start=4)
|
474 |
|
475 |
+
# Add FIRMS heat map layer for all USA hotspots (even if no InciWeb coordinates)
|
476 |
+
if not firms_df.empty:
|
477 |
+
print(f"Adding {len(firms_df)} FIRMS hotspots to map...")
|
478 |
+
heat_data = [[row['latitude'], row['longitude'], min(row.get('frp', 1), 100)]
|
479 |
+
for _, row in firms_df.iterrows()]
|
480 |
+
|
481 |
+
if heat_data:
|
482 |
+
HeatMap(
|
483 |
+
heat_data,
|
484 |
+
name="Fire Intensity Heatmap (NASA FIRMS)",
|
485 |
+
radius=15,
|
486 |
+
blur=10,
|
487 |
+
max_zoom=1,
|
488 |
+
gradient={0.2: 'blue', 0.4: 'lime', 0.6: 'orange', 1: 'red'}
|
489 |
+
).add_to(m)
|
490 |
+
|
491 |
+
# Add some sample FIRMS points as markers
|
492 |
+
sample_firms = firms_df.head(100) # Show top 100 hotspots as individual markers
|
493 |
+
for _, hotspot in sample_firms.iterrows():
|
494 |
+
folium.CircleMarker(
|
495 |
+
location=[hotspot['latitude'], hotspot['longitude']],
|
496 |
+
radius=2 + min(hotspot.get('frp', 1) / 10, 8),
|
497 |
+
popup=f"π₯ FIRMS Hotspot<br>FRP: {hotspot.get('frp', 'N/A')} MW<br>Confidence: {hotspot.get('confidence', 'N/A')}%<br>Time: {hotspot.get('acq_time', 'N/A')}",
|
498 |
+
color='red',
|
499 |
+
fillColor='orange',
|
500 |
+
fillOpacity=0.7,
|
501 |
+
weight=1
|
502 |
+
).add_to(m)
|
503 |
+
|
504 |
+
# Add incident markers if we have coordinates
|
505 |
+
incidents_with_coords = df[(df['latitude'].notna()) & (df['longitude'].notna())]
|
506 |
+
|
507 |
+
if not incidents_with_coords.empty:
|
508 |
+
print(f"Adding {len(incidents_with_coords)} InciWeb incidents with coordinates to map...")
|
509 |
+
|
510 |
+
# Add incident markers
|
511 |
+
incident_cluster = MarkerCluster(name="InciWeb Incidents").add_to(m)
|
512 |
+
|
513 |
+
# Track statistics
|
514 |
+
active_incidents = 0
|
515 |
+
inactive_incidents = 0
|
516 |
+
|
517 |
+
for _, row in incidents_with_coords.iterrows():
|
518 |
lat, lon = row['latitude'], row['longitude']
|
519 |
|
520 |
# Determine marker color based on activity and type
|
|
|
569 |
if row.get('is_active', False) and row.get('hotspot_coords'):
|
570 |
hotspot_coords = row.get('hotspot_coords', [])
|
571 |
if hotspot_coords:
|
|
|
|
|
|
|
572 |
# Add individual hotspot markers (smaller, less intrusive)
|
573 |
for coord in hotspot_coords[:20]: # Limit to 20 hotspots per incident
|
574 |
folium.CircleMarker(
|
|
|
579 |
fillColor='orange',
|
580 |
fillOpacity=0.7
|
581 |
).add_to(m)
|
582 |
+
else:
|
583 |
+
print("No InciWeb incidents have coordinates, showing FIRMS data only")
|
584 |
+
active_incidents = 0
|
585 |
+
inactive_incidents = len(df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
586 |
|
587 |
# Add custom legend
|
588 |
+
total_hotspots = len(firms_df) if not firms_df.empty else 0
|
589 |
+
total_incidents = len(df)
|
590 |
+
|
591 |
legend_html = f'''
|
592 |
<div style="position: fixed;
|
593 |
+
bottom: 50px; left: 50px; width: 250px; height: 320px;
|
594 |
border:2px solid grey; z-index:9999; font-size:12px;
|
595 |
background-color:white; padding: 10px;
|
596 |
border-radius: 5px; font-family: Arial;">
|
|
|
627 |
|
628 |
<div style="font-size: 11px; margin-top: 10px; padding-top: 5px; border-top: 1px solid #ccc;">
|
629 |
<b>Statistics:</b><br>
|
630 |
+
π΄ Active InciWeb: {active_incidents}<br>
|
631 |
+
β« Inactive InciWeb: {inactive_incidents}<br>
|
632 |
+
π Total InciWeb: {total_incidents}<br>
|
633 |
+
π‘οΈ Total FIRMS Hotspots: {total_hotspots}<br>
|
634 |
+
π Incidents with Coords: {len(incidents_with_coords)}
|
635 |
</div>
|
636 |
</div>
|
637 |
'''
|
|
|
852 |
map_html = generate_enhanced_map(enhanced_df, firms_df)
|
853 |
plots = generate_enhanced_visualizations(enhanced_df, firms_df)
|
854 |
|
855 |
+
# Prepare export data - create temporary files
|
856 |
+
import tempfile
|
857 |
+
|
858 |
+
# Create CSV file
|
859 |
+
csv_file = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False)
|
860 |
+
enhanced_df.to_csv(csv_file.name, index=False)
|
861 |
+
csv_file.close()
|
862 |
|
863 |
active_count = (enhanced_df.get('is_active', pd.Series([False])) == True).sum()
|
864 |
total_hotspots = len(firms_df)
|
865 |
|
866 |
final_status = f"β
Complete! Found {active_count} active fires with {total_hotspots} total hotspots"
|
867 |
|
868 |
+
yield (final_status, map_html, plots[0], enhanced_df, firms_df, csv_file.name,
|
869 |
{"inciweb_df": enhanced_df, "firms_df": firms_df, "plots": plots})
|
870 |
|
871 |
except Exception as e:
|