Spaces:
Sleeping
Sleeping
import gradio as gr | |
import requests | |
import pandas as pd | |
import folium | |
from folium.plugins import MarkerCluster | |
import tempfile | |
import os | |
import json | |
# Get API credentials from environment variables | |
EPA_AQS_API_BASE_URL = "https://aqs.epa.gov/data/api" | |
EMAIL = os.environ.get("EPA_AQS_EMAIL", "") # Get from environment variable | |
API_KEY = os.environ.get("EPA_AQS_API_KEY", "") # Get from environment variable | |
class AirQualityApp: | |
def __init__(self): | |
self.states = { | |
"AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas", | |
"CA": "California", "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware", | |
"FL": "Florida", "GA": "Georgia", "HI": "Hawaii", "ID": "Idaho", | |
"IL": "Illinois", "IN": "Indiana", "IA": "Iowa", "KS": "Kansas", | |
"KY": "Kentucky", "LA": "Louisiana", "ME": "Maine", "MD": "Maryland", | |
"MA": "Massachusetts", "MI": "Michigan", "MN": "Minnesota", "MS": "Mississippi", | |
"MO": "Missouri", "MT": "Montana", "NE": "Nebraska", "NV": "Nevada", | |
"NH": "New Hampshire", "NJ": "New Jersey", "NM": "New Mexico", "NY": "New York", | |
"NC": "North Carolina", "ND": "North Dakota", "OH": "Ohio", "OK": "Oklahoma", | |
"OR": "Oregon", "PA": "Pennsylvania", "RI": "Rhode Island", "SC": "South Carolina", | |
"SD": "South Dakota", "TN": "Tennessee", "TX": "Texas", "UT": "Utah", | |
"VT": "Vermont", "VA": "Virginia", "WA": "Washington", "WV": "West Virginia", | |
"WI": "Wisconsin", "WY": "Wyoming", "DC": "District of Columbia" | |
} | |
# Mapping from two-letter state codes to numeric state codes for API | |
self.state_code_mapping = { | |
"AL": "01", "AK": "02", "AZ": "04", "AR": "05", | |
"CA": "06", "CO": "08", "CT": "09", "DE": "10", | |
"FL": "12", "GA": "13", "HI": "15", "ID": "16", | |
"IL": "17", "IN": "18", "IA": "19", "KS": "20", | |
"KY": "21", "LA": "22", "ME": "23", "MD": "24", | |
"MA": "25", "MI": "26", "MN": "27", "MS": "28", | |
"MO": "29", "MT": "30", "NE": "31", "NV": "32", | |
"NH": "33", "NJ": "34", "NM": "35", "NY": "36", | |
"NC": "37", "ND": "38", "OH": "39", "OK": "40", | |
"OR": "41", "PA": "42", "RI": "44", "SC": "45", | |
"SD": "46", "TN": "47", "TX": "48", "UT": "49", | |
"VT": "50", "VA": "51", "WA": "53", "WV": "54", | |
"WI": "55", "WY": "56", "DC": "11" | |
} | |
# AQI categories with their corresponding colors | |
self.aqi_categories = { | |
"Good": "#00e400", # Green | |
"Moderate": "#ffff00", # Yellow | |
"Unhealthy for Sensitive Groups": "#ff7e00", # Orange | |
"Unhealthy": "#ff0000", # Red | |
"Very Unhealthy": "#99004c", # Purple | |
"Hazardous": "#7e0023" # Maroon | |
} | |
# Sample county data for demo | |
self.mock_counties = { | |
"CA": [ | |
{"code": "037", "value": "Los Angeles"}, | |
{"code": "067", "value": "Sacramento"}, | |
{"code": "073", "value": "San Diego"}, | |
{"code": "075", "value": "San Francisco"} | |
], | |
"NY": [ | |
{"code": "061", "value": "New York"}, | |
{"code": "047", "value": "Kings (Brooklyn)"}, | |
{"code": "081", "value": "Queens"}, | |
{"code": "005", "value": "Bronx"} | |
], | |
"TX": [ | |
{"code": "201", "value": "Harris (Houston)"}, | |
{"code": "113", "value": "Dallas"}, | |
{"code": "029", "value": "Bexar (San Antonio)"}, | |
{"code": "453", "value": "Travis (Austin)"} | |
] | |
} | |
# Sample parameters for demo | |
self.mock_parameters = [ | |
{"code": "88101", "value_represented": "PM2.5 - Local Conditions"}, | |
{"code": "44201", "value_represented": "Ozone"}, | |
{"code": "42401", "value_represented": "Sulfur dioxide"}, | |
{"code": "42101", "value_represented": "Carbon monoxide"}, | |
{"code": "42602", "value_represented": "Nitrogen dioxide"}, | |
{"code": "81102", "value_represented": "PM10 - Local Conditions"} | |
] | |
def get_monitors(self, state_code, county_code=None, parameter_code=None): | |
"""Fetch monitoring stations for a given state and optional county""" | |
# If we don't have API credentials, use mock data | |
if not EMAIL or not API_KEY: | |
return self.mock_get_monitors(state_code, county_code, parameter_code) | |
# Convert state code to numeric format for API | |
api_state_code = state_code | |
if len(state_code) == 2 and state_code in self.state_code_mapping: | |
api_state_code = self.state_code_mapping[state_code] | |
# API endpoint for monitoring sites | |
endpoint = f"{EPA_AQS_API_BASE_URL}/monitors/byState" | |
params = { | |
"email": EMAIL, | |
"key": API_KEY, | |
"state": api_state_code, | |
"bdate": "20240101", # Beginning date (YYYYMMDD) | |
"edate": "20240414", # End date (YYYYMMDD) | |
} | |
if county_code: | |
params["county"] = county_code | |
if parameter_code: | |
params["param"] = parameter_code | |
try: | |
response = requests.get(endpoint, params=params) | |
data = response.json() | |
# Handle the specific response structure we observed | |
if isinstance(data, dict): | |
if "Data" in data and isinstance(data["Data"], list): | |
return data["Data"] | |
elif "Header" in data and isinstance(data["Header"], list): | |
if data["Header"][0].get("status") == "Success": | |
return data.get("Data", []) | |
# If we couldn't parse the response format, return empty list | |
print(f"Unexpected response format for monitors: {type(data)}") | |
return [] | |
except Exception as e: | |
print(f"Error fetching monitors: {e}") | |
return [] | |
def get_counties(self, state_code): | |
"""Fetch counties for a given state""" | |
# If we don't have API credentials, use mock data | |
if not EMAIL or not API_KEY: | |
return self.mock_get_counties(state_code) | |
# Convert state code to numeric format for API | |
api_state_code = state_code | |
if len(state_code) == 2 and state_code in self.state_code_mapping: | |
api_state_code = self.state_code_mapping[state_code] | |
endpoint = f"{EPA_AQS_API_BASE_URL}/list/countiesByState" | |
params = { | |
"email": EMAIL, | |
"key": API_KEY, | |
"state": api_state_code | |
} | |
try: | |
response = requests.get(endpoint, params=params) | |
data = response.json() | |
# Handle the specific response structure we observed | |
counties = [] | |
if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list): | |
counties = data["Data"] | |
# Format as "code: name" for dropdown | |
result = [] | |
for c in counties: | |
code = c.get("code") | |
value = c.get("value_represented") | |
if code and value: | |
result.append(f"{code}: {value}") | |
return result | |
except Exception as e: | |
print(f"Error fetching counties: {e}") | |
return [] | |
def get_parameters(self): | |
"""Fetch available parameter codes (pollutants)""" | |
# If we don't have API credentials, use mock data | |
if not EMAIL or not API_KEY: | |
return self.mock_get_parameters() | |
endpoint = f"{EPA_AQS_API_BASE_URL}/list/parametersByClass" | |
params = { | |
"email": EMAIL, | |
"key": API_KEY, | |
"pc": "CRITERIA" # Filter to criteria pollutants | |
} | |
try: | |
response = requests.get(endpoint, params=params) | |
data = response.json() | |
# Handle the specific response structure we observed | |
parameters = [] | |
if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list): | |
parameters = data["Data"] | |
# Format as "code: name" for dropdown | |
result = [] | |
for p in parameters: | |
code = p.get("code") | |
value = p.get("value_represented") | |
if not code: | |
code = p.get("parameter_code") | |
if not value: | |
value = p.get("parameter_name") | |
if code and value: | |
result.append(f"{code}: {value}") | |
return result | |
except Exception as e: | |
print(f"Error fetching parameters: {e}") | |
return [] | |
def get_latest_aqi(self, state_code, county_code=None, parameter_code=None): | |
"""Fetch the latest AQI data for monitors""" | |
# If we don't have API credentials, use mock data | |
if not EMAIL or not API_KEY: | |
return [] # We don't have mock AQI data for simplicity | |
# Convert state code to numeric format for API | |
api_state_code = state_code | |
if len(state_code) == 2 and state_code in self.state_code_mapping: | |
api_state_code = self.state_code_mapping[state_code] | |
endpoint = f"{EPA_AQS_API_BASE_URL}/dailyData/byState" | |
params = { | |
"email": EMAIL, | |
"key": API_KEY, | |
"state": api_state_code, | |
"bdate": "20240314", # Beginning date (YYYYMMDD) - last 30 days | |
"edate": "20240414", # End date (YYYYMMDD) - current date | |
} | |
if county_code: | |
params["county"] = county_code | |
if parameter_code: | |
params["param"] = parameter_code | |
try: | |
response = requests.get(endpoint, params=params) | |
data = response.json() | |
# Handle the specific response structure we observed | |
if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list): | |
return data["Data"] | |
else: | |
print(f"Unexpected response format for AQI data: {type(data)}") | |
return [] | |
except Exception as e: | |
print(f"Error fetching AQI data: {e}") | |
return [] | |
def create_map(self, state_code, county_code=None, parameter_code=None): | |
"""Create a map with air quality monitoring stations""" | |
monitors = self.get_monitors(state_code, county_code, parameter_code) | |
if not monitors: | |
return "No monitoring stations found for the selected criteria." | |
# Convert to DataFrame for easier manipulation | |
df = pd.DataFrame(monitors) | |
# Create a map centered on the mean latitude and longitude | |
center_lat = df["latitude"].mean() | |
center_lon = df["longitude"].mean() | |
# Create a map with a specific width and height - make it bigger | |
m = folium.Map(location=[center_lat, center_lon], zoom_start=7, width='100%', height=700) | |
# Add a marker cluster | |
marker_cluster = MarkerCluster().add_to(m) | |
# Get latest AQI data if credentials are provided | |
aqi_data = {} | |
if EMAIL and API_KEY: | |
aqi_results = self.get_latest_aqi(state_code, county_code, parameter_code) | |
# Create a lookup dictionary by site ID | |
for item in aqi_results: | |
site_id = f"{item['state_code']}-{item['county_code']}-{item['site_number']}" | |
if site_id not in aqi_data or item['date_local'] > aqi_data[site_id]['date_local']: | |
aqi_data[site_id] = item | |
# Add markers for each monitoring station | |
for _, row in df.iterrows(): | |
site_id = f"{row['state_code']}-{row['county_code']}-{row['site_number']}" | |
# Default marker color is blue | |
color = "blue" | |
aqi_info = "" | |
# If we have AQI data for this monitor, set the color based on AQI category | |
if site_id in aqi_data: | |
aqi_value = aqi_data[site_id].get('aqi', 0) | |
if aqi_value: | |
aqi_category = self.get_aqi_category(aqi_value) | |
color = self.aqi_categories.get(aqi_category, "blue") | |
aqi_info = f"<br>Latest AQI: {aqi_value} ({aqi_category})" | |
# Create popup content | |
popup_content = f""" | |
<b>{row['local_site_name']}</b><br> | |
Parameter: {row['parameter_name']}<br> | |
Site ID: {site_id}<br> | |
Latitude: {row['latitude']}, Longitude: {row['longitude']}{aqi_info} | |
""" | |
# Add marker to cluster | |
folium.Marker( | |
location=[row["latitude"], row["longitude"]], | |
popup=folium.Popup(popup_content, max_width=300), | |
icon=folium.Icon(color=color, icon="cloud"), | |
).add_to(marker_cluster) | |
# Return map HTML and legend HTML separately | |
map_html = m._repr_html_() | |
# Create legend HTML outside the map | |
legend_html = self.create_legend_html() | |
return {"map": map_html, "legend": legend_html} | |
def create_legend_html(self): | |
"""Create the HTML for the AQI legend""" | |
legend_html = """ | |
<div style="padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: white; margin-top: 10px;"> | |
<h4 style="margin-top: 0;">AQI Categories</h4> | |
<div style="display: grid; grid-template-columns: auto 1fr; grid-gap: 5px; align-items: center;"> | |
""" | |
for category, color in self.aqi_categories.items(): | |
legend_html += f'<span style="background-color: {color}; width: 20px; height: 20px; display: inline-block;"></span>' | |
legend_html += f'<span>{category}</span>' | |
legend_html += """ | |
</div> | |
</div> | |
""" | |
return legend_html | |
def get_aqi_category(self, aqi_value): | |
"""Determine AQI category based on value""" | |
aqi = int(aqi_value) | |
if aqi <= 50: | |
return "Good" | |
elif aqi <= 100: | |
return "Moderate" | |
elif aqi <= 150: | |
return "Unhealthy for Sensitive Groups" | |
elif aqi <= 200: | |
return "Unhealthy" | |
elif aqi <= 300: | |
return "Very Unhealthy" | |
else: | |
return "Hazardous" | |
def mock_get_counties(self, state_code): | |
"""Return mock county data for the specified state""" | |
if state_code in self.mock_counties: | |
counties = self.mock_counties[state_code] | |
return [f"{c['code']}: {c['value']}" for c in counties] | |
else: | |
# Return generic counties for other states | |
return [ | |
"001: County 1", | |
"002: County 2", | |
"003: County 3", | |
"004: County 4" | |
] | |
def mock_get_parameters(self): | |
"""Return mock parameter data""" | |
return [f"{p['code']}: {p['value_represented']}" for p in self.mock_parameters] | |
def mock_get_monitors(self, state_code, county_code=None, parameter_code=None): | |
"""Mock function to return sample data for development""" | |
# Get state code in proper format | |
if len(state_code) == 2: | |
# Convert 2-letter state code to numeric format for mock data | |
state_code_mapping = { | |
"CA": "06", | |
"NY": "36", | |
"TX": "48" | |
} | |
numeric_state_code = state_code_mapping.get(state_code, "01") # Default to "01" if not found | |
else: | |
numeric_state_code = state_code | |
# Sample data for California | |
if state_code == "CA" or numeric_state_code == "06": | |
monitors = [ | |
{ | |
"state_code": "06", | |
"county_code": "037", | |
"site_number": "0001", | |
"parameter_code": "88101", | |
"parameter_name": "PM2.5 - Local Conditions", | |
"poc": 1, | |
"latitude": 34.0667, | |
"longitude": -118.2275, | |
"local_site_name": "Los Angeles - North Main Street", | |
"address": "1630 North Main Street", | |
"city_name": "Los Angeles", | |
"cbsa_name": "Los Angeles-Long Beach-Anaheim", | |
"date_established": "1998-01-01", | |
"last_sample_date": "2024-04-10" | |
}, | |
{ | |
"state_code": "06", | |
"county_code": "037", | |
"site_number": "0002", | |
"parameter_code": "44201", | |
"parameter_name": "Ozone", | |
"poc": 1, | |
"latitude": 34.0667, | |
"longitude": -118.2275, | |
"local_site_name": "Los Angeles - North Main Street", | |
"address": "1630 North Main Street", | |
"city_name": "Los Angeles", | |
"cbsa_name": "Los Angeles-Long Beach-Anaheim", | |
"date_established": "1998-01-01", | |
"last_sample_date": "2024-04-10" | |
}, | |
{ | |
"state_code": "06", | |
"county_code": "067", | |
"site_number": "0010", | |
"parameter_code": "88101", | |
"parameter_name": "PM2.5 - Local Conditions", | |
"poc": 1, | |
"latitude": 38.5661, | |
"longitude": -121.4926, | |
"local_site_name": "Sacramento - T Street", | |
"address": "1309 T Street", | |
"city_name": "Sacramento", | |
"cbsa_name": "Sacramento-Roseville", | |
"date_established": "1999-03-01", | |
"last_sample_date": "2024-04-10" | |
}, | |
{ | |
"state_code": "06", | |
"county_code": "073", | |
"site_number": "0005", | |
"parameter_code": "88101", | |
"parameter_name": "PM2.5 - Local Conditions", | |
"poc": 1, | |
"latitude": 32.7333, | |
"longitude": -117.1500, | |
"local_site_name": "San Diego - Beardsley Street", | |
"address": "1110 Beardsley Street", | |
"city_name": "San Diego", | |
"cbsa_name": "San Diego-Carlsbad", | |
"date_established": "1999-04-15", | |
"last_sample_date": "2024-04-10" | |
} | |
] | |
# Sample data for New York | |
elif state_code == "NY" or numeric_state_code == "36": | |
monitors = [ | |
{ | |
"state_code": "36", | |
"county_code": "061", | |
"site_number": "0010", | |
"parameter_code": "88101", | |
"parameter_name": "PM2.5 - Local Conditions", | |
"poc": 1, | |
"latitude": 40.7159, | |
"longitude": -73.9876, | |
"local_site_name": "New York - PS 59", | |
"address": "228 East 57th Street", | |
"city_name": "New York", | |
"cbsa_name": "New York-Newark-Jersey City", | |
"date_established": "1999-07-15", | |
"last_sample_date": "2024-04-10" | |
}, | |
{ | |
"state_code": "36", | |
"county_code": "061", | |
"site_number": "0079", | |
"parameter_code": "44201", | |
"parameter_name": "Ozone", | |
"poc": 1, | |
"latitude": 40.8160, | |
"longitude": -73.9510, | |
"local_site_name": "New York - IS 52", | |
"address": "681 Kelly Street", | |
"city_name": "Bronx", | |
"cbsa_name": "New York-Newark-Jersey City", | |
"date_established": "1998-01-01", | |
"last_sample_date": "2024-04-10" | |
} | |
] | |
# Sample data for Texas | |
elif state_code == "TX" or numeric_state_code == "48": | |
monitors = [ | |
{ | |
"state_code": "48", | |
"county_code": "201", | |
"site_number": "0024", | |
"parameter_code": "88101", | |
"parameter_name": "PM2.5 - Local Conditions", | |
"poc": 1, | |
"latitude": 29.7349, | |
"longitude": -95.3063, | |
"local_site_name": "Houston - Clinton Drive", | |
"address": "9525 Clinton Drive", | |
"city_name": "Houston", | |
"cbsa_name": "Houston-The Woodlands-Sugar Land", | |
"date_established": "1997-09-01", | |
"last_sample_date": "2024-04-10" | |
}, | |
{ | |
"state_code": "48", | |
"county_code": "113", | |
"site_number": "0050", | |
"parameter_code": "44201", | |
"parameter_name": "Ozone", | |
"poc": 1, | |
"latitude": 32.8198, | |
"longitude": -96.8602, | |
"local_site_name": "Dallas - Hinton Street", | |
"address": "1415 Hinton Street", | |
"city_name": "Dallas", | |
"cbsa_name": "Dallas-Fort Worth-Arlington", | |
"date_established": "1998-01-01", | |
"last_sample_date": "2024-04-10" | |
} | |
] | |
else: | |
# Default data for other states - generate some random monitors | |
monitors = [ | |
{ | |
"state_code": state_code, | |
"county_code": "001", | |
"site_number": "0001", | |
"parameter_code": "88101", | |
"parameter_name": "PM2.5 - Local Conditions", | |
"poc": 1, | |
"latitude": 40.0 + float(ord(state_code[0])) / 10, | |
"longitude": -90.0 - float(ord(state_code[1])) / 10, | |
"local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 1", | |
"address": "123 Main Street", | |
"city_name": "City 1", | |
"cbsa_name": f"{self.states.get(state_code, 'Unknown')} Metro Area", | |
"date_established": "2000-01-01", | |
"last_sample_date": "2024-04-10" | |
}, | |
{ | |
"state_code": state_code, | |
"county_code": "002", | |
"site_number": "0002", | |
"parameter_code": "44201", | |
"parameter_name": "Ozone", | |
"poc": 1, | |
"latitude": 40.5 + float(ord(state_code[0])) / 10, | |
"longitude": -90.5 - float(ord(state_code[1])) / 10, | |
"local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 2", | |
"address": "456 Oak Street", | |
"city_name": "City 2", | |
"cbsa_name": f"{self.states.get(state_code, 'Unknown')} Metro Area", | |
"date_established": "2000-01-01", | |
"last_sample_date": "2024-04-10" | |
} | |
] | |
# Filter by county if provided | |
if county_code: | |
monitors = [m for m in monitors if m["county_code"] == county_code] | |
# Filter by parameter if provided | |
if parameter_code: | |
monitors = [m for m in monitors if m["parameter_code"] == parameter_code] | |
return monitors | |
def create_air_quality_map_ui(): | |
"""Create the Gradio interface for the Air Quality Map application""" | |
app = AirQualityApp() | |
def update_counties(state_code): | |
"""Callback to update counties dropdown when state changes""" | |
counties = app.get_counties(state_code) | |
return counties | |
def show_map(state, county=None, parameter=None): | |
"""Callback to generate and display the map""" | |
# Extract code from county string if provided | |
county_code = None | |
if county and ":" in county: | |
county_code = county.split(":")[0].strip() | |
# Extract code from parameter string if provided | |
parameter_code = None | |
if parameter and ":" in parameter: | |
parameter_code = parameter.split(":")[0].strip() | |
# Generate the map | |
result = app.create_map(state, county_code, parameter_code) | |
if isinstance(result, dict): | |
# Combine map and legend HTML | |
html_content = f""" | |
<div> | |
{result["map"]} | |
{result["legend"]} | |
</div> | |
""" | |
return html_content | |
else: | |
# Return error message or whatever was returned | |
return result | |
# Create the UI | |
with gr.Blocks(title="Air Quality Monitoring Stations") as interface: | |
gr.Markdown("# NOAA Air Quality Monitoring Stations Map") | |
gr.Markdown(""" | |
This application displays air quality monitoring stations in the United States. | |
**Note:** To use the actual EPA AQS API, you need to register for an API key at | |
[https://aqs.epa.gov/aqsweb/documents/data_api.html](https://aqs.epa.gov/aqsweb/documents/data_api.html) | |
and update the EMAIL and API_KEY constants in the code. | |
For demonstration without an API key, the app shows sample data for California (CA), New York (NY), and Texas (TX). | |
""") | |
with gr.Row(): | |
with gr.Column(scale=1): | |
# State dropdown with default value | |
state_dropdown = gr.Dropdown( | |
choices=list(app.states.keys()), | |
label="Select State", | |
value="CA" | |
) | |
# County dropdown with mock counties for the default state | |
county_dropdown = gr.Dropdown( | |
choices=app.mock_get_counties("CA"), | |
label="Select County (Optional)", | |
allow_custom_value=True | |
) | |
# Parameter dropdown (pollutant type) | |
parameter_dropdown = gr.Dropdown( | |
choices=app.mock_get_parameters(), | |
label="Select Pollutant (Optional)", | |
allow_custom_value=True | |
) | |
# Button to generate map | |
map_button = gr.Button("Show Map") | |
# HTML component to display the map in a larger column | |
with gr.Column(scale=3): | |
map_html = gr.HTML(label="Air Quality Monitoring Stations Map") | |
# Set up event handlers | |
state_dropdown.change( | |
fn=update_counties, | |
inputs=state_dropdown, | |
outputs=county_dropdown | |
) | |
map_button.click( | |
fn=show_map, | |
inputs=[state_dropdown, county_dropdown, parameter_dropdown], | |
outputs=map_html | |
) | |
return interface | |
# Create and launch the app | |
if __name__ == "__main__": | |
air_quality_map_ui = create_air_quality_map_ui() | |
air_quality_map_ui.launch() |