|
import gradio as gr |
|
import pandas as pd |
|
import requests |
|
try: |
|
from bs4 import BeautifulSoup |
|
except ImportError: |
|
import subprocess |
|
import sys |
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "beautifulsoup4"]) |
|
from bs4 import BeautifulSoup |
|
import plotly.express as px |
|
import plotly.graph_objects as go |
|
import folium |
|
from folium.plugins import MarkerCluster, HeatMap |
|
import re |
|
import numpy as np |
|
from urllib.parse import urljoin |
|
import time |
|
import json |
|
import os |
|
from geopy.distance import geodesic |
|
from datetime import datetime, timedelta |
|
import warnings |
|
warnings.filterwarnings('ignore') |
|
|
|
|
|
def dms_to_decimal(degrees, minutes, seconds, direction): |
|
decimal = float(degrees) + float(minutes)/60 + float(seconds)/3600 |
|
if direction in ['S', 'W', '-']: |
|
decimal = -decimal |
|
return decimal |
|
|
|
|
|
def parse_dms_coordinates(text): |
|
if not text: |
|
return None, None |
|
|
|
|
|
text = text.replace('**', '').replace('\n', ' ').strip() |
|
|
|
|
|
lat_pattern = r'(\d+)Β°\s*(\d+)\'\s*(\d+\.?\d*)\'?\'\s*(?:Latitude|[NS])' |
|
lon_pattern = r'(-?\d+)Β°\s*(\d+)\'\s*(\d+\.?\d*)\'?\'\s*(?:Longitude|[EW])' |
|
|
|
lat_match = re.search(lat_pattern, text) |
|
lon_match = re.search(lon_pattern, text) |
|
|
|
latitude = None |
|
longitude = None |
|
|
|
if lat_match: |
|
lat_deg, lat_min, lat_sec = lat_match.groups() |
|
|
|
lat_dir = 'N' |
|
if 'S' in text: |
|
lat_dir = 'S' |
|
latitude = dms_to_decimal(lat_deg, lat_min, lat_sec, lat_dir) |
|
|
|
if lon_match: |
|
lon_deg, lon_min, lon_sec = lon_match.groups() |
|
|
|
lon_dir = 'E' |
|
if 'W' in text or '-' in lon_deg: |
|
lon_dir = 'W' |
|
longitude = dms_to_decimal(lon_deg.replace('-', ''), lon_min, lon_sec, lon_dir) |
|
|
|
return latitude, longitude |
|
|
|
|
|
def fetch_firms_data(): |
|
""" |
|
Fetch NASA FIRMS VIIRS active fire data for the last 24 hours |
|
Filters for USA only and returns relevant fire hotspot data |
|
""" |
|
firms_url = "https://firms.modaps.eosdis.nasa.gov/data/active_fire/viirs/csv/J1_VIIRS_C2_Global_24h.csv" |
|
|
|
try: |
|
print("Fetching NASA FIRMS data...") |
|
response = requests.get(firms_url, timeout=60) |
|
response.raise_for_status() |
|
|
|
|
|
from io import StringIO |
|
firms_df = pd.read_csv(StringIO(response.text)) |
|
|
|
print(f"Retrieved {len(firms_df)} global fire hotspots") |
|
|
|
|
|
|
|
usa_firms = firms_df[ |
|
( |
|
|
|
((firms_df['latitude'] >= 24.5) & (firms_df['latitude'] <= 49.0) & |
|
(firms_df['longitude'] >= -125.0) & (firms_df['longitude'] <= -66.0)) | |
|
|
|
((firms_df['latitude'] >= 54.0) & (firms_df['latitude'] <= 72.0) & |
|
(firms_df['longitude'] >= -180.0) & (firms_df['longitude'] <= -130.0)) | |
|
|
|
((firms_df['latitude'] >= 18.0) & (firms_df['latitude'] <= 23.0) & |
|
(firms_df['longitude'] >= -162.0) & (firms_df['longitude'] <= -154.0)) |
|
) |
|
].copy() |
|
|
|
print(f"Filtered to {len(usa_firms)} USA fire hotspots") |
|
|
|
|
|
usa_firms['datetime'] = pd.to_datetime(usa_firms['acq_date'] + ' ' + usa_firms['acq_time'].astype(str).str.zfill(4), |
|
format='%Y-%m-%d %H%M') |
|
|
|
|
|
usa_firms = usa_firms.sort_values('datetime', ascending=False) |
|
|
|
return usa_firms |
|
|
|
except Exception as e: |
|
print(f"Error fetching FIRMS data: {e}") |
|
return pd.DataFrame() |
|
|
|
|
|
def match_firms_to_inciweb(inciweb_df, firms_df, max_distance_km=50): |
|
""" |
|
Match FIRMS hotspots to InciWeb incidents based on geographic proximity |
|
|
|
Args: |
|
inciweb_df: DataFrame with InciWeb incident data (must have latitude/longitude) |
|
firms_df: DataFrame with FIRMS hotspot data |
|
max_distance_km: Maximum distance in km to consider a match |
|
|
|
Returns: |
|
Enhanced inciweb_df with FIRMS data and activity status |
|
""" |
|
if firms_df.empty or inciweb_df.empty: |
|
return inciweb_df |
|
|
|
print(f"Matching {len(firms_df)} FIRMS hotspots to {len(inciweb_df)} InciWeb incidents...") |
|
|
|
|
|
inciweb_df = inciweb_df.copy() |
|
inciweb_df['firms_hotspots'] = 0 |
|
inciweb_df['total_frp'] = 0.0 |
|
inciweb_df['avg_confidence'] = 0.0 |
|
inciweb_df['latest_hotspot'] = None |
|
inciweb_df['is_active'] = False |
|
inciweb_df['hotspot_coords'] = None |
|
inciweb_df['activity_level'] = 'Unknown' |
|
|
|
|
|
incidents_with_coords = inciweb_df[ |
|
(inciweb_df['latitude'].notna()) & (inciweb_df['longitude'].notna()) |
|
].copy() |
|
|
|
print(f"Processing {len(incidents_with_coords)} incidents with coordinates...") |
|
|
|
for idx, incident in incidents_with_coords.iterrows(): |
|
incident_coords = (incident['latitude'], incident['longitude']) |
|
|
|
|
|
hotspot_distances = [] |
|
matched_hotspots = [] |
|
|
|
for _, hotspot in firms_df.iterrows(): |
|
hotspot_coords = (hotspot['latitude'], hotspot['longitude']) |
|
|
|
try: |
|
distance = geodesic(incident_coords, hotspot_coords).kilometers |
|
if distance <= max_distance_km: |
|
hotspot_distances.append(distance) |
|
matched_hotspots.append(hotspot) |
|
except Exception as e: |
|
continue |
|
|
|
if matched_hotspots: |
|
matched_df = pd.DataFrame(matched_hotspots) |
|
|
|
|
|
num_hotspots = len(matched_hotspots) |
|
total_frp = matched_df['frp'].sum() if 'frp' in matched_df.columns else 0 |
|
avg_confidence = matched_df['confidence'].mean() if 'confidence' in matched_df.columns else 0 |
|
latest_hotspot = matched_df['datetime'].max() if 'datetime' in matched_df.columns else None |
|
|
|
|
|
if num_hotspots >= 20 and total_frp >= 100: |
|
activity_level = 'Very High' |
|
elif num_hotspots >= 10 and total_frp >= 50: |
|
activity_level = 'High' |
|
elif num_hotspots >= 5 and total_frp >= 20: |
|
activity_level = 'Medium' |
|
elif num_hotspots >= 1: |
|
activity_level = 'Low' |
|
else: |
|
activity_level = 'Minimal' |
|
|
|
|
|
inciweb_df.at[idx, 'firms_hotspots'] = num_hotspots |
|
inciweb_df.at[idx, 'total_frp'] = total_frp |
|
inciweb_df.at[idx, 'avg_confidence'] = avg_confidence |
|
inciweb_df.at[idx, 'latest_hotspot'] = latest_hotspot |
|
inciweb_df.at[idx, 'is_active'] = True |
|
inciweb_df.at[idx, 'activity_level'] = activity_level |
|
|
|
|
|
hotspot_coords = [(hs['latitude'], hs['longitude'], hs.get('frp', 1)) |
|
for hs in matched_hotspots] |
|
inciweb_df.at[idx, 'hotspot_coords'] = hotspot_coords |
|
|
|
print(f" {incident['name']}: {num_hotspots} hotspots, {total_frp:.1f} FRP, {activity_level} activity") |
|
|
|
|
|
active_count = (inciweb_df['is_active'] == True).sum() |
|
total_with_coords = len(incidents_with_coords) |
|
|
|
print(f"Found {active_count} active incidents out of {total_with_coords} with coordinates") |
|
|
|
return inciweb_df |
|
|
|
|
|
def fetch_inciweb_data(): |
|
base_url = "https://inciweb.wildfire.gov" |
|
accessible_url = urljoin(base_url, "/accessible-view") |
|
|
|
try: |
|
print(f"Fetching data from: {accessible_url}") |
|
response = requests.get(accessible_url, timeout=30) |
|
response.raise_for_status() |
|
except requests.exceptions.RequestException as e: |
|
print(f"Error fetching data from InciWeb: {e}") |
|
return pd.DataFrame() |
|
|
|
soup = BeautifulSoup(response.content, "html.parser") |
|
|
|
incidents = [] |
|
|
|
|
|
incident_links = soup.find_all("a", href=re.compile(r"/incident-information/")) |
|
|
|
for link in incident_links: |
|
try: |
|
incident = {} |
|
|
|
|
|
incident["name"] = link.text.strip() |
|
incident["link"] = urljoin(base_url, link.get("href")) |
|
incident["id"] = link.get("href").split("/")[-1] |
|
|
|
|
|
row = link.parent |
|
if row and row.name == "td": |
|
row_cells = row.parent.find_all("td") |
|
|
|
|
|
if len(row_cells) >= 5: |
|
incident_type_cell = row_cells[1] if len(row_cells) > 1 else None |
|
if incident_type_cell: |
|
incident["type"] = incident_type_cell.text.strip() |
|
|
|
location_cell = row_cells[2] if len(row_cells) > 2 else None |
|
if location_cell: |
|
incident["location"] = location_cell.text.strip() |
|
state_match = re.search(r'([A-Z]{2})', incident["location"]) |
|
if state_match: |
|
incident["state"] = state_match.group(1) |
|
else: |
|
state_parts = incident["location"].split(',') |
|
if state_parts: |
|
incident["state"] = state_parts[0].strip() |
|
else: |
|
incident["state"] = None |
|
|
|
size_cell = row_cells[3] if len(row_cells) > 3 else None |
|
if size_cell: |
|
size_text = size_cell.text.strip() |
|
size_match = re.search(r'(\d+(?:,\d+)*)', size_text) |
|
if size_match: |
|
incident["size"] = int(size_match.group(1).replace(',', '')) |
|
else: |
|
incident["size"] = None |
|
|
|
updated_cell = row_cells[4] if len(row_cells) > 4 else None |
|
if updated_cell: |
|
incident["updated"] = updated_cell.text.strip() |
|
|
|
incidents.append(incident) |
|
except Exception as e: |
|
print(f"Error processing incident: {e}") |
|
continue |
|
|
|
df = pd.DataFrame(incidents) |
|
|
|
|
|
for col in ["size", "type", "location", "state", "updated"]: |
|
if col not in df.columns: |
|
df[col] = None |
|
|
|
df["size"] = pd.to_numeric(df["size"], errors="coerce") |
|
|
|
print(f"Fetched {len(df)} incidents") |
|
return df |
|
|
|
|
|
def get_incident_coordinates_basic(incident_url): |
|
"""Simplified coordinate extraction for demo purposes""" |
|
try: |
|
response = requests.get(incident_url, timeout=15) |
|
response.raise_for_status() |
|
soup = BeautifulSoup(response.content, "html.parser") |
|
|
|
|
|
script_tags = soup.find_all("script") |
|
for script in script_tags: |
|
if not script.string: |
|
continue |
|
|
|
script_text = script.string |
|
|
|
|
|
if "L.map" in script_text or "leaflet" in script_text.lower(): |
|
setview_match = re.search(r'setView\s*\(\s*\[\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*\]', |
|
script_text, re.IGNORECASE) |
|
if setview_match: |
|
return float(setview_match.group(1)), float(setview_match.group(2)) |
|
|
|
|
|
lat_match = re.search(r'(?:lat|latitude)\s*[=:]\s*(-?\d+\.?\d*)', script_text, re.IGNORECASE) |
|
lon_match = re.search(r'(?:lon|lng|longitude)\s*[=:]\s*(-?\d+\.?\d*)', script_text, re.IGNORECASE) |
|
|
|
if lat_match and lon_match: |
|
return float(lat_match.group(1)), float(lon_match.group(1)) |
|
|
|
return None, None |
|
|
|
except Exception as e: |
|
print(f"Error extracting coordinates: {e}") |
|
return None, None |
|
|
|
|
|
def add_coordinates_to_incidents(df, max_incidents=20): |
|
"""Add coordinates to a subset of incidents for demo purposes""" |
|
df = df.copy() |
|
df['latitude'] = None |
|
df['longitude'] = None |
|
|
|
|
|
wildfires = df[df['type'].str.contains('Wildfire', na=False)].head(max_incidents // 2) |
|
others = df[~df['type'].str.contains('Wildfire', na=False)].head(max_incidents // 2) |
|
sample_df = pd.concat([wildfires, others]).head(max_incidents) |
|
|
|
print(f"Getting coordinates for {len(sample_df)} incidents...") |
|
|
|
for idx, row in sample_df.iterrows(): |
|
if pd.notna(row.get("link")): |
|
try: |
|
lat, lon = get_incident_coordinates_basic(row["link"]) |
|
if lat is not None and lon is not None: |
|
df.at[idx, 'latitude'] = lat |
|
df.at[idx, 'longitude'] = lon |
|
print(f" Got coordinates for {row['name']}: {lat:.4f}, {lon:.4f}") |
|
|
|
time.sleep(0.5) |
|
except Exception as e: |
|
print(f" Error getting coordinates for {row['name']}: {e}") |
|
continue |
|
|
|
return df |
|
|
|
|
|
def generate_enhanced_map(df, firms_df): |
|
"""Generate map with both InciWeb incidents and FIRMS hotspots""" |
|
if df.empty: |
|
return "<div style='padding: 20px; text-align: center;'>No data available to generate map.</div>" |
|
|
|
|
|
m = folium.Map(location=[39.8283, -98.5795], zoom_start=4) |
|
|
|
|
|
incident_cluster = MarkerCluster(name="InciWeb Incidents").add_to(m) |
|
|
|
|
|
active_incidents = 0 |
|
inactive_incidents = 0 |
|
|
|
for _, row in df.iterrows(): |
|
if pd.notna(row.get('latitude')) and pd.notna(row.get('longitude')): |
|
lat, lon = row['latitude'], row['longitude'] |
|
|
|
|
|
if row.get('is_active', False): |
|
active_incidents += 1 |
|
activity_level = row.get('activity_level', 'Unknown') |
|
if activity_level == 'Very High': |
|
color = 'red' |
|
icon = 'fire' |
|
elif activity_level == 'High': |
|
color = 'orange' |
|
icon = 'fire' |
|
elif activity_level == 'Medium': |
|
color = 'yellow' |
|
icon = 'fire' |
|
else: |
|
color = 'lightred' |
|
icon = 'fire' |
|
else: |
|
inactive_incidents += 1 |
|
color = 'gray' |
|
icon = 'pause' |
|
|
|
|
|
popup_content = f""" |
|
<div style="width: 300px;"> |
|
<h4>{row.get('name', 'Unknown')}</h4> |
|
<b>Type:</b> {row.get('type', 'N/A')}<br> |
|
<b>Location:</b> {row.get('location', 'N/A')}<br> |
|
<b>Size:</b> {row.get('size', 'N/A')} acres<br> |
|
<b>Last Updated:</b> {row.get('updated', 'N/A')}<br> |
|
|
|
<hr style="margin: 10px 0;"> |
|
<h5>π₯ Fire Activity (NASA FIRMS)</h5> |
|
<b>Status:</b> {'π΄ ACTIVE' if row.get('is_active', False) else 'β« Inactive'}<br> |
|
<b>Activity Level:</b> {row.get('activity_level', 'Unknown')}<br> |
|
<b>Hotspots (24h):</b> {row.get('firms_hotspots', 0)}<br> |
|
<b>Total Fire Power:</b> {row.get('total_frp', 0):.1f} MW<br> |
|
<b>Avg Confidence:</b> {row.get('avg_confidence', 0):.1f}%<br> |
|
|
|
<a href="{row.get('link', '#')}" target="_blank">π More Details</a> |
|
</div> |
|
""" |
|
|
|
folium.Marker( |
|
location=[lat, lon], |
|
popup=folium.Popup(popup_content, max_width=350), |
|
icon=folium.Icon(color=color, icon=icon, prefix='fa') |
|
).add_to(incident_cluster) |
|
|
|
|
|
if row.get('is_active', False) and row.get('hotspot_coords'): |
|
hotspot_coords = row.get('hotspot_coords', []) |
|
if hotspot_coords: |
|
|
|
heat_data = [[coord[0], coord[1], min(coord[2], 100)] for coord in hotspot_coords] |
|
|
|
|
|
for coord in hotspot_coords[:20]: |
|
folium.CircleMarker( |
|
location=[coord[0], coord[1]], |
|
radius=3 + min(coord[2] / 20, 10), |
|
popup=f"π₯ Hotspot<br>FRP: {coord[2]:.1f} MW", |
|
color='red', |
|
fillColor='orange', |
|
fillOpacity=0.7 |
|
).add_to(m) |
|
|
|
|
|
if not firms_df.empty: |
|
heat_data = [[row['latitude'], row['longitude'], min(row.get('frp', 1), 100)] |
|
for _, row in firms_df.iterrows()] |
|
|
|
if heat_data: |
|
HeatMap( |
|
heat_data, |
|
name="Fire Intensity Heatmap", |
|
radius=15, |
|
blur=10, |
|
max_zoom=1, |
|
gradient={0.2: 'blue', 0.4: 'lime', 0.6: 'orange', 1: 'red'} |
|
).add_to(m) |
|
|
|
|
|
legend_html = f''' |
|
<div style="position: fixed; |
|
bottom: 50px; left: 50px; width: 220px; height: 280px; |
|
border:2px solid grey; z-index:9999; font-size:12px; |
|
background-color:white; padding: 10px; |
|
border-radius: 5px; font-family: Arial;"> |
|
<div style="font-weight: bold; margin-bottom: 8px; font-size: 14px;">π₯ Wildfire Activity Status</div> |
|
|
|
<div style="margin-bottom: 8px;"><b>InciWeb Incidents:</b></div> |
|
<div style="display: flex; align-items: center; margin-bottom: 3px;"> |
|
<div style="background-color: red; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div> |
|
<div>Very High Activity</div> |
|
</div> |
|
<div style="display: flex; align-items: center; margin-bottom: 3px;"> |
|
<div style="background-color: orange; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div> |
|
<div>High Activity</div> |
|
</div> |
|
<div style="display: flex; align-items: center; margin-bottom: 3px;"> |
|
<div style="background-color: yellow; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div> |
|
<div>Medium Activity</div> |
|
</div> |
|
<div style="display: flex; align-items: center; margin-bottom: 3px;"> |
|
<div style="background-color: lightcoral; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div> |
|
<div>Low Activity</div> |
|
</div> |
|
<div style="display: flex; align-items: center; margin-bottom: 8px;"> |
|
<div style="background-color: gray; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div> |
|
<div>Inactive/No Data</div> |
|
</div> |
|
|
|
<div style="margin-bottom: 5px;"><b>NASA FIRMS Data:</b></div> |
|
<div style="display: flex; align-items: center; margin-bottom: 3px;"> |
|
<div style="background-color: orange; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div> |
|
<div>Fire Hotspots (24h)</div> |
|
</div> |
|
<div style="margin-bottom: 8px; font-style: italic;">Heat map shows fire intensity</div> |
|
|
|
<div style="font-size: 11px; margin-top: 10px; padding-top: 5px; border-top: 1px solid #ccc;"> |
|
<b>Statistics:</b><br> |
|
π΄ Active: {active_incidents}<br> |
|
β« Inactive: {inactive_incidents}<br> |
|
π‘οΈ Total Hotspots: {len(firms_df) if not firms_df.empty else 0} |
|
</div> |
|
</div> |
|
''' |
|
|
|
|
|
folium.LayerControl().add_to(m) |
|
|
|
|
|
map_html = m._repr_html_() |
|
map_with_legend = map_html.replace('</body>', legend_html + '</body>') |
|
|
|
return map_with_legend |
|
|
|
|
|
def generate_enhanced_visualizations(df, firms_df): |
|
"""Generate enhanced visualizations with FIRMS data integration""" |
|
figures = [] |
|
|
|
if df.empty: |
|
return [px.bar(title="No data available")] |
|
|
|
|
|
if 'is_active' in df.columns: |
|
activity_summary = df['is_active'].value_counts().reset_index() |
|
activity_summary.columns = ['is_active', 'count'] |
|
activity_summary['status'] = activity_summary['is_active'].map({True: 'Active (FIRMS detected)', False: 'Inactive/Unknown'}) |
|
|
|
fig1 = px.pie( |
|
activity_summary, |
|
values='count', |
|
names='status', |
|
title="π₯ Wildfire Activity Status (Based on NASA FIRMS Data)", |
|
color_discrete_map={'Active (FIRMS detected)': 'red', 'Inactive/Unknown': 'gray'} |
|
) |
|
fig1.update_traces(textinfo='label+percent+value') |
|
else: |
|
fig1 = px.bar(title="Activity status data not available") |
|
figures.append(fig1) |
|
|
|
|
|
if 'activity_level' in df.columns and df['activity_level'].notna().any(): |
|
activity_levels = df[df['activity_level'] != 'Unknown']['activity_level'].value_counts().reset_index() |
|
activity_levels.columns = ['activity_level', 'count'] |
|
|
|
|
|
level_order = ['Very High', 'High', 'Medium', 'Low', 'Minimal'] |
|
color_map = {'Very High': 'darkred', 'High': 'red', 'Medium': 'orange', 'Low': 'yellow', 'Minimal': 'lightblue'} |
|
|
|
fig2 = px.bar( |
|
activity_levels, |
|
x='activity_level', |
|
y='count', |
|
title="π Fire Activity Levels (NASA FIRMS Intensity)", |
|
labels={'activity_level': 'Activity Level', 'count': 'Number of Incidents'}, |
|
color='activity_level', |
|
color_discrete_map=color_map, |
|
category_orders={'activity_level': level_order} |
|
) |
|
else: |
|
fig2 = px.bar(title="Activity level data not available") |
|
figures.append(fig2) |
|
|
|
|
|
if 'total_frp' in df.columns and 'size' in df.columns: |
|
active_df = df[(df['is_active'] == True) & (df['total_frp'] > 0) & (df['size'].notna())].copy() |
|
|
|
if not active_df.empty: |
|
fig3 = px.scatter( |
|
active_df, |
|
x='size', |
|
y='total_frp', |
|
size='firms_hotspots', |
|
color='activity_level', |
|
hover_data=['name', 'state', 'firms_hotspots'], |
|
title="π₯ Fire Intensity vs Incident Size (Active Fires Only)", |
|
labels={'size': 'Incident Size (acres)', 'total_frp': 'Total Fire Radiative Power (MW)'}, |
|
color_discrete_map={'Very High': 'darkred', 'High': 'red', 'Medium': 'orange', 'Low': 'yellow'} |
|
) |
|
fig3.update_layout(xaxis_type="log", yaxis_type="log") |
|
else: |
|
fig3 = px.bar(title="No active fires with size and intensity data") |
|
else: |
|
fig3 = px.bar(title="Fire intensity vs size data not available") |
|
figures.append(fig3) |
|
|
|
|
|
if not firms_df.empty and 'datetime' in firms_df.columns: |
|
|
|
firms_df['hour'] = firms_df['datetime'].dt.floor('H') |
|
hourly_detections = firms_df.groupby('hour').size().reset_index(name='detections') |
|
|
|
fig4 = px.line( |
|
hourly_detections, |
|
x='hour', |
|
y='detections', |
|
title="π Fire Hotspot Detections Over Time (Last 24 Hours)", |
|
labels={'hour': 'Time', 'detections': 'Number of Hotspots Detected'} |
|
) |
|
fig4.update_traces(line_color='red') |
|
else: |
|
fig4 = px.bar(title="FIRMS temporal data not available") |
|
figures.append(fig4) |
|
|
|
|
|
if 'state' in df.columns and 'is_active' in df.columns: |
|
state_activity = df.groupby(['state', 'is_active']).size().reset_index(name='count') |
|
state_activity['status'] = state_activity['is_active'].map({True: 'Active', False: 'Inactive'}) |
|
|
|
|
|
top_states = df['state'].value_counts().head(10).index.tolist() |
|
state_activity_filtered = state_activity[state_activity['state'].isin(top_states)] |
|
|
|
if not state_activity_filtered.empty: |
|
fig5 = px.bar( |
|
state_activity_filtered, |
|
x='state', |
|
y='count', |
|
color='status', |
|
title="πΊοΈ Active vs Inactive Incidents by State (Top 10)", |
|
labels={'state': 'State', 'count': 'Number of Incidents'}, |
|
color_discrete_map={'Active': 'red', 'Inactive': 'gray'} |
|
) |
|
else: |
|
fig5 = px.bar(title="State activity data not available") |
|
else: |
|
fig5 = px.bar(title="State activity data not available") |
|
figures.append(fig5) |
|
|
|
return figures |
|
|
|
|
|
def create_enhanced_wildfire_app(): |
|
"""Create the enhanced Gradio application""" |
|
|
|
with gr.Blocks(title="Enhanced InciWeb + NASA FIRMS Wildfire Tracker", theme=gr.themes.Soft()) as app: |
|
gr.Markdown(""" |
|
# π₯ Enhanced Wildfire Tracker |
|
## InciWeb Incidents + NASA FIRMS Real-Time Fire Detection |
|
|
|
This application combines wildfire incident reports from InciWeb with real-time satellite fire detection data from NASA FIRMS to provide: |
|
- **Active fire status** based on satellite hotspot detection |
|
- **Fire intensity metrics** using Fire Radiative Power (FRP) |
|
- **Real-time hotspot mapping** from the last 24 hours |
|
- **Enhanced situational awareness** for wildfire management |
|
""") |
|
|
|
with gr.Row(): |
|
fetch_btn = gr.Button("π Fetch Latest Data (InciWeb + NASA FIRMS)", variant="primary", size="lg") |
|
status_text = gr.Textbox(label="Status", interactive=False, value="Ready to fetch data...") |
|
|
|
with gr.Tabs(): |
|
with gr.TabItem("πΊοΈ Enhanced Map"): |
|
map_display = gr.HTML(label="Interactive Map with Fire Activity") |
|
|
|
with gr.TabItem("π Enhanced Analytics"): |
|
with gr.Row(): |
|
plot_selector = gr.Dropdown( |
|
choices=[ |
|
"Activity Status Overview", |
|
"Fire Activity Levels", |
|
"Intensity vs Size Analysis", |
|
"Hotspot Detection Timeline", |
|
"State Activity Breakdown" |
|
], |
|
label="Select Visualization", |
|
value="Activity Status Overview" |
|
) |
|
plot_display = gr.Plot(label="Enhanced Analytics") |
|
|
|
with gr.TabItem("π Data Tables"): |
|
with gr.Tabs(): |
|
with gr.TabItem("InciWeb Incidents"): |
|
inciweb_table = gr.Dataframe(label="InciWeb Incidents with FIRMS Integration") |
|
with gr.TabItem("NASA FIRMS Hotspots"): |
|
firms_table = gr.Dataframe(label="NASA FIRMS Fire Hotspots (USA, 24h)") |
|
|
|
with gr.TabItem("π Export Data"): |
|
gr.Markdown("### Download Enhanced Dataset") |
|
with gr.Row(): |
|
download_csv = gr.File(label="Download Enhanced CSV") |
|
download_geojson = gr.File(label="Download GeoJSON") |
|
|
|
|
|
app_state = gr.State({}) |
|
|
|
def fetch_and_process_data(): |
|
"""Main data processing function""" |
|
try: |
|
yield "π‘ Fetching InciWeb incident data...", None, None, None, None, None, None |
|
|
|
|
|
inciweb_df = fetch_inciweb_data() |
|
if inciweb_df.empty: |
|
yield "β Failed to fetch InciWeb data", None, None, None, None, None, None |
|
return |
|
|
|
yield f"β
Found {len(inciweb_df)} InciWeb incidents. Getting coordinates...", None, None, None, None, None, None |
|
|
|
|
|
inciweb_df = add_coordinates_to_incidents(inciweb_df, max_incidents=15) |
|
|
|
yield "π°οΈ Fetching NASA FIRMS fire detection data...", None, None, None, None, None, None |
|
|
|
|
|
firms_df = fetch_firms_data() |
|
if firms_df.empty: |
|
yield "β οΈ FIRMS data unavailable, proceeding with InciWeb only", None, None, inciweb_df, firms_df, None, None |
|
return |
|
|
|
yield f"β
Found {len(firms_df)} USA fire hotspots. Matching with incidents...", None, None, None, None, None, None |
|
|
|
|
|
enhanced_df = match_firms_to_inciweb(inciweb_df, firms_df) |
|
|
|
yield "πΊοΈ Generating enhanced map...", None, None, None, None, None, None |
|
|
|
|
|
map_html = generate_enhanced_map(enhanced_df, firms_df) |
|
plots = generate_enhanced_visualizations(enhanced_df, firms_df) |
|
|
|
|
|
csv_data = enhanced_df.to_csv(index=False) |
|
|
|
active_count = (enhanced_df.get('is_active', pd.Series([False])) == True).sum() |
|
total_hotspots = len(firms_df) |
|
|
|
final_status = f"β
Complete! Found {active_count} active fires with {total_hotspots} total hotspots" |
|
|
|
yield (final_status, map_html, plots[0], enhanced_df, firms_df, csv_data, |
|
{"inciweb_df": enhanced_df, "firms_df": firms_df, "plots": plots}) |
|
|
|
except Exception as e: |
|
yield f"β Error: {str(e)}", None, None, None, None, None, None |
|
|
|
def update_plot(plot_name, state_data): |
|
"""Update plot based on selection""" |
|
if not state_data or "plots" not in state_data: |
|
return px.bar(title="No data available") |
|
|
|
plot_options = [ |
|
"Activity Status Overview", |
|
"Fire Activity Levels", |
|
"Intensity vs Size Analysis", |
|
"Hotspot Detection Timeline", |
|
"State Activity Breakdown" |
|
] |
|
|
|
try: |
|
plot_index = plot_options.index(plot_name) |
|
return state_data["plots"][plot_index] |
|
except (ValueError, IndexError): |
|
return state_data["plots"][0] if state_data["plots"] else px.bar(title="Plot not available") |
|
|
|
|
|
fetch_btn.click( |
|
fetch_and_process_data, |
|
outputs=[status_text, map_display, plot_display, inciweb_table, firms_table, download_csv, app_state] |
|
) |
|
|
|
plot_selector.change( |
|
update_plot, |
|
inputs=[plot_selector, app_state], |
|
outputs=[plot_display] |
|
) |
|
|
|
return app |
|
|
|
|
|
if __name__ == "__main__": |
|
app = create_enhanced_wildfire_app() |
|
app.launch(share=True, debug=True) |