Spaces:
Sleeping
Sleeping
Update air_quality_map.py
Browse files- air_quality_map.py +294 -165
air_quality_map.py
CHANGED
@@ -6,6 +6,8 @@ from folium.plugins import MarkerCluster
|
|
6 |
import tempfile
|
7 |
import os
|
8 |
import json
|
|
|
|
|
9 |
|
10 |
# Get API credentials from environment variables
|
11 |
EPA_AQS_API_BASE_URL = "https://aqs.epa.gov/data/api"
|
@@ -14,7 +16,6 @@ API_KEY = os.environ.get("EPA_AQS_API_KEY", "") # Get from environment variable
|
|
14 |
|
15 |
class AirQualityApp:
|
16 |
def __init__(self):
|
17 |
-
# [Keep all your existing initialization code the same]
|
18 |
self.states = {
|
19 |
"AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas",
|
20 |
"CA": "California", "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware",
|
@@ -67,45 +68,67 @@ class AirQualityApp:
|
|
67 |
"Very Unhealthy": "#99004c", # Purple
|
68 |
"Hazardous": "#7e0023" # Maroon
|
69 |
}
|
|
|
|
|
|
|
|
|
70 |
|
71 |
-
#
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
"TX": [
|
86 |
-
{"code": "201", "value": "Harris (Houston)"},
|
87 |
-
{"code": "113", "value": "Dallas"},
|
88 |
-
{"code": "029", "value": "Bexar (San Antonio)"},
|
89 |
-
{"code": "453", "value": "Travis (Austin)"}
|
90 |
-
]
|
91 |
-
}
|
92 |
|
93 |
-
#
|
94 |
-
|
95 |
-
{
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
|
103 |
-
#
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
|
110 |
def _generate_mock_aqi_data(self, state_code):
|
111 |
"""Generate mock AQI data for a state"""
|
@@ -117,43 +140,80 @@ class AirQualityApp:
|
|
117 |
# Get numeric state code
|
118 |
numeric_state_code = self.state_code_mapping.get(state_code, "01")
|
119 |
|
120 |
-
#
|
121 |
-
|
122 |
-
# Generate
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
|
130 |
-
# Generate data for each
|
131 |
-
for
|
132 |
-
|
133 |
-
|
|
|
|
|
134 |
|
135 |
-
# Generate
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
|
152 |
return aqi_data
|
153 |
|
154 |
-
# [Keep all your existing methods the same]
|
155 |
def get_monitors(self, state_code, county_code=None, parameter_code=None):
|
156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
# If we don't have API credentials, use mock data
|
158 |
if not EMAIL or not API_KEY:
|
159 |
return self.mock_get_monitors(state_code, county_code, parameter_code)
|
@@ -184,12 +244,7 @@ class AirQualityApp:
|
|
184 |
response = requests.get(endpoint, params=params)
|
185 |
data = response.json()
|
186 |
|
187 |
-
#
|
188 |
-
print(f"API Response Keys: {list(data.keys()) if isinstance(data, dict) else 'Not a dictionary'}")
|
189 |
-
if isinstance(data, dict) and "Header" in data:
|
190 |
-
print(f"Header type: {type(data['Header'])}, content: {data['Header'][:100]}...")
|
191 |
-
|
192 |
-
# Handle the specific response structure we observed
|
193 |
if isinstance(data, dict):
|
194 |
if "Data" in data and isinstance(data["Data"], list):
|
195 |
return data["Data"]
|
@@ -209,7 +264,6 @@ class AirQualityApp:
|
|
209 |
return self.mock_get_monitors(state_code, county_code, parameter_code)
|
210 |
|
211 |
def get_counties(self, state_code):
|
212 |
-
# [Same as original]
|
213 |
"""Fetch counties for a given state"""
|
214 |
# If we don't have API credentials, use mock data
|
215 |
if not EMAIL or not API_KEY:
|
@@ -251,7 +305,6 @@ class AirQualityApp:
|
|
251 |
return []
|
252 |
|
253 |
def get_parameters(self):
|
254 |
-
# [Same as original]
|
255 |
"""Fetch available parameter codes (pollutants)"""
|
256 |
# If we don't have API credentials, use mock data
|
257 |
if not EMAIL or not API_KEY:
|
@@ -294,22 +347,24 @@ class AirQualityApp:
|
|
294 |
|
295 |
def get_latest_aqi(self, state_code, county_code=None, parameter_code=None):
|
296 |
"""Fetch the latest AQI data for monitors"""
|
297 |
-
#
|
298 |
-
if
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
if county_code:
|
305 |
-
aqi_data = [item for item in aqi_data if item.get('county_code') == county_code]
|
306 |
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
|
311 |
-
|
312 |
-
|
|
|
|
|
|
|
|
|
313 |
|
314 |
# Convert state code to numeric format for API
|
315 |
api_state_code = state_code
|
@@ -336,11 +391,6 @@ class AirQualityApp:
|
|
336 |
response = requests.get(endpoint, params=params)
|
337 |
data = response.json()
|
338 |
|
339 |
-
# Add detailed debugging
|
340 |
-
print(f"AQI API Response Keys: {list(data.keys()) if isinstance(data, dict) else 'Not a dictionary'}")
|
341 |
-
if isinstance(data, dict) and "Header" in data:
|
342 |
-
print(f"AQI Header type: {type(data['Header'])}, content: {data['Header'][:100]}...")
|
343 |
-
|
344 |
# Handle the specific response structure we observed
|
345 |
aqi_data = []
|
346 |
if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
|
@@ -355,51 +405,69 @@ class AirQualityApp:
|
|
355 |
print(f"Error fetching AQI data: {e}")
|
356 |
return []
|
357 |
|
358 |
-
def create_map(self,
|
359 |
-
"""Create a map with air quality monitoring stations"""
|
360 |
-
#
|
361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
362 |
|
363 |
-
if not
|
364 |
return {"map": "No monitoring stations found for the selected criteria.", "legend": "", "data": None}
|
365 |
|
366 |
# Convert to DataFrame for easier manipulation
|
367 |
-
df = pd.DataFrame(
|
368 |
|
369 |
-
#
|
370 |
-
if
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
center_lat = df["latitude"].mean()
|
381 |
-
center_lon = df["longitude"].mean()
|
382 |
|
383 |
-
# Create a map with a specific width and height
|
384 |
-
m = folium.Map(location=[center_lat, center_lon], zoom_start=
|
385 |
|
386 |
# Add a marker cluster
|
387 |
marker_cluster = MarkerCluster().add_to(m)
|
388 |
|
389 |
-
# Get
|
390 |
-
|
391 |
-
|
392 |
|
393 |
-
#
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
|
404 |
# Add markers for each monitoring station
|
405 |
for _, row in df.iterrows():
|
@@ -409,7 +477,7 @@ class AirQualityApp:
|
|
409 |
color = "blue"
|
410 |
|
411 |
# Get AQI data for this station if available
|
412 |
-
station_aqi_data =
|
413 |
latest_aqi = None
|
414 |
aqi_category = None
|
415 |
|
@@ -464,14 +532,14 @@ class AirQualityApp:
|
|
464 |
# Create popup content with detailed information
|
465 |
popup_content = f"""
|
466 |
<div style="min-width: 300px;">
|
467 |
-
<h3>{row
|
468 |
<p><strong>Site ID:</strong> {site_id}</p>
|
469 |
<p><strong>Address:</strong> {row.get('address', 'N/A')}</p>
|
470 |
<p><strong>City:</strong> {row.get('city_name', 'N/A')}</p>
|
471 |
<p><strong>County:</strong> {row.get('county_name', 'N/A')}</p>
|
472 |
-
<p><strong>State:</strong> {row.get('state_name', '
|
473 |
-
<p><strong>Parameter:</strong> {row
|
474 |
-
<p><strong>Coordinates:</strong> {row
|
475 |
{aqi_readings_html}
|
476 |
</div>
|
477 |
"""
|
@@ -493,11 +561,10 @@ class AirQualityApp:
|
|
493 |
return {
|
494 |
"map": map_html,
|
495 |
"legend": legend_html,
|
496 |
-
"data":
|
497 |
}
|
498 |
|
499 |
def create_legend_html(self):
|
500 |
-
# [Same as original]
|
501 |
"""Create the HTML for the AQI legend"""
|
502 |
legend_html = """
|
503 |
<div style="padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: white; margin-top: 10px;">
|
@@ -516,7 +583,6 @@ class AirQualityApp:
|
|
516 |
return legend_html
|
517 |
|
518 |
def get_aqi_category(self, aqi_value):
|
519 |
-
# [Same as original]
|
520 |
"""Determine AQI category based on value"""
|
521 |
try:
|
522 |
aqi = int(aqi_value)
|
@@ -535,11 +601,25 @@ class AirQualityApp:
|
|
535 |
except (ValueError, TypeError):
|
536 |
return "Unknown"
|
537 |
|
538 |
-
def format_air_quality_data_table(self, aqi_data):
|
539 |
"""Format air quality data as an HTML table for display"""
|
540 |
if not aqi_data or len(aqi_data) == 0:
|
541 |
return "<p>No air quality data available for the selected criteria.</p>"
|
542 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
543 |
# Sort by date (most recent first) and then by AQI value (highest first)
|
544 |
sorted_data = sorted(aqi_data,
|
545 |
key=lambda x: (x.get('date_local', ''), -int(x.get('aqi', 0)) if x.get('aqi') and str(x.get('aqi')).isdigit() else 0),
|
@@ -562,6 +642,8 @@ class AirQualityApp:
|
|
562 |
<table style="width:100%; border-collapse: collapse;">
|
563 |
<tr style="background-color: #f2f2f2; position: sticky; top: 0;">
|
564 |
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Date</th>
|
|
|
|
|
565 |
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Location</th>
|
566 |
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Pollutant</th>
|
567 |
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">AQI</th>
|
@@ -573,11 +655,18 @@ class AirQualityApp:
|
|
573 |
for i, item in enumerate(site_data.values()):
|
574 |
date = item.get('date_local', 'N/A')
|
575 |
|
576 |
-
#
|
577 |
state_code = item.get('state_code', 'N/A')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
578 |
county_code = item.get('county_code', 'N/A')
|
579 |
site_number = item.get('site_number', 'N/A')
|
580 |
-
location = f"Site {
|
581 |
|
582 |
pollutant = item.get('parameter_name', 'N/A')
|
583 |
aqi_value = item.get('aqi', 'N/A')
|
@@ -590,6 +679,8 @@ class AirQualityApp:
|
|
590 |
html += f"""
|
591 |
<tr{row_style}>
|
592 |
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{date}</td>
|
|
|
|
|
593 |
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{location}</td>
|
594 |
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{pollutant}</td>
|
595 |
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{aqi_value}</td>
|
@@ -604,11 +695,32 @@ class AirQualityApp:
|
|
604 |
|
605 |
return html
|
606 |
|
607 |
-
# [Keep all your mock data methods the same]
|
608 |
def mock_get_counties(self, state_code):
|
609 |
"""Return mock county data for the specified state"""
|
610 |
-
|
611 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
612 |
return [f"{c['code']}: {c['value']}" for c in counties]
|
613 |
else:
|
614 |
# Return generic counties for other states
|
@@ -621,7 +733,17 @@ class AirQualityApp:
|
|
621 |
|
622 |
def mock_get_parameters(self):
|
623 |
"""Return mock parameter data"""
|
624 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
625 |
|
626 |
def mock_get_monitors(self, state_code, county_code=None, parameter_code=None):
|
627 |
"""Mock function to return sample data for development"""
|
@@ -787,8 +909,8 @@ class AirQualityApp:
|
|
787 |
"parameter_code": "88101",
|
788 |
"parameter_name": "PM2.5 - Local Conditions",
|
789 |
"poc": 1,
|
790 |
-
"latitude": 40.0 + float(ord(state_code[0])) / 10,
|
791 |
-
"longitude": -90.0 - float(ord(state_code[
|
792 |
"local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 1",
|
793 |
"address": "123 Main Street",
|
794 |
"city_name": "City 1",
|
@@ -803,8 +925,8 @@ class AirQualityApp:
|
|
803 |
"parameter_code": "44201",
|
804 |
"parameter_name": "Ozone",
|
805 |
"poc": 1,
|
806 |
-
"latitude": 40.5 + float(ord(state_code[0])) / 10,
|
807 |
-
"longitude": -90.5 - float(ord(state_code[
|
808 |
"local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 2",
|
809 |
"address": "456 Oak Street",
|
810 |
"city_name": "City 2",
|
@@ -824,9 +946,9 @@ class AirQualityApp:
|
|
824 |
|
825 |
return monitors
|
826 |
|
827 |
-
# Create the UI with the
|
828 |
def create_air_quality_map_ui():
|
829 |
-
"""Create the Gradio interface for the Air Quality Map application with
|
830 |
app = AirQualityApp()
|
831 |
|
832 |
def update_counties(state_code):
|
@@ -834,7 +956,7 @@ def create_air_quality_map_ui():
|
|
834 |
counties = app.get_counties(state_code)
|
835 |
return gr.Dropdown(choices=counties)
|
836 |
|
837 |
-
def show_map_and_data(state, county=None, parameter=None):
|
838 |
"""Callback to generate and display both the map and the air quality data"""
|
839 |
# Extract code from county string if provided
|
840 |
county_code = None
|
@@ -846,7 +968,7 @@ def create_air_quality_map_ui():
|
|
846 |
if parameter and ":" in parameter:
|
847 |
parameter_code = parameter.split(":")[0].strip()
|
848 |
|
849 |
-
# Generate the map and get data
|
850 |
result = app.create_map(state, county_code, parameter_code)
|
851 |
|
852 |
if isinstance(result, dict):
|
@@ -860,7 +982,7 @@ def create_air_quality_map_ui():
|
|
860 |
|
861 |
# Process air quality data for the separate panel
|
862 |
if result["data"]:
|
863 |
-
data_html = app.format_air_quality_data_table(result["data"])
|
864 |
else:
|
865 |
data_html = "<p>No air quality data available for the selected criteria.</p>"
|
866 |
|
@@ -874,39 +996,39 @@ def create_air_quality_map_ui():
|
|
874 |
with gr.Blocks(title="Air Quality Monitoring Stations") as interface:
|
875 |
gr.Markdown("# NOAA Air Quality Monitoring Stations Map")
|
876 |
gr.Markdown("""
|
877 |
-
This application displays air quality monitoring stations
|
878 |
|
879 |
**Note:** To use the actual EPA AQS API, you need to register for an API key and set
|
880 |
`EPA_AQS_EMAIL` and `EPA_AQS_API_KEY` environment variables in your Hugging Face Space.
|
881 |
|
882 |
-
For demonstration without an API key, the app shows sample data for California (CA), New York (NY), and Texas (TX).
|
883 |
""")
|
884 |
|
885 |
with gr.Row():
|
886 |
with gr.Column(scale=1):
|
887 |
-
# State dropdown with default
|
888 |
state_dropdown = gr.Dropdown(
|
889 |
-
choices=list(app.states.keys()),
|
890 |
-
label="
|
891 |
-
value="
|
892 |
)
|
893 |
|
894 |
-
# County dropdown
|
895 |
county_dropdown = gr.Dropdown(
|
896 |
-
choices=
|
897 |
-
label="
|
898 |
allow_custom_value=True
|
899 |
)
|
900 |
|
901 |
# Parameter dropdown (pollutant type)
|
902 |
parameter_dropdown = gr.Dropdown(
|
903 |
choices=app.mock_get_parameters(),
|
904 |
-
label="
|
905 |
allow_custom_value=True
|
906 |
)
|
907 |
|
908 |
-
# Button to
|
909 |
-
map_button = gr.Button("
|
910 |
|
911 |
# Create two tabs for the map and data
|
912 |
with gr.Tabs() as tabs:
|
@@ -930,6 +1052,13 @@ def create_air_quality_map_ui():
|
|
930 |
inputs=[state_dropdown, county_dropdown, parameter_dropdown],
|
931 |
outputs=[map_html, data_html]
|
932 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
933 |
|
934 |
return interface
|
935 |
|
|
|
6 |
import tempfile
|
7 |
import os
|
8 |
import json
|
9 |
+
import time
|
10 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
11 |
|
12 |
# Get API credentials from environment variables
|
13 |
EPA_AQS_API_BASE_URL = "https://aqs.epa.gov/data/api"
|
|
|
16 |
|
17 |
class AirQualityApp:
|
18 |
def __init__(self):
|
|
|
19 |
self.states = {
|
20 |
"AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas",
|
21 |
"CA": "California", "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware",
|
|
|
68 |
"Very Unhealthy": "#99004c", # Purple
|
69 |
"Hazardous": "#7e0023" # Maroon
|
70 |
}
|
71 |
+
|
72 |
+
# Cache for storing monitored data
|
73 |
+
self.all_monitors_cache = {}
|
74 |
+
self.all_aqi_data_cache = {}
|
75 |
|
76 |
+
# Load data on initialization
|
77 |
+
print("Initializing and loading all monitors data...")
|
78 |
+
self.load_all_monitors()
|
79 |
+
print("Loading AQI data...")
|
80 |
+
self.load_all_aqi_data()
|
81 |
+
print("Initialization complete.")
|
82 |
+
|
83 |
+
def load_all_monitors(self):
|
84 |
+
"""Load monitors data for all states"""
|
85 |
+
# If we don't have API credentials, use mock data
|
86 |
+
if not EMAIL or not API_KEY:
|
87 |
+
for state_code in self.states.keys():
|
88 |
+
self.all_monitors_cache[state_code] = self.mock_get_monitors(state_code)
|
89 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
|
91 |
+
# With API credentials, load data for all states using multithreading
|
92 |
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
93 |
+
future_to_state = {executor.submit(self.get_monitors, state_code): state_code for state_code in self.states.keys()}
|
94 |
+
for future in as_completed(future_to_state):
|
95 |
+
state_code = future_to_state[future]
|
96 |
+
try:
|
97 |
+
result = future.result()
|
98 |
+
self.all_monitors_cache[state_code] = result
|
99 |
+
print(f"Loaded {len(result)} monitors for {state_code}")
|
100 |
+
except Exception as e:
|
101 |
+
print(f"Error loading monitors for {state_code}: {e}")
|
102 |
+
# Fall back to mock data
|
103 |
+
self.all_monitors_cache[state_code] = self.mock_get_monitors(state_code)
|
104 |
+
|
105 |
+
# Sleep briefly to avoid overwhelming the API
|
106 |
+
time.sleep(0.5)
|
107 |
+
|
108 |
+
def load_all_aqi_data(self):
|
109 |
+
"""Load AQI data for all states"""
|
110 |
+
# If we don't have API credentials, use mock data
|
111 |
+
if not EMAIL or not API_KEY:
|
112 |
+
for state_code in self.states.keys():
|
113 |
+
self.all_aqi_data_cache[state_code] = self._generate_mock_aqi_data(state_code)
|
114 |
+
return
|
115 |
|
116 |
+
# With API credentials, load data for all states using multithreading
|
117 |
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
118 |
+
future_to_state = {executor.submit(self.get_latest_aqi, state_code): state_code for state_code in self.states.keys()}
|
119 |
+
for future in as_completed(future_to_state):
|
120 |
+
state_code = future_to_state[future]
|
121 |
+
try:
|
122 |
+
result = future.result()
|
123 |
+
self.all_aqi_data_cache[state_code] = result
|
124 |
+
print(f"Loaded {len(result)} AQI readings for {state_code}")
|
125 |
+
except Exception as e:
|
126 |
+
print(f"Error loading AQI data for {state_code}: {e}")
|
127 |
+
# Fall back to mock data
|
128 |
+
self.all_aqi_data_cache[state_code] = self._generate_mock_aqi_data(state_code)
|
129 |
+
|
130 |
+
# Sleep briefly to avoid overwhelming the API
|
131 |
+
time.sleep(0.5)
|
132 |
|
133 |
def _generate_mock_aqi_data(self, state_code):
|
134 |
"""Generate mock AQI data for a state"""
|
|
|
140 |
# Get numeric state code
|
141 |
numeric_state_code = self.state_code_mapping.get(state_code, "01")
|
142 |
|
143 |
+
# Make mock data for our standard states
|
144 |
+
if state_code in ["CA", "NY", "TX"]:
|
145 |
+
# Generate data for the most recent 7 days
|
146 |
+
for days_ago in range(7):
|
147 |
+
# Generate date
|
148 |
+
date = (datetime.now() - timedelta(days=days_ago)).strftime("%Y-%m-%d")
|
149 |
+
|
150 |
+
# Get monitors for this state from cache
|
151 |
+
monitors = self.all_monitors_cache.get(state_code, self.mock_get_monitors(state_code))
|
152 |
|
153 |
+
# Generate AQI data for each monitor
|
154 |
+
for monitor in monitors:
|
155 |
+
county_code = monitor.get("county_code", "001")
|
156 |
+
site_number = monitor.get("site_number", "0001")
|
157 |
+
parameter_code = monitor.get("parameter_code", "88101")
|
158 |
+
parameter_name = monitor.get("parameter_name", "PM2.5 - Local Conditions")
|
159 |
|
160 |
+
# Generate random AQI value (between 0 and 300)
|
161 |
+
aqi_value = random.randint(0, 300)
|
162 |
+
|
163 |
+
aqi_data.append({
|
164 |
+
"state_code": numeric_state_code,
|
165 |
+
"county_code": county_code,
|
166 |
+
"site_number": site_number,
|
167 |
+
"parameter_code": parameter_code,
|
168 |
+
"parameter_name": parameter_name,
|
169 |
+
"date_local": date,
|
170 |
+
"aqi": aqi_value
|
171 |
+
})
|
172 |
+
else:
|
173 |
+
# For other states, generate minimal data
|
174 |
+
# Current date
|
175 |
+
date = datetime.now().strftime("%Y-%m-%d")
|
176 |
+
|
177 |
+
# Make 2 fake monitors with random AQI values
|
178 |
+
aqi_data.append({
|
179 |
+
"state_code": numeric_state_code,
|
180 |
+
"county_code": "001",
|
181 |
+
"site_number": "0001",
|
182 |
+
"parameter_code": "88101",
|
183 |
+
"parameter_name": "PM2.5 - Local Conditions",
|
184 |
+
"date_local": date,
|
185 |
+
"aqi": random.randint(0, 300)
|
186 |
+
})
|
187 |
+
|
188 |
+
aqi_data.append({
|
189 |
+
"state_code": numeric_state_code,
|
190 |
+
"county_code": "001",
|
191 |
+
"site_number": "0002",
|
192 |
+
"parameter_code": "44201",
|
193 |
+
"parameter_name": "Ozone",
|
194 |
+
"date_local": date,
|
195 |
+
"aqi": random.randint(0, 300)
|
196 |
+
})
|
197 |
|
198 |
return aqi_data
|
199 |
|
|
|
200 |
def get_monitors(self, state_code, county_code=None, parameter_code=None):
|
201 |
+
"""Fetch monitoring stations for a given state and optional county"""
|
202 |
+
# Check cache first
|
203 |
+
if state_code in self.all_monitors_cache:
|
204 |
+
monitors = self.all_monitors_cache[state_code]
|
205 |
+
|
206 |
+
# Filter by county if provided
|
207 |
+
if county_code:
|
208 |
+
monitors = [m for m in monitors if m.get("county_code") == county_code]
|
209 |
+
|
210 |
+
# Filter by parameter if provided
|
211 |
+
if parameter_code:
|
212 |
+
monitors = [m for m in monitors if m.get("parameter_code") == parameter_code]
|
213 |
+
|
214 |
+
return monitors
|
215 |
+
|
216 |
+
# If not in cache, fetch from API
|
217 |
# If we don't have API credentials, use mock data
|
218 |
if not EMAIL or not API_KEY:
|
219 |
return self.mock_get_monitors(state_code, county_code, parameter_code)
|
|
|
244 |
response = requests.get(endpoint, params=params)
|
245 |
data = response.json()
|
246 |
|
247 |
+
# Handle the specific response structure
|
|
|
|
|
|
|
|
|
|
|
248 |
if isinstance(data, dict):
|
249 |
if "Data" in data and isinstance(data["Data"], list):
|
250 |
return data["Data"]
|
|
|
264 |
return self.mock_get_monitors(state_code, county_code, parameter_code)
|
265 |
|
266 |
def get_counties(self, state_code):
|
|
|
267 |
"""Fetch counties for a given state"""
|
268 |
# If we don't have API credentials, use mock data
|
269 |
if not EMAIL or not API_KEY:
|
|
|
305 |
return []
|
306 |
|
307 |
def get_parameters(self):
|
|
|
308 |
"""Fetch available parameter codes (pollutants)"""
|
309 |
# If we don't have API credentials, use mock data
|
310 |
if not EMAIL or not API_KEY:
|
|
|
347 |
|
348 |
def get_latest_aqi(self, state_code, county_code=None, parameter_code=None):
|
349 |
"""Fetch the latest AQI data for monitors"""
|
350 |
+
# Check cache first
|
351 |
+
if state_code in self.all_aqi_data_cache:
|
352 |
+
aqi_data = self.all_aqi_data_cache[state_code]
|
353 |
+
|
354 |
+
# Filter by county if provided
|
355 |
+
if county_code:
|
356 |
+
aqi_data = [item for item in aqi_data if item.get('county_code') == county_code]
|
|
|
|
|
357 |
|
358 |
+
# Filter by parameter if provided
|
359 |
+
if parameter_code:
|
360 |
+
aqi_data = [item for item in aqi_data if item.get('parameter_code') == parameter_code]
|
361 |
|
362 |
+
return aqi_data
|
363 |
+
|
364 |
+
# If not in cache, fetch from API
|
365 |
+
# If we don't have API credentials, use mock data
|
366 |
+
if not EMAIL or not API_KEY:
|
367 |
+
return self._generate_mock_aqi_data(state_code)
|
368 |
|
369 |
# Convert state code to numeric format for API
|
370 |
api_state_code = state_code
|
|
|
391 |
response = requests.get(endpoint, params=params)
|
392 |
data = response.json()
|
393 |
|
|
|
|
|
|
|
|
|
|
|
394 |
# Handle the specific response structure we observed
|
395 |
aqi_data = []
|
396 |
if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
|
|
|
405 |
print(f"Error fetching AQI data: {e}")
|
406 |
return []
|
407 |
|
408 |
+
def create_map(self, focus_state=None, county_code=None, parameter_code=None):
|
409 |
+
"""Create a map with air quality monitoring stations for all states"""
|
410 |
+
# Get all monitors - either focused on a state or all states
|
411 |
+
all_monitors = []
|
412 |
+
|
413 |
+
if focus_state:
|
414 |
+
# Get monitors just for the focused state
|
415 |
+
monitors = self.get_monitors(focus_state, county_code, parameter_code)
|
416 |
+
if monitors:
|
417 |
+
all_monitors.extend(monitors)
|
418 |
+
else:
|
419 |
+
# Get all monitors from all states
|
420 |
+
for state_code in self.states.keys():
|
421 |
+
monitors = self.get_monitors(state_code)
|
422 |
+
if monitors:
|
423 |
+
all_monitors.extend(monitors)
|
424 |
|
425 |
+
if not all_monitors:
|
426 |
return {"map": "No monitoring stations found for the selected criteria.", "legend": "", "data": None}
|
427 |
|
428 |
# Convert to DataFrame for easier manipulation
|
429 |
+
df = pd.DataFrame(all_monitors)
|
430 |
|
431 |
+
# Create a map centered on the continental US
|
432 |
+
if focus_state:
|
433 |
+
# Center on the focused state
|
434 |
+
center_lat = df["latitude"].mean()
|
435 |
+
center_lon = df["longitude"].mean()
|
436 |
+
zoom_start = 7
|
437 |
+
else:
|
438 |
+
# Center on continental US
|
439 |
+
center_lat = 39.8283
|
440 |
+
center_lon = -98.5795
|
441 |
+
zoom_start = 4
|
|
|
|
|
442 |
|
443 |
+
# Create a map with a specific width and height
|
444 |
+
m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom_start, width='100%', height=700)
|
445 |
|
446 |
# Add a marker cluster
|
447 |
marker_cluster = MarkerCluster().add_to(m)
|
448 |
|
449 |
+
# Get all AQI data
|
450 |
+
all_aqi_data = []
|
451 |
+
aqi_data_by_site = {}
|
452 |
|
453 |
+
# Process AQI data for each state
|
454 |
+
for state_code in self.states.keys():
|
455 |
+
# Skip states we don't need if focusing on a specific state
|
456 |
+
if focus_state and state_code != focus_state:
|
457 |
+
continue
|
458 |
+
|
459 |
+
# Get AQI data for this state
|
460 |
+
state_aqi_data = self.get_latest_aqi(state_code, county_code, parameter_code)
|
461 |
+
|
462 |
+
if state_aqi_data:
|
463 |
+
all_aqi_data.extend(state_aqi_data)
|
464 |
+
|
465 |
+
# Create a lookup dictionary by site ID
|
466 |
+
for item in state_aqi_data:
|
467 |
+
site_id = f"{item['state_code']}-{item['county_code']}-{item['site_number']}"
|
468 |
+
if site_id not in aqi_data_by_site:
|
469 |
+
aqi_data_by_site[site_id] = []
|
470 |
+
aqi_data_by_site[site_id].append(item)
|
471 |
|
472 |
# Add markers for each monitoring station
|
473 |
for _, row in df.iterrows():
|
|
|
477 |
color = "blue"
|
478 |
|
479 |
# Get AQI data for this station if available
|
480 |
+
station_aqi_data = aqi_data_by_site.get(site_id, [])
|
481 |
latest_aqi = None
|
482 |
aqi_category = None
|
483 |
|
|
|
532 |
# Create popup content with detailed information
|
533 |
popup_content = f"""
|
534 |
<div style="min-width: 300px;">
|
535 |
+
<h3>{row.get('local_site_name', 'Monitoring Station')}</h3>
|
536 |
<p><strong>Site ID:</strong> {site_id}</p>
|
537 |
<p><strong>Address:</strong> {row.get('address', 'N/A')}</p>
|
538 |
<p><strong>City:</strong> {row.get('city_name', 'N/A')}</p>
|
539 |
<p><strong>County:</strong> {row.get('county_name', 'N/A')}</p>
|
540 |
+
<p><strong>State:</strong> {row.get('state_name', self.states.get(row.get('state_code', ''), 'Unknown'))}</p>
|
541 |
+
<p><strong>Parameter:</strong> {row.get('parameter_name', 'N/A')}</p>
|
542 |
+
<p><strong>Coordinates:</strong> {row.get('latitude', 'N/A')}, {row.get('longitude', 'N/A')}</p>
|
543 |
{aqi_readings_html}
|
544 |
</div>
|
545 |
"""
|
|
|
561 |
return {
|
562 |
"map": map_html,
|
563 |
"legend": legend_html,
|
564 |
+
"data": all_aqi_data
|
565 |
}
|
566 |
|
567 |
def create_legend_html(self):
|
|
|
568 |
"""Create the HTML for the AQI legend"""
|
569 |
legend_html = """
|
570 |
<div style="padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: white; margin-top: 10px;">
|
|
|
583 |
return legend_html
|
584 |
|
585 |
def get_aqi_category(self, aqi_value):
|
|
|
586 |
"""Determine AQI category based on value"""
|
587 |
try:
|
588 |
aqi = int(aqi_value)
|
|
|
601 |
except (ValueError, TypeError):
|
602 |
return "Unknown"
|
603 |
|
604 |
+
def format_air_quality_data_table(self, aqi_data, state_filter=None, county_filter=None):
|
605 |
"""Format air quality data as an HTML table for display"""
|
606 |
if not aqi_data or len(aqi_data) == 0:
|
607 |
return "<p>No air quality data available for the selected criteria.</p>"
|
608 |
|
609 |
+
# Filter by state if provided
|
610 |
+
if state_filter:
|
611 |
+
# Convert state code if needed
|
612 |
+
if len(state_filter) == 2:
|
613 |
+
state_filter = self.state_code_mapping.get(state_filter, state_filter)
|
614 |
+
aqi_data = [item for item in aqi_data if item.get('state_code') == state_filter]
|
615 |
+
|
616 |
+
# Filter by county if provided
|
617 |
+
if county_filter:
|
618 |
+
aqi_data = [item for item in aqi_data if item.get('county_code') == county_filter]
|
619 |
+
|
620 |
+
if not aqi_data or len(aqi_data) == 0:
|
621 |
+
return "<p>No air quality data available for the selected criteria.</p>"
|
622 |
+
|
623 |
# Sort by date (most recent first) and then by AQI value (highest first)
|
624 |
sorted_data = sorted(aqi_data,
|
625 |
key=lambda x: (x.get('date_local', ''), -int(x.get('aqi', 0)) if x.get('aqi') and str(x.get('aqi')).isdigit() else 0),
|
|
|
642 |
<table style="width:100%; border-collapse: collapse;">
|
643 |
<tr style="background-color: #f2f2f2; position: sticky; top: 0;">
|
644 |
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Date</th>
|
645 |
+
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">State</th>
|
646 |
+
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">County</th>
|
647 |
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Location</th>
|
648 |
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Pollutant</th>
|
649 |
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">AQI</th>
|
|
|
655 |
for i, item in enumerate(site_data.values()):
|
656 |
date = item.get('date_local', 'N/A')
|
657 |
|
658 |
+
# Get state and county names
|
659 |
state_code = item.get('state_code', 'N/A')
|
660 |
+
state_name = 'N/A'
|
661 |
+
# Reverse lookup state name
|
662 |
+
for code, name in self.state_code_mapping.items():
|
663 |
+
if name == state_code:
|
664 |
+
state_name = self.states.get(code, 'Unknown')
|
665 |
+
break
|
666 |
+
|
667 |
county_code = item.get('county_code', 'N/A')
|
668 |
site_number = item.get('site_number', 'N/A')
|
669 |
+
location = f"Site {site_number}"
|
670 |
|
671 |
pollutant = item.get('parameter_name', 'N/A')
|
672 |
aqi_value = item.get('aqi', 'N/A')
|
|
|
679 |
html += f"""
|
680 |
<tr{row_style}>
|
681 |
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{date}</td>
|
682 |
+
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{state_name}</td>
|
683 |
+
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{county_code}</td>
|
684 |
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{location}</td>
|
685 |
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{pollutant}</td>
|
686 |
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{aqi_value}</td>
|
|
|
695 |
|
696 |
return html
|
697 |
|
|
|
698 |
def mock_get_counties(self, state_code):
|
699 |
"""Return mock county data for the specified state"""
|
700 |
+
# Sample county data for demo
|
701 |
+
mock_counties = {
|
702 |
+
"CA": [
|
703 |
+
{"code": "037", "value": "Los Angeles"},
|
704 |
+
{"code": "067", "value": "Sacramento"},
|
705 |
+
{"code": "073", "value": "San Diego"},
|
706 |
+
{"code": "075", "value": "San Francisco"}
|
707 |
+
],
|
708 |
+
"NY": [
|
709 |
+
{"code": "061", "value": "New York"},
|
710 |
+
{"code": "047", "value": "Kings (Brooklyn)"},
|
711 |
+
{"code": "081", "value": "Queens"},
|
712 |
+
{"code": "005", "value": "Bronx"}
|
713 |
+
],
|
714 |
+
"TX": [
|
715 |
+
{"code": "201", "value": "Harris (Houston)"},
|
716 |
+
{"code": "113", "value": "Dallas"},
|
717 |
+
{"code": "029", "value": "Bexar (San Antonio)"},
|
718 |
+
{"code": "453", "value": "Travis (Austin)"}
|
719 |
+
]
|
720 |
+
}
|
721 |
+
|
722 |
+
if state_code in mock_counties:
|
723 |
+
counties = mock_counties[state_code]
|
724 |
return [f"{c['code']}: {c['value']}" for c in counties]
|
725 |
else:
|
726 |
# Return generic counties for other states
|
|
|
733 |
|
734 |
def mock_get_parameters(self):
|
735 |
"""Return mock parameter data"""
|
736 |
+
# Sample parameters for demo
|
737 |
+
mock_parameters = [
|
738 |
+
{"code": "88101", "value_represented": "PM2.5 - Local Conditions"},
|
739 |
+
{"code": "44201", "value_represented": "Ozone"},
|
740 |
+
{"code": "42401", "value_represented": "Sulfur dioxide"},
|
741 |
+
{"code": "42101", "value_represented": "Carbon monoxide"},
|
742 |
+
{"code": "42602", "value_represented": "Nitrogen dioxide"},
|
743 |
+
{"code": "81102", "value_represented": "PM10 - Local Conditions"}
|
744 |
+
]
|
745 |
+
|
746 |
+
return [f"{p['code']}: {p['value_represented']}" for p in mock_parameters]
|
747 |
|
748 |
def mock_get_monitors(self, state_code, county_code=None, parameter_code=None):
|
749 |
"""Mock function to return sample data for development"""
|
|
|
909 |
"parameter_code": "88101",
|
910 |
"parameter_name": "PM2.5 - Local Conditions",
|
911 |
"poc": 1,
|
912 |
+
"latitude": 40.0 + float(ord(state_code[0]) % 10) / 10,
|
913 |
+
"longitude": -90.0 - float(ord(state_code[0]) % 10) / 10,
|
914 |
"local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 1",
|
915 |
"address": "123 Main Street",
|
916 |
"city_name": "City 1",
|
|
|
925 |
"parameter_code": "44201",
|
926 |
"parameter_name": "Ozone",
|
927 |
"poc": 1,
|
928 |
+
"latitude": 40.5 + float(ord(state_code[0]) % 10) / 10,
|
929 |
+
"longitude": -90.5 - float(ord(state_code[0]) % 10) / 10,
|
930 |
"local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 2",
|
931 |
"address": "456 Oak Street",
|
932 |
"city_name": "City 2",
|
|
|
946 |
|
947 |
return monitors
|
948 |
|
949 |
+
# Create the new UI with the nationwide map
|
950 |
def create_air_quality_map_ui():
|
951 |
+
"""Create the Gradio interface for the Air Quality Map application with nationwide data preloaded"""
|
952 |
app = AirQualityApp()
|
953 |
|
954 |
def update_counties(state_code):
|
|
|
956 |
counties = app.get_counties(state_code)
|
957 |
return gr.Dropdown(choices=counties)
|
958 |
|
959 |
+
def show_map_and_data(state=None, county=None, parameter=None):
|
960 |
"""Callback to generate and display both the map and the air quality data"""
|
961 |
# Extract code from county string if provided
|
962 |
county_code = None
|
|
|
968 |
if parameter and ":" in parameter:
|
969 |
parameter_code = parameter.split(":")[0].strip()
|
970 |
|
971 |
+
# Generate the map and get data - focus on state if selected
|
972 |
result = app.create_map(state, county_code, parameter_code)
|
973 |
|
974 |
if isinstance(result, dict):
|
|
|
982 |
|
983 |
# Process air quality data for the separate panel
|
984 |
if result["data"]:
|
985 |
+
data_html = app.format_air_quality_data_table(result["data"], state, county_code)
|
986 |
else:
|
987 |
data_html = "<p>No air quality data available for the selected criteria.</p>"
|
988 |
|
|
|
996 |
with gr.Blocks(title="Air Quality Monitoring Stations") as interface:
|
997 |
gr.Markdown("# NOAA Air Quality Monitoring Stations Map")
|
998 |
gr.Markdown("""
|
999 |
+
This application displays air quality monitoring stations across the United States and shows current air quality readings.
|
1000 |
|
1001 |
**Note:** To use the actual EPA AQS API, you need to register for an API key and set
|
1002 |
`EPA_AQS_EMAIL` and `EPA_AQS_API_KEY` environment variables in your Hugging Face Space.
|
1003 |
|
1004 |
+
For demonstration without an API key, the app shows sample data with more detailed information for California (CA), New York (NY), and Texas (TX).
|
1005 |
""")
|
1006 |
|
1007 |
with gr.Row():
|
1008 |
with gr.Column(scale=1):
|
1009 |
+
# State dropdown with empty default (all states)
|
1010 |
state_dropdown = gr.Dropdown(
|
1011 |
+
choices=[""] + list(app.states.keys()),
|
1012 |
+
label="Filter by State (Optional)",
|
1013 |
+
value=""
|
1014 |
)
|
1015 |
|
1016 |
+
# County dropdown (initially empty)
|
1017 |
county_dropdown = gr.Dropdown(
|
1018 |
+
choices=[],
|
1019 |
+
label="Filter by County (Optional)",
|
1020 |
allow_custom_value=True
|
1021 |
)
|
1022 |
|
1023 |
# Parameter dropdown (pollutant type)
|
1024 |
parameter_dropdown = gr.Dropdown(
|
1025 |
choices=app.mock_get_parameters(),
|
1026 |
+
label="Filter by Pollutant (Optional)",
|
1027 |
allow_custom_value=True
|
1028 |
)
|
1029 |
|
1030 |
+
# Button to update filters
|
1031 |
+
map_button = gr.Button("Update Filters")
|
1032 |
|
1033 |
# Create two tabs for the map and data
|
1034 |
with gr.Tabs() as tabs:
|
|
|
1052 |
inputs=[state_dropdown, county_dropdown, parameter_dropdown],
|
1053 |
outputs=[map_html, data_html]
|
1054 |
)
|
1055 |
+
|
1056 |
+
# Load initial map when the app starts
|
1057 |
+
interface.load(
|
1058 |
+
fn=show_map_and_data,
|
1059 |
+
inputs=None,
|
1060 |
+
outputs=[map_html, data_html]
|
1061 |
+
)
|
1062 |
|
1063 |
return interface
|
1064 |
|