nakas commited on
Commit
e95f321
Β·
verified Β·
1 Parent(s): 381fbee

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +773 -0
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)