Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,773 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import pandas as pd
|
3 |
+
import requests
|
4 |
+
from bs4 import BeautifulSoup
|
5 |
+
import plotly.express as px
|
6 |
+
import plotly.graph_objects as go
|
7 |
+
import folium
|
8 |
+
from folium.plugins import MarkerCluster, HeatMap
|
9 |
+
import re
|
10 |
+
import numpy as np
|
11 |
+
from urllib.parse import urljoin
|
12 |
+
import time
|
13 |
+
import json
|
14 |
+
import os
|
15 |
+
from geopy.distance import geodesic
|
16 |
+
from datetime import datetime, timedelta
|
17 |
+
import warnings
|
18 |
+
warnings.filterwarnings('ignore')
|
19 |
+
|
20 |
+
# Function to convert degrees, minutes, seconds to decimal degrees
|
21 |
+
def dms_to_decimal(degrees, minutes, seconds, direction):
|
22 |
+
decimal = float(degrees) + float(minutes)/60 + float(seconds)/3600
|
23 |
+
if direction in ['S', 'W', '-']:
|
24 |
+
decimal = -decimal
|
25 |
+
return decimal
|
26 |
+
|
27 |
+
# Function to parse DMS coordinates from text
|
28 |
+
def parse_dms_coordinates(text):
|
29 |
+
if not text:
|
30 |
+
return None, None
|
31 |
+
|
32 |
+
# Clean up the text
|
33 |
+
text = text.replace('**', '').replace('\n', ' ').strip()
|
34 |
+
|
35 |
+
# Look for DMS format
|
36 |
+
lat_pattern = r'(\d+)Β°\s*(\d+)\'\s*(\d+\.?\d*)\'?\'\s*(?:Latitude|[NS])'
|
37 |
+
lon_pattern = r'(-?\d+)Β°\s*(\d+)\'\s*(\d+\.?\d*)\'?\'\s*(?:Longitude|[EW])'
|
38 |
+
|
39 |
+
lat_match = re.search(lat_pattern, text)
|
40 |
+
lon_match = re.search(lon_pattern, text)
|
41 |
+
|
42 |
+
latitude = None
|
43 |
+
longitude = None
|
44 |
+
|
45 |
+
if lat_match:
|
46 |
+
lat_deg, lat_min, lat_sec = lat_match.groups()
|
47 |
+
# Determine direction (N positive, S negative)
|
48 |
+
lat_dir = 'N'
|
49 |
+
if 'S' in text:
|
50 |
+
lat_dir = 'S'
|
51 |
+
latitude = dms_to_decimal(lat_deg, lat_min, lat_sec, lat_dir)
|
52 |
+
|
53 |
+
if lon_match:
|
54 |
+
lon_deg, lon_min, lon_sec = lon_match.groups()
|
55 |
+
# Determine direction (E positive, W negative)
|
56 |
+
lon_dir = 'E'
|
57 |
+
if 'W' in text or '-' in lon_deg:
|
58 |
+
lon_dir = 'W'
|
59 |
+
longitude = dms_to_decimal(lon_deg.replace('-', ''), lon_min, lon_sec, lon_dir)
|
60 |
+
|
61 |
+
return latitude, longitude
|
62 |
+
|
63 |
+
# Function to fetch NASA FIRMS data
|
64 |
+
def fetch_firms_data():
|
65 |
+
"""
|
66 |
+
Fetch NASA FIRMS VIIRS active fire data for the last 24 hours
|
67 |
+
Filters for USA only and returns relevant fire hotspot data
|
68 |
+
"""
|
69 |
+
firms_url = "https://firms.modaps.eosdis.nasa.gov/data/active_fire/viirs/csv/J1_VIIRS_C2_Global_24h.csv"
|
70 |
+
|
71 |
+
try:
|
72 |
+
print("Fetching NASA FIRMS data...")
|
73 |
+
response = requests.get(firms_url, timeout=60)
|
74 |
+
response.raise_for_status()
|
75 |
+
|
76 |
+
# Read CSV data
|
77 |
+
from io import StringIO
|
78 |
+
firms_df = pd.read_csv(StringIO(response.text))
|
79 |
+
|
80 |
+
print(f"Retrieved {len(firms_df)} global fire hotspots")
|
81 |
+
|
82 |
+
# Filter for USA coordinates (approximate bounding box)
|
83 |
+
# Continental US, Alaska, Hawaii
|
84 |
+
usa_firms = firms_df[
|
85 |
+
(
|
86 |
+
# Continental US
|
87 |
+
((firms_df['latitude'] >= 24.5) & (firms_df['latitude'] <= 49.0) &
|
88 |
+
(firms_df['longitude'] >= -125.0) & (firms_df['longitude'] <= -66.0)) |
|
89 |
+
# Alaska
|
90 |
+
((firms_df['latitude'] >= 54.0) & (firms_df['latitude'] <= 72.0) &
|
91 |
+
(firms_df['longitude'] >= -180.0) & (firms_df['longitude'] <= -130.0)) |
|
92 |
+
# Hawaii
|
93 |
+
((firms_df['latitude'] >= 18.0) & (firms_df['latitude'] <= 23.0) &
|
94 |
+
(firms_df['longitude'] >= -162.0) & (firms_df['longitude'] <= -154.0))
|
95 |
+
)
|
96 |
+
].copy()
|
97 |
+
|
98 |
+
print(f"Filtered to {len(usa_firms)} USA fire hotspots")
|
99 |
+
|
100 |
+
# Add datetime column for easier processing
|
101 |
+
usa_firms['datetime'] = pd.to_datetime(usa_firms['acq_date'] + ' ' + usa_firms['acq_time'].astype(str).str.zfill(4),
|
102 |
+
format='%Y-%m-%d %H%M')
|
103 |
+
|
104 |
+
# Sort by acquisition time (most recent first)
|
105 |
+
usa_firms = usa_firms.sort_values('datetime', ascending=False)
|
106 |
+
|
107 |
+
return usa_firms
|
108 |
+
|
109 |
+
except Exception as e:
|
110 |
+
print(f"Error fetching FIRMS data: {e}")
|
111 |
+
return pd.DataFrame()
|
112 |
+
|
113 |
+
# Function to match FIRMS hotspots with InciWeb incidents
|
114 |
+
def match_firms_to_inciweb(inciweb_df, firms_df, max_distance_km=50):
|
115 |
+
"""
|
116 |
+
Match FIRMS hotspots to InciWeb incidents based on geographic proximity
|
117 |
+
|
118 |
+
Args:
|
119 |
+
inciweb_df: DataFrame with InciWeb incident data (must have latitude/longitude)
|
120 |
+
firms_df: DataFrame with FIRMS hotspot data
|
121 |
+
max_distance_km: Maximum distance in km to consider a match
|
122 |
+
|
123 |
+
Returns:
|
124 |
+
Enhanced inciweb_df with FIRMS data and activity status
|
125 |
+
"""
|
126 |
+
if firms_df.empty or inciweb_df.empty:
|
127 |
+
return inciweb_df
|
128 |
+
|
129 |
+
print(f"Matching {len(firms_df)} FIRMS hotspots to {len(inciweb_df)} InciWeb incidents...")
|
130 |
+
|
131 |
+
# Initialize new columns
|
132 |
+
inciweb_df = inciweb_df.copy()
|
133 |
+
inciweb_df['firms_hotspots'] = 0
|
134 |
+
inciweb_df['total_frp'] = 0.0 # Fire Radiative Power
|
135 |
+
inciweb_df['avg_confidence'] = 0.0
|
136 |
+
inciweb_df['latest_hotspot'] = None
|
137 |
+
inciweb_df['is_active'] = False
|
138 |
+
inciweb_df['hotspot_coords'] = None
|
139 |
+
inciweb_df['activity_level'] = 'Unknown'
|
140 |
+
|
141 |
+
# Only process incidents that have coordinates
|
142 |
+
incidents_with_coords = inciweb_df[
|
143 |
+
(inciweb_df['latitude'].notna()) & (inciweb_df['longitude'].notna())
|
144 |
+
].copy()
|
145 |
+
|
146 |
+
print(f"Processing {len(incidents_with_coords)} incidents with coordinates...")
|
147 |
+
|
148 |
+
for idx, incident in incidents_with_coords.iterrows():
|
149 |
+
incident_coords = (incident['latitude'], incident['longitude'])
|
150 |
+
|
151 |
+
# Find FIRMS hotspots within the specified distance
|
152 |
+
hotspot_distances = []
|
153 |
+
matched_hotspots = []
|
154 |
+
|
155 |
+
for _, hotspot in firms_df.iterrows():
|
156 |
+
hotspot_coords = (hotspot['latitude'], hotspot['longitude'])
|
157 |
+
|
158 |
+
try:
|
159 |
+
distance = geodesic(incident_coords, hotspot_coords).kilometers
|
160 |
+
if distance <= max_distance_km:
|
161 |
+
hotspot_distances.append(distance)
|
162 |
+
matched_hotspots.append(hotspot)
|
163 |
+
except Exception as e:
|
164 |
+
continue # Skip invalid coordinates
|
165 |
+
|
166 |
+
if matched_hotspots:
|
167 |
+
matched_df = pd.DataFrame(matched_hotspots)
|
168 |
+
|
169 |
+
# Calculate aggregated metrics
|
170 |
+
num_hotspots = len(matched_hotspots)
|
171 |
+
total_frp = matched_df['frp'].sum() if 'frp' in matched_df.columns else 0
|
172 |
+
avg_confidence = matched_df['confidence'].mean() if 'confidence' in matched_df.columns else 0
|
173 |
+
latest_hotspot = matched_df['datetime'].max() if 'datetime' in matched_df.columns else None
|
174 |
+
|
175 |
+
# Determine activity level based on hotspot count and FRP
|
176 |
+
if num_hotspots >= 20 and total_frp >= 100:
|
177 |
+
activity_level = 'Very High'
|
178 |
+
elif num_hotspots >= 10 and total_frp >= 50:
|
179 |
+
activity_level = 'High'
|
180 |
+
elif num_hotspots >= 5 and total_frp >= 20:
|
181 |
+
activity_level = 'Medium'
|
182 |
+
elif num_hotspots >= 1:
|
183 |
+
activity_level = 'Low'
|
184 |
+
else:
|
185 |
+
activity_level = 'Minimal'
|
186 |
+
|
187 |
+
# Update the incident data
|
188 |
+
inciweb_df.at[idx, 'firms_hotspots'] = num_hotspots
|
189 |
+
inciweb_df.at[idx, 'total_frp'] = total_frp
|
190 |
+
inciweb_df.at[idx, 'avg_confidence'] = avg_confidence
|
191 |
+
inciweb_df.at[idx, 'latest_hotspot'] = latest_hotspot
|
192 |
+
inciweb_df.at[idx, 'is_active'] = True
|
193 |
+
inciweb_df.at[idx, 'activity_level'] = activity_level
|
194 |
+
|
195 |
+
# Store hotspot coordinates for visualization
|
196 |
+
hotspot_coords = [(hs['latitude'], hs['longitude'], hs.get('frp', 1))
|
197 |
+
for hs in matched_hotspots]
|
198 |
+
inciweb_df.at[idx, 'hotspot_coords'] = hotspot_coords
|
199 |
+
|
200 |
+
print(f" {incident['name']}: {num_hotspots} hotspots, {total_frp:.1f} FRP, {activity_level} activity")
|
201 |
+
|
202 |
+
# Mark incidents without recent hotspots as potentially inactive
|
203 |
+
active_count = (inciweb_df['is_active'] == True).sum()
|
204 |
+
total_with_coords = len(incidents_with_coords)
|
205 |
+
|
206 |
+
print(f"Found {active_count} active incidents out of {total_with_coords} with coordinates")
|
207 |
+
|
208 |
+
return inciweb_df
|
209 |
+
|
210 |
+
# Function to scrape InciWeb data from the accessible view page
|
211 |
+
def fetch_inciweb_data():
|
212 |
+
base_url = "https://inciweb.wildfire.gov"
|
213 |
+
accessible_url = urljoin(base_url, "/accessible-view")
|
214 |
+
|
215 |
+
try:
|
216 |
+
print(f"Fetching data from: {accessible_url}")
|
217 |
+
response = requests.get(accessible_url, timeout=30)
|
218 |
+
response.raise_for_status()
|
219 |
+
except requests.exceptions.RequestException as e:
|
220 |
+
print(f"Error fetching data from InciWeb: {e}")
|
221 |
+
return pd.DataFrame()
|
222 |
+
|
223 |
+
soup = BeautifulSoup(response.content, "html.parser")
|
224 |
+
|
225 |
+
incidents = []
|
226 |
+
|
227 |
+
# Find all incident links and process them
|
228 |
+
incident_links = soup.find_all("a", href=re.compile(r"/incident-information/"))
|
229 |
+
|
230 |
+
for link in incident_links:
|
231 |
+
try:
|
232 |
+
incident = {}
|
233 |
+
|
234 |
+
# Extract incident name and ID from link
|
235 |
+
incident["name"] = link.text.strip()
|
236 |
+
incident["link"] = urljoin(base_url, link.get("href"))
|
237 |
+
incident["id"] = link.get("href").split("/")[-1]
|
238 |
+
|
239 |
+
# Navigate through the structure to get incident details
|
240 |
+
row = link.parent
|
241 |
+
if row and row.name == "td":
|
242 |
+
row_cells = row.parent.find_all("td")
|
243 |
+
|
244 |
+
# Parse the row cells to extract information
|
245 |
+
if len(row_cells) >= 5:
|
246 |
+
incident_type_cell = row_cells[1] if len(row_cells) > 1 else None
|
247 |
+
if incident_type_cell:
|
248 |
+
incident["type"] = incident_type_cell.text.strip()
|
249 |
+
|
250 |
+
location_cell = row_cells[2] if len(row_cells) > 2 else None
|
251 |
+
if location_cell:
|
252 |
+
incident["location"] = location_cell.text.strip()
|
253 |
+
state_match = re.search(r'([A-Z]{2})', incident["location"])
|
254 |
+
if state_match:
|
255 |
+
incident["state"] = state_match.group(1)
|
256 |
+
else:
|
257 |
+
state_parts = incident["location"].split(',')
|
258 |
+
if state_parts:
|
259 |
+
incident["state"] = state_parts[0].strip()
|
260 |
+
else:
|
261 |
+
incident["state"] = None
|
262 |
+
|
263 |
+
size_cell = row_cells[3] if len(row_cells) > 3 else None
|
264 |
+
if size_cell:
|
265 |
+
size_text = size_cell.text.strip()
|
266 |
+
size_match = re.search(r'(\d+(?:,\d+)*)', size_text)
|
267 |
+
if size_match:
|
268 |
+
incident["size"] = int(size_match.group(1).replace(',', ''))
|
269 |
+
else:
|
270 |
+
incident["size"] = None
|
271 |
+
|
272 |
+
updated_cell = row_cells[4] if len(row_cells) > 4 else None
|
273 |
+
if updated_cell:
|
274 |
+
incident["updated"] = updated_cell.text.strip()
|
275 |
+
|
276 |
+
incidents.append(incident)
|
277 |
+
except Exception as e:
|
278 |
+
print(f"Error processing incident: {e}")
|
279 |
+
continue
|
280 |
+
|
281 |
+
df = pd.DataFrame(incidents)
|
282 |
+
|
283 |
+
# Ensure all expected columns exist
|
284 |
+
for col in ["size", "type", "location", "state", "updated"]:
|
285 |
+
if col not in df.columns:
|
286 |
+
df[col] = None
|
287 |
+
|
288 |
+
df["size"] = pd.to_numeric(df["size"], errors="coerce")
|
289 |
+
|
290 |
+
print(f"Fetched {len(df)} incidents")
|
291 |
+
return df
|
292 |
+
|
293 |
+
# Simplified coordinate extraction function (focusing on key incidents for demo)
|
294 |
+
def get_incident_coordinates_basic(incident_url):
|
295 |
+
"""Simplified coordinate extraction for demo purposes"""
|
296 |
+
try:
|
297 |
+
response = requests.get(incident_url, timeout=15)
|
298 |
+
response.raise_for_status()
|
299 |
+
soup = BeautifulSoup(response.content, "html.parser")
|
300 |
+
|
301 |
+
# Look for script tags with map data
|
302 |
+
script_tags = soup.find_all("script")
|
303 |
+
for script in script_tags:
|
304 |
+
if not script.string:
|
305 |
+
continue
|
306 |
+
|
307 |
+
script_text = script.string
|
308 |
+
|
309 |
+
# Look for map initialization patterns
|
310 |
+
if "L.map" in script_text or "leaflet" in script_text.lower():
|
311 |
+
setview_match = re.search(r'setView\s*\(\s*\[\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*\]',
|
312 |
+
script_text, re.IGNORECASE)
|
313 |
+
if setview_match:
|
314 |
+
return float(setview_match.group(1)), float(setview_match.group(2))
|
315 |
+
|
316 |
+
# Look for direct coordinate assignments
|
317 |
+
lat_match = re.search(r'(?:lat|latitude)\s*[=:]\s*(-?\d+\.?\d*)', script_text, re.IGNORECASE)
|
318 |
+
lon_match = re.search(r'(?:lon|lng|longitude)\s*[=:]\s*(-?\d+\.?\d*)', script_text, re.IGNORECASE)
|
319 |
+
|
320 |
+
if lat_match and lon_match:
|
321 |
+
return float(lat_match.group(1)), float(lon_match.group(1))
|
322 |
+
|
323 |
+
return None, None
|
324 |
+
|
325 |
+
except Exception as e:
|
326 |
+
print(f"Error extracting coordinates: {e}")
|
327 |
+
return None, None
|
328 |
+
|
329 |
+
# Function to get coordinates for a subset of incidents (for demo efficiency)
|
330 |
+
def add_coordinates_to_incidents(df, max_incidents=20):
|
331 |
+
"""Add coordinates to a subset of incidents for demo purposes"""
|
332 |
+
df = df.copy()
|
333 |
+
df['latitude'] = None
|
334 |
+
df['longitude'] = None
|
335 |
+
|
336 |
+
# Focus on wildfires first, then take others
|
337 |
+
wildfires = df[df['type'].str.contains('Wildfire', na=False)].head(max_incidents // 2)
|
338 |
+
others = df[~df['type'].str.contains('Wildfire', na=False)].head(max_incidents // 2)
|
339 |
+
sample_df = pd.concat([wildfires, others]).head(max_incidents)
|
340 |
+
|
341 |
+
print(f"Getting coordinates for {len(sample_df)} incidents...")
|
342 |
+
|
343 |
+
for idx, row in sample_df.iterrows():
|
344 |
+
if pd.notna(row.get("link")):
|
345 |
+
try:
|
346 |
+
lat, lon = get_incident_coordinates_basic(row["link"])
|
347 |
+
if lat is not None and lon is not None:
|
348 |
+
df.at[idx, 'latitude'] = lat
|
349 |
+
df.at[idx, 'longitude'] = lon
|
350 |
+
print(f" Got coordinates for {row['name']}: {lat:.4f}, {lon:.4f}")
|
351 |
+
|
352 |
+
time.sleep(0.5) # Rate limiting
|
353 |
+
except Exception as e:
|
354 |
+
print(f" Error getting coordinates for {row['name']}: {e}")
|
355 |
+
continue
|
356 |
+
|
357 |
+
return df
|
358 |
+
|
359 |
+
# Enhanced map generation with FIRMS data
|
360 |
+
def generate_enhanced_map(df, firms_df):
|
361 |
+
"""Generate map with both InciWeb incidents and FIRMS hotspots"""
|
362 |
+
if df.empty:
|
363 |
+
return "<div style='padding: 20px; text-align: center;'>No data available to generate map.</div>"
|
364 |
+
|
365 |
+
# Create map centered on the US
|
366 |
+
m = folium.Map(location=[39.8283, -98.5795], zoom_start=4)
|
367 |
+
|
368 |
+
# Add incident markers
|
369 |
+
incident_cluster = MarkerCluster(name="InciWeb Incidents").add_to(m)
|
370 |
+
|
371 |
+
# Track statistics
|
372 |
+
active_incidents = 0
|
373 |
+
inactive_incidents = 0
|
374 |
+
|
375 |
+
for _, row in df.iterrows():
|
376 |
+
if pd.notna(row.get('latitude')) and pd.notna(row.get('longitude')):
|
377 |
+
lat, lon = row['latitude'], row['longitude']
|
378 |
+
|
379 |
+
# Determine marker color based on activity and type
|
380 |
+
if row.get('is_active', False):
|
381 |
+
active_incidents += 1
|
382 |
+
activity_level = row.get('activity_level', 'Unknown')
|
383 |
+
if activity_level == 'Very High':
|
384 |
+
color = 'red'
|
385 |
+
icon = 'fire'
|
386 |
+
elif activity_level == 'High':
|
387 |
+
color = 'orange'
|
388 |
+
icon = 'fire'
|
389 |
+
elif activity_level == 'Medium':
|
390 |
+
color = 'yellow'
|
391 |
+
icon = 'fire'
|
392 |
+
else:
|
393 |
+
color = 'lightred'
|
394 |
+
icon = 'fire'
|
395 |
+
else:
|
396 |
+
inactive_incidents += 1
|
397 |
+
color = 'gray'
|
398 |
+
icon = 'pause'
|
399 |
+
|
400 |
+
# Create detailed popup
|
401 |
+
popup_content = f"""
|
402 |
+
<div style="width: 300px;">
|
403 |
+
<h4>{row.get('name', 'Unknown')}</h4>
|
404 |
+
<b>Type:</b> {row.get('type', 'N/A')}<br>
|
405 |
+
<b>Location:</b> {row.get('location', 'N/A')}<br>
|
406 |
+
<b>Size:</b> {row.get('size', 'N/A')} acres<br>
|
407 |
+
<b>Last Updated:</b> {row.get('updated', 'N/A')}<br>
|
408 |
+
|
409 |
+
<hr style="margin: 10px 0;">
|
410 |
+
<h5>π₯ Fire Activity (NASA FIRMS)</h5>
|
411 |
+
<b>Status:</b> {'π΄ ACTIVE' if row.get('is_active', False) else 'β« Inactive'}<br>
|
412 |
+
<b>Activity Level:</b> {row.get('activity_level', 'Unknown')}<br>
|
413 |
+
<b>Hotspots (24h):</b> {row.get('firms_hotspots', 0)}<br>
|
414 |
+
<b>Total Fire Power:</b> {row.get('total_frp', 0):.1f} MW<br>
|
415 |
+
<b>Avg Confidence:</b> {row.get('avg_confidence', 0):.1f}%<br>
|
416 |
+
|
417 |
+
<a href="{row.get('link', '#')}" target="_blank">π More Details</a>
|
418 |
+
</div>
|
419 |
+
"""
|
420 |
+
|
421 |
+
folium.Marker(
|
422 |
+
location=[lat, lon],
|
423 |
+
popup=folium.Popup(popup_content, max_width=350),
|
424 |
+
icon=folium.Icon(color=color, icon=icon, prefix='fa')
|
425 |
+
).add_to(incident_cluster)
|
426 |
+
|
427 |
+
# Add hotspot visualization for active incidents
|
428 |
+
if row.get('is_active', False) and row.get('hotspot_coords'):
|
429 |
+
hotspot_coords = row.get('hotspot_coords', [])
|
430 |
+
if hotspot_coords:
|
431 |
+
# Create heat map data for this incident
|
432 |
+
heat_data = [[coord[0], coord[1], min(coord[2], 100)] for coord in hotspot_coords]
|
433 |
+
|
434 |
+
# Add individual hotspot markers (smaller, less intrusive)
|
435 |
+
for coord in hotspot_coords[:20]: # Limit to 20 hotspots per incident
|
436 |
+
folium.CircleMarker(
|
437 |
+
location=[coord[0], coord[1]],
|
438 |
+
radius=3 + min(coord[2] / 20, 10), # Size based on FRP
|
439 |
+
popup=f"π₯ Hotspot<br>FRP: {coord[2]:.1f} MW",
|
440 |
+
color='red',
|
441 |
+
fillColor='orange',
|
442 |
+
fillOpacity=0.7
|
443 |
+
).add_to(m)
|
444 |
+
|
445 |
+
# Add FIRMS heat map layer for all USA hotspots
|
446 |
+
if not firms_df.empty:
|
447 |
+
heat_data = [[row['latitude'], row['longitude'], min(row.get('frp', 1), 100)]
|
448 |
+
for _, row in firms_df.iterrows()]
|
449 |
+
|
450 |
+
if heat_data:
|
451 |
+
HeatMap(
|
452 |
+
heat_data,
|
453 |
+
name="Fire Intensity Heatmap",
|
454 |
+
radius=15,
|
455 |
+
blur=10,
|
456 |
+
max_zoom=1,
|
457 |
+
gradient={0.2: 'blue', 0.4: 'lime', 0.6: 'orange', 1: 'red'}
|
458 |
+
).add_to(m)
|
459 |
+
|
460 |
+
# Add custom legend
|
461 |
+
legend_html = f'''
|
462 |
+
<div style="position: fixed;
|
463 |
+
bottom: 50px; left: 50px; width: 220px; height: 280px;
|
464 |
+
border:2px solid grey; z-index:9999; font-size:12px;
|
465 |
+
background-color:white; padding: 10px;
|
466 |
+
border-radius: 5px; font-family: Arial;">
|
467 |
+
<div style="font-weight: bold; margin-bottom: 8px; font-size: 14px;">π₯ Wildfire Activity Status</div>
|
468 |
+
|
469 |
+
<div style="margin-bottom: 8px;"><b>InciWeb Incidents:</b></div>
|
470 |
+
<div style="display: flex; align-items: center; margin-bottom: 3px;">
|
471 |
+
<div style="background-color: red; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div>
|
472 |
+
<div>Very High Activity</div>
|
473 |
+
</div>
|
474 |
+
<div style="display: flex; align-items: center; margin-bottom: 3px;">
|
475 |
+
<div style="background-color: orange; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div>
|
476 |
+
<div>High Activity</div>
|
477 |
+
</div>
|
478 |
+
<div style="display: flex; align-items: center; margin-bottom: 3px;">
|
479 |
+
<div style="background-color: yellow; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div>
|
480 |
+
<div>Medium Activity</div>
|
481 |
+
</div>
|
482 |
+
<div style="display: flex; align-items: center; margin-bottom: 3px;">
|
483 |
+
<div style="background-color: lightcoral; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div>
|
484 |
+
<div>Low Activity</div>
|
485 |
+
</div>
|
486 |
+
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
487 |
+
<div style="background-color: gray; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div>
|
488 |
+
<div>Inactive/No Data</div>
|
489 |
+
</div>
|
490 |
+
|
491 |
+
<div style="margin-bottom: 5px;"><b>NASA FIRMS Data:</b></div>
|
492 |
+
<div style="display: flex; align-items: center; margin-bottom: 3px;">
|
493 |
+
<div style="background-color: orange; width: 12px; height: 12px; margin-right: 5px; border-radius: 50%;"></div>
|
494 |
+
<div>Fire Hotspots (24h)</div>
|
495 |
+
</div>
|
496 |
+
<div style="margin-bottom: 8px; font-style: italic;">Heat map shows fire intensity</div>
|
497 |
+
|
498 |
+
<div style="font-size: 11px; margin-top: 10px; padding-top: 5px; border-top: 1px solid #ccc;">
|
499 |
+
<b>Statistics:</b><br>
|
500 |
+
π΄ Active: {active_incidents}<br>
|
501 |
+
β« Inactive: {inactive_incidents}<br>
|
502 |
+
π‘οΈ Total Hotspots: {len(firms_df) if not firms_df.empty else 0}
|
503 |
+
</div>
|
504 |
+
</div>
|
505 |
+
'''
|
506 |
+
|
507 |
+
# Add layer control
|
508 |
+
folium.LayerControl().add_to(m)
|
509 |
+
|
510 |
+
# Get map HTML and add legend
|
511 |
+
map_html = m._repr_html_()
|
512 |
+
map_with_legend = map_html.replace('</body>', legend_html + '</body>')
|
513 |
+
|
514 |
+
return map_with_legend
|
515 |
+
|
516 |
+
# Enhanced visualization functions
|
517 |
+
def generate_enhanced_visualizations(df, firms_df):
|
518 |
+
"""Generate enhanced visualizations with FIRMS data integration"""
|
519 |
+
figures = []
|
520 |
+
|
521 |
+
if df.empty:
|
522 |
+
return [px.bar(title="No data available")]
|
523 |
+
|
524 |
+
# 1. Activity Status Overview
|
525 |
+
if 'is_active' in df.columns:
|
526 |
+
activity_summary = df['is_active'].value_counts().reset_index()
|
527 |
+
activity_summary.columns = ['is_active', 'count']
|
528 |
+
activity_summary['status'] = activity_summary['is_active'].map({True: 'Active (FIRMS detected)', False: 'Inactive/Unknown'})
|
529 |
+
|
530 |
+
fig1 = px.pie(
|
531 |
+
activity_summary,
|
532 |
+
values='count',
|
533 |
+
names='status',
|
534 |
+
title="π₯ Wildfire Activity Status (Based on NASA FIRMS Data)",
|
535 |
+
color_discrete_map={'Active (FIRMS detected)': 'red', 'Inactive/Unknown': 'gray'}
|
536 |
+
)
|
537 |
+
fig1.update_traces(textinfo='label+percent+value')
|
538 |
+
else:
|
539 |
+
fig1 = px.bar(title="Activity status data not available")
|
540 |
+
figures.append(fig1)
|
541 |
+
|
542 |
+
# 2. Activity Level Distribution
|
543 |
+
if 'activity_level' in df.columns and df['activity_level'].notna().any():
|
544 |
+
activity_levels = df[df['activity_level'] != 'Unknown']['activity_level'].value_counts().reset_index()
|
545 |
+
activity_levels.columns = ['activity_level', 'count']
|
546 |
+
|
547 |
+
# Define order and colors
|
548 |
+
level_order = ['Very High', 'High', 'Medium', 'Low', 'Minimal']
|
549 |
+
color_map = {'Very High': 'darkred', 'High': 'red', 'Medium': 'orange', 'Low': 'yellow', 'Minimal': 'lightblue'}
|
550 |
+
|
551 |
+
fig2 = px.bar(
|
552 |
+
activity_levels,
|
553 |
+
x='activity_level',
|
554 |
+
y='count',
|
555 |
+
title="π Fire Activity Levels (NASA FIRMS Intensity)",
|
556 |
+
labels={'activity_level': 'Activity Level', 'count': 'Number of Incidents'},
|
557 |
+
color='activity_level',
|
558 |
+
color_discrete_map=color_map,
|
559 |
+
category_orders={'activity_level': level_order}
|
560 |
+
)
|
561 |
+
else:
|
562 |
+
fig2 = px.bar(title="Activity level data not available")
|
563 |
+
figures.append(fig2)
|
564 |
+
|
565 |
+
# 3. Fire Radiative Power vs Incident Size
|
566 |
+
if 'total_frp' in df.columns and 'size' in df.columns:
|
567 |
+
active_df = df[(df['is_active'] == True) & (df['total_frp'] > 0) & (df['size'].notna())].copy()
|
568 |
+
|
569 |
+
if not active_df.empty:
|
570 |
+
fig3 = px.scatter(
|
571 |
+
active_df,
|
572 |
+
x='size',
|
573 |
+
y='total_frp',
|
574 |
+
size='firms_hotspots',
|
575 |
+
color='activity_level',
|
576 |
+
hover_data=['name', 'state', 'firms_hotspots'],
|
577 |
+
title="π₯ Fire Intensity vs Incident Size (Active Fires Only)",
|
578 |
+
labels={'size': 'Incident Size (acres)', 'total_frp': 'Total Fire Radiative Power (MW)'},
|
579 |
+
color_discrete_map={'Very High': 'darkred', 'High': 'red', 'Medium': 'orange', 'Low': 'yellow'}
|
580 |
+
)
|
581 |
+
fig3.update_layout(xaxis_type="log", yaxis_type="log")
|
582 |
+
else:
|
583 |
+
fig3 = px.bar(title="No active fires with size and intensity data")
|
584 |
+
else:
|
585 |
+
fig3 = px.bar(title="Fire intensity vs size data not available")
|
586 |
+
figures.append(fig3)
|
587 |
+
|
588 |
+
# 4. Hotspot Detection Over Time (if FIRMS data available)
|
589 |
+
if not firms_df.empty and 'datetime' in firms_df.columns:
|
590 |
+
# Group by hour to show detection pattern
|
591 |
+
firms_df['hour'] = firms_df['datetime'].dt.floor('H')
|
592 |
+
hourly_detections = firms_df.groupby('hour').size().reset_index(name='detections')
|
593 |
+
|
594 |
+
fig4 = px.line(
|
595 |
+
hourly_detections,
|
596 |
+
x='hour',
|
597 |
+
y='detections',
|
598 |
+
title="π Fire Hotspot Detections Over Time (Last 24 Hours)",
|
599 |
+
labels={'hour': 'Time', 'detections': 'Number of Hotspots Detected'}
|
600 |
+
)
|
601 |
+
fig4.update_traces(line_color='red')
|
602 |
+
else:
|
603 |
+
fig4 = px.bar(title="FIRMS temporal data not available")
|
604 |
+
figures.append(fig4)
|
605 |
+
|
606 |
+
# 5. State-wise Active vs Inactive Breakdown
|
607 |
+
if 'state' in df.columns and 'is_active' in df.columns:
|
608 |
+
state_activity = df.groupby(['state', 'is_active']).size().reset_index(name='count')
|
609 |
+
state_activity['status'] = state_activity['is_active'].map({True: 'Active', False: 'Inactive'})
|
610 |
+
|
611 |
+
# Get top 10 states by total incidents
|
612 |
+
top_states = df['state'].value_counts().head(10).index.tolist()
|
613 |
+
state_activity_filtered = state_activity[state_activity['state'].isin(top_states)]
|
614 |
+
|
615 |
+
if not state_activity_filtered.empty:
|
616 |
+
fig5 = px.bar(
|
617 |
+
state_activity_filtered,
|
618 |
+
x='state',
|
619 |
+
y='count',
|
620 |
+
color='status',
|
621 |
+
title="πΊοΈ Active vs Inactive Incidents by State (Top 10)",
|
622 |
+
labels={'state': 'State', 'count': 'Number of Incidents'},
|
623 |
+
color_discrete_map={'Active': 'red', 'Inactive': 'gray'}
|
624 |
+
)
|
625 |
+
else:
|
626 |
+
fig5 = px.bar(title="State activity data not available")
|
627 |
+
else:
|
628 |
+
fig5 = px.bar(title="State activity data not available")
|
629 |
+
figures.append(fig5)
|
630 |
+
|
631 |
+
return figures
|
632 |
+
|
633 |
+
# Main application function
|
634 |
+
def create_enhanced_wildfire_app():
|
635 |
+
"""Create the enhanced Gradio application"""
|
636 |
+
|
637 |
+
with gr.Blocks(title="Enhanced InciWeb + NASA FIRMS Wildfire Tracker", theme=gr.themes.Soft()) as app:
|
638 |
+
gr.Markdown("""
|
639 |
+
# π₯ Enhanced Wildfire Tracker
|
640 |
+
## InciWeb Incidents + NASA FIRMS Real-Time Fire Detection
|
641 |
+
|
642 |
+
This application combines wildfire incident reports from InciWeb with real-time satellite fire detection data from NASA FIRMS to provide:
|
643 |
+
- **Active fire status** based on satellite hotspot detection
|
644 |
+
- **Fire intensity metrics** using Fire Radiative Power (FRP)
|
645 |
+
- **Real-time hotspot mapping** from the last 24 hours
|
646 |
+
- **Enhanced situational awareness** for wildfire management
|
647 |
+
""")
|
648 |
+
|
649 |
+
with gr.Row():
|
650 |
+
fetch_btn = gr.Button("π Fetch Latest Data (InciWeb + NASA FIRMS)", variant="primary", size="lg")
|
651 |
+
status_text = gr.Textbox(label="Status", interactive=False, value="Ready to fetch data...")
|
652 |
+
|
653 |
+
with gr.Tabs():
|
654 |
+
with gr.TabItem("πΊοΈ Enhanced Map"):
|
655 |
+
map_display = gr.HTML(label="Interactive Map with Fire Activity")
|
656 |
+
|
657 |
+
with gr.TabItem("π Enhanced Analytics"):
|
658 |
+
with gr.Row():
|
659 |
+
plot_selector = gr.Dropdown(
|
660 |
+
choices=[
|
661 |
+
"Activity Status Overview",
|
662 |
+
"Fire Activity Levels",
|
663 |
+
"Intensity vs Size Analysis",
|
664 |
+
"Hotspot Detection Timeline",
|
665 |
+
"State Activity Breakdown"
|
666 |
+
],
|
667 |
+
label="Select Visualization",
|
668 |
+
value="Activity Status Overview"
|
669 |
+
)
|
670 |
+
plot_display = gr.Plot(label="Enhanced Analytics")
|
671 |
+
|
672 |
+
with gr.TabItem("π Data Tables"):
|
673 |
+
with gr.Tabs():
|
674 |
+
with gr.TabItem("InciWeb Incidents"):
|
675 |
+
inciweb_table = gr.Dataframe(label="InciWeb Incidents with FIRMS Integration")
|
676 |
+
with gr.TabItem("NASA FIRMS Hotspots"):
|
677 |
+
firms_table = gr.Dataframe(label="NASA FIRMS Fire Hotspots (USA, 24h)")
|
678 |
+
|
679 |
+
with gr.TabItem("π Export Data"):
|
680 |
+
gr.Markdown("### Download Enhanced Dataset")
|
681 |
+
with gr.Row():
|
682 |
+
download_csv = gr.File(label="Download Enhanced CSV")
|
683 |
+
download_geojson = gr.File(label="Download GeoJSON")
|
684 |
+
|
685 |
+
# Store data in state
|
686 |
+
app_state = gr.State({})
|
687 |
+
|
688 |
+
def fetch_and_process_data():
|
689 |
+
"""Main data processing function"""
|
690 |
+
try:
|
691 |
+
yield "π‘ Fetching InciWeb incident data...", None, None, None, None, None, None
|
692 |
+
|
693 |
+
# Fetch InciWeb data
|
694 |
+
inciweb_df = fetch_inciweb_data()
|
695 |
+
if inciweb_df.empty:
|
696 |
+
yield "β Failed to fetch InciWeb data", None, None, None, None, None, None
|
697 |
+
return
|
698 |
+
|
699 |
+
yield f"β
Found {len(inciweb_df)} InciWeb incidents. Getting coordinates...", None, None, None, None, None, None
|
700 |
+
|
701 |
+
# Get coordinates for sample incidents
|
702 |
+
inciweb_df = add_coordinates_to_incidents(inciweb_df, max_incidents=15)
|
703 |
+
|
704 |
+
yield "π°οΈ Fetching NASA FIRMS fire detection data...", None, None, None, None, None, None
|
705 |
+
|
706 |
+
# Fetch FIRMS data
|
707 |
+
firms_df = fetch_firms_data()
|
708 |
+
if firms_df.empty:
|
709 |
+
yield "β οΈ FIRMS data unavailable, proceeding with InciWeb only", None, None, inciweb_df, firms_df, None, None
|
710 |
+
return
|
711 |
+
|
712 |
+
yield f"β
Found {len(firms_df)} USA fire hotspots. Matching with incidents...", None, None, None, None, None, None
|
713 |
+
|
714 |
+
# Match FIRMS data to InciWeb incidents
|
715 |
+
enhanced_df = match_firms_to_inciweb(inciweb_df, firms_df)
|
716 |
+
|
717 |
+
yield "πΊοΈ Generating enhanced map...", None, None, None, None, None, None
|
718 |
+
|
719 |
+
# Generate map and visualizations
|
720 |
+
map_html = generate_enhanced_map(enhanced_df, firms_df)
|
721 |
+
plots = generate_enhanced_visualizations(enhanced_df, firms_df)
|
722 |
+
|
723 |
+
# Prepare export data
|
724 |
+
csv_data = enhanced_df.to_csv(index=False)
|
725 |
+
|
726 |
+
active_count = (enhanced_df.get('is_active', pd.Series([False])) == True).sum()
|
727 |
+
total_hotspots = len(firms_df)
|
728 |
+
|
729 |
+
final_status = f"β
Complete! Found {active_count} active fires with {total_hotspots} total hotspots"
|
730 |
+
|
731 |
+
yield (final_status, map_html, plots[0], enhanced_df, firms_df, csv_data,
|
732 |
+
{"inciweb_df": enhanced_df, "firms_df": firms_df, "plots": plots})
|
733 |
+
|
734 |
+
except Exception as e:
|
735 |
+
yield f"β Error: {str(e)}", None, None, None, None, None, None
|
736 |
+
|
737 |
+
def update_plot(plot_name, state_data):
|
738 |
+
"""Update plot based on selection"""
|
739 |
+
if not state_data or "plots" not in state_data:
|
740 |
+
return px.bar(title="No data available")
|
741 |
+
|
742 |
+
plot_options = [
|
743 |
+
"Activity Status Overview",
|
744 |
+
"Fire Activity Levels",
|
745 |
+
"Intensity vs Size Analysis",
|
746 |
+
"Hotspot Detection Timeline",
|
747 |
+
"State Activity Breakdown"
|
748 |
+
]
|
749 |
+
|
750 |
+
try:
|
751 |
+
plot_index = plot_options.index(plot_name)
|
752 |
+
return state_data["plots"][plot_index]
|
753 |
+
except (ValueError, IndexError):
|
754 |
+
return state_data["plots"][0] if state_data["plots"] else px.bar(title="Plot not available")
|
755 |
+
|
756 |
+
# Wire up the interface
|
757 |
+
fetch_btn.click(
|
758 |
+
fetch_and_process_data,
|
759 |
+
outputs=[status_text, map_display, plot_display, inciweb_table, firms_table, download_csv, app_state]
|
760 |
+
)
|
761 |
+
|
762 |
+
plot_selector.change(
|
763 |
+
update_plot,
|
764 |
+
inputs=[plot_selector, app_state],
|
765 |
+
outputs=[plot_display]
|
766 |
+
)
|
767 |
+
|
768 |
+
return app
|
769 |
+
|
770 |
+
# Create and launch the application
|
771 |
+
if __name__ == "__main__":
|
772 |
+
app = create_enhanced_wildfire_app()
|
773 |
+
app.launch(share=True, debug=True)
|