|
import gradio as gr |
|
import requests |
|
import pandas as pd |
|
import folium |
|
from folium import plugins |
|
import json |
|
from datetime import datetime, timedelta |
|
import plotly.express as px |
|
import plotly.graph_objects as go |
|
from plotly.subplots import make_subplots |
|
|
|
class AirQualityMonitor: |
|
def __init__(self): |
|
self.base_url = "https://www.airnowapi.org/aq" |
|
|
|
self.api_key = "YOUR_API_KEY_HERE" |
|
|
|
def get_current_observations(self, state_code=None, bbox=None): |
|
"""Get current air quality observations""" |
|
if not self.api_key or self.api_key == "YOUR_API_KEY_HERE": |
|
|
|
return self._get_sample_data() |
|
|
|
url = f"{self.base_url}/observation/zipCode/current/" |
|
params = { |
|
'format': 'application/json', |
|
'API_KEY': self.api_key, |
|
'distance': 100 |
|
} |
|
|
|
if state_code: |
|
|
|
params['zipCode'] = self._get_state_zip(state_code) |
|
|
|
try: |
|
response = requests.get(url, params=params) |
|
response.raise_for_status() |
|
return response.json() |
|
except Exception as e: |
|
print(f"API Error: {e}") |
|
return self._get_sample_data() |
|
|
|
def _get_state_zip(self, state_code): |
|
"""Get a representative zip code for a state""" |
|
state_zips = { |
|
'CA': '90210', 'NY': '10001', 'TX': '73301', 'FL': '33101', |
|
'IL': '60601', 'PA': '19101', 'OH': '43215', 'GA': '30301', |
|
'NC': '27601', 'MI': '48201', 'NJ': '07001', 'VA': '23219', |
|
'WA': '98101', 'AZ': '85001', 'MA': '02101', 'TN': '37201', |
|
'IN': '46201', 'MO': '63101', 'MD': '21201', 'WI': '53201' |
|
} |
|
return state_zips.get(state_code, '90210') |
|
|
|
def _get_sample_data(self): |
|
"""Return sample data for demonstration""" |
|
return [ |
|
{ |
|
"DateObserved": "2025-06-29", |
|
"HourObserved": 14, |
|
"LocalTimeZone": "PST", |
|
"ReportingArea": "Los Angeles-South Coast Air Basin", |
|
"StateCode": "CA", |
|
"Latitude": 34.0522, |
|
"Longitude": -118.2437, |
|
"ParameterName": "PM2.5", |
|
"AQI": 65, |
|
"Category": {"Number": 2, "Name": "Moderate"} |
|
}, |
|
{ |
|
"DateObserved": "2025-06-29", |
|
"HourObserved": 14, |
|
"LocalTimeZone": "EST", |
|
"ReportingArea": "New York", |
|
"StateCode": "NY", |
|
"Latitude": 40.7128, |
|
"Longitude": -74.0060, |
|
"ParameterName": "Ozone", |
|
"AQI": 45, |
|
"Category": {"Number": 1, "Name": "Good"} |
|
}, |
|
{ |
|
"DateObserved": "2025-06-29", |
|
"HourObserved": 14, |
|
"LocalTimeZone": "CST", |
|
"ReportingArea": "Chicago", |
|
"StateCode": "IL", |
|
"Latitude": 41.8781, |
|
"Longitude": -87.6298, |
|
"ParameterName": "PM2.5", |
|
"AQI": 85, |
|
"Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"} |
|
}, |
|
{ |
|
"DateObserved": "2025-06-29", |
|
"HourObserved": 14, |
|
"LocalTimeZone": "MST", |
|
"ReportingArea": "Phoenix", |
|
"StateCode": "AZ", |
|
"Latitude": 33.4484, |
|
"Longitude": -112.0740, |
|
"ParameterName": "Ozone", |
|
"AQI": 95, |
|
"Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"} |
|
}, |
|
{ |
|
"DateObserved": "2025-06-29", |
|
"HourObserved": 14, |
|
"LocalTimeZone": "PST", |
|
"ReportingArea": "Seattle", |
|
"StateCode": "WA", |
|
"Latitude": 47.6062, |
|
"Longitude": -122.3321, |
|
"ParameterName": "PM2.5", |
|
"AQI": 25, |
|
"Category": {"Number": 1, "Name": "Good"} |
|
}, |
|
{ |
|
"DateObserved": "2025-06-29", |
|
"HourObserved": 14, |
|
"LocalTimeZone": "EST", |
|
"ReportingArea": "Miami", |
|
"StateCode": "FL", |
|
"Latitude": 25.7617, |
|
"Longitude": -80.1918, |
|
"ParameterName": "Ozone", |
|
"AQI": 55, |
|
"Category": {"Number": 2, "Name": "Moderate"} |
|
} |
|
] |
|
|
|
def get_aqi_color(aqi): |
|
"""Return color based on AQI value""" |
|
if aqi <= 50: |
|
return "#00E400" |
|
elif aqi <= 100: |
|
return "#FFFF00" |
|
elif aqi <= 150: |
|
return "#FF7E00" |
|
elif aqi <= 200: |
|
return "#FF0000" |
|
elif aqi <= 300: |
|
return "#8F3F97" |
|
else: |
|
return "#7E0023" |
|
|
|
def create_air_quality_map(data): |
|
"""Create an interactive map with air quality data""" |
|
if not data: |
|
|
|
m = folium.Map(location=[39.8283, -98.5795], zoom_start=4) |
|
folium.Marker( |
|
[39.8283, -98.5795], |
|
popup="No data available. Please add your AirNow API key.", |
|
icon=folium.Icon(color='red') |
|
).add_to(m) |
|
return m._repr_html_() |
|
|
|
|
|
lats = [float(station['Latitude']) for station in data] |
|
lons = [float(station['Longitude']) for station in data] |
|
center_lat = sum(lats) / len(lats) |
|
center_lon = sum(lons) / len(lons) |
|
|
|
|
|
m = folium.Map(location=[center_lat, center_lon], zoom_start=4) |
|
|
|
|
|
for station in data: |
|
lat = float(station['Latitude']) |
|
lon = float(station['Longitude']) |
|
aqi = station.get('AQI', 0) |
|
parameter = station.get('ParameterName', 'Unknown') |
|
area = station.get('ReportingArea', 'Unknown Area') |
|
category = station.get('Category', {}).get('Name', 'Unknown') |
|
|
|
color = get_aqi_color(aqi) |
|
|
|
popup_html = f""" |
|
<div style="width: 200px;"> |
|
<h4>{area}</h4> |
|
<p><strong>AQI:</strong> {aqi}</p> |
|
<p><strong>Parameter:</strong> {parameter}</p> |
|
<p><strong>Category:</strong> {category}</p> |
|
<p><strong>Date:</strong> {station.get('DateObserved', 'N/A')}</p> |
|
<p><strong>Time:</strong> {station.get('HourObserved', 'N/A')}:00 {station.get('LocalTimeZone', '')}</p> |
|
</div> |
|
""" |
|
|
|
folium.CircleMarker( |
|
location=[lat, lon], |
|
radius=8, |
|
popup=folium.Popup(popup_html, max_width=250), |
|
color='black', |
|
weight=1, |
|
fill=True, |
|
fillColor=color, |
|
fillOpacity=0.8, |
|
tooltip=f"{area}: AQI {aqi}" |
|
).add_to(m) |
|
|
|
|
|
legend_html = ''' |
|
<div style="position: fixed; |
|
top: 10px; right: 10px; width: 160px; height: 140px; |
|
background-color: white; border:2px solid grey; z-index:9999; |
|
font-size:14px; padding: 10px"> |
|
<h4>AQI Legend</h4> |
|
<p><i class="fa fa-circle" style="color:#00E400"></i> Good (0-50)</p> |
|
<p><i class="fa fa-circle" style="color:#FFFF00"></i> Moderate (51-100)</p> |
|
<p><i class="fa fa-circle" style="color:#FF7E00"></i> Unhealthy for Sensitive (101-150)</p> |
|
<p><i class="fa fa-circle" style="color:#FF0000"></i> Unhealthy (151-200)</p> |
|
<p><i class="fa fa-circle" style="color:#8F3F97"></i> Very Unhealthy (201-300)</p> |
|
</div> |
|
''' |
|
m.get_root().html.add_child(folium.Element(legend_html)) |
|
|
|
return m._repr_html_() |
|
|
|
def create_aqi_chart(data): |
|
"""Create a chart showing AQI levels by location""" |
|
if not data: |
|
return None |
|
|
|
df = pd.DataFrame(data) |
|
df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce') |
|
|
|
|
|
fig = px.bar(df, |
|
x='ReportingArea', |
|
y='AQI', |
|
color='AQI', |
|
color_continuous_scale='RdYlGn_r', |
|
title='Air Quality Index by Location', |
|
labels={'AQI': 'Air Quality Index', 'ReportingArea': 'Location'}) |
|
|
|
fig.update_layout(xaxis_tickangle=-45, height=500) |
|
fig.update_traces(hovertemplate='<b>%{x}</b><br>AQI: %{y}<extra></extra>') |
|
|
|
return fig |
|
|
|
def get_air_quality_summary(data): |
|
"""Generate a summary of air quality data""" |
|
if not data: |
|
return "No air quality data available." |
|
|
|
df = pd.DataFrame(data) |
|
df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce') |
|
|
|
total_stations = len(df) |
|
avg_aqi = df['AQI'].mean() |
|
max_aqi = df['AQI'].max() |
|
min_aqi = df['AQI'].min() |
|
|
|
|
|
categories = df['Category'].apply(lambda x: x.get('Name', 'Unknown') if isinstance(x, dict) else 'Unknown') |
|
category_counts = categories.value_counts() |
|
|
|
summary = f""" |
|
## Air Quality Summary |
|
|
|
**Total Monitoring Stations:** {total_stations} |
|
**Average AQI:** {avg_aqi:.1f} |
|
**Highest AQI:** {max_aqi} |
|
**Lowest AQI:** {min_aqi} |
|
|
|
### Air Quality Categories: |
|
""" |
|
|
|
for category, count in category_counts.items(): |
|
percentage = (count / total_stations) * 100 |
|
summary += f"- **{category}:** {count} stations ({percentage:.1f}%)\n" |
|
|
|
return summary |
|
|
|
def update_data(): |
|
"""Main function to fetch and display air quality data""" |
|
monitor = AirQualityMonitor() |
|
data = monitor.get_current_observations() |
|
|
|
map_html = create_air_quality_map(data) |
|
chart = create_aqi_chart(data) |
|
summary = get_air_quality_summary(data) |
|
|
|
return map_html, chart, summary |
|
|
|
|
|
with gr.Blocks(title="Air Quality Monitor", theme=gr.themes.Soft()) as demo: |
|
gr.Markdown("# π¬οΈ Real-Time Air Quality Monitor") |
|
gr.Markdown("Monitor air quality data from NOAA/EPA AirNow stations across the United States") |
|
|
|
with gr.Row(): |
|
gr.Markdown(""" |
|
This application displays real-time air quality data from over 2,000 monitoring stations |
|
across the United States. The data includes Air Quality Index (AQI) values for various |
|
pollutants like PM2.5, PM10, Ozone, and more. |
|
|
|
**Note:** To access live data, you need to: |
|
1. Get a free API key from [AirNow API](https://docs.airnowapi.org/) |
|
2. Replace `YOUR_API_KEY_HERE` in the code with your actual API key |
|
|
|
Currently showing sample data for demonstration. |
|
""") |
|
|
|
with gr.Row(): |
|
update_btn = gr.Button("π Update Air Quality Data", variant="primary", size="lg") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=2): |
|
map_output = gr.HTML(label="Air Quality Map") |
|
with gr.Column(scale=1): |
|
summary_output = gr.Markdown(label="Summary") |
|
|
|
with gr.Row(): |
|
chart_output = gr.Plot(label="AQI Chart") |
|
|
|
|
|
with gr.Row(): |
|
gr.Markdown(""" |
|
## About Air Quality Index (AQI) |
|
|
|
The AQI is an index for reporting daily air quality. It tells you how clean or polluted your air is: |
|
|
|
- **Good (0-50)**: π’ Air quality is satisfactory |
|
- **Moderate (51-100)**: π‘ Acceptable for most people |
|
- **Unhealthy for Sensitive Groups (101-150)**: π May cause problems for sensitive individuals |
|
- **Unhealthy (151-200)**: π΄ May cause health problems for everyone |
|
- **Very Unhealthy (201-300)**: π£ Health alert for everyone |
|
- **Hazardous (301+)**: π€ Health emergency |
|
|
|
**Data Source:** EPA AirNow - Real-time air quality observations from government monitoring stations |
|
""") |
|
|
|
|
|
demo.load(update_data, outputs=[map_output, chart_output, summary_output]) |
|
update_btn.click(update_data, outputs=[map_output, chart_output, summary_output]) |
|
|
|
if __name__ == "__main__": |
|
demo.launch() |