Update app.py
Browse files
app.py
CHANGED
@@ -1,338 +1,341 @@
|
|
1 |
import gradio as gr
|
2 |
import requests
|
3 |
-
import pandas as pd
|
4 |
import folium
|
5 |
-
from folium import plugins
|
6 |
import json
|
7 |
-
|
8 |
-
import
|
9 |
-
import
|
10 |
-
|
11 |
|
12 |
-
class
|
|
|
|
|
13 |
def __init__(self):
|
14 |
-
self.base_url = "https://www.airnowapi.org
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
#
|
22 |
-
return self._get_sample_data()
|
23 |
-
|
24 |
-
url = f"{self.base_url}/observation/zipCode/current/"
|
25 |
-
params = {
|
26 |
-
'format': 'application/json',
|
27 |
-
'API_KEY': self.api_key,
|
28 |
-
'distance': 100
|
29 |
}
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
response.raise_for_status()
|
38 |
-
return response.json()
|
39 |
-
except Exception as e:
|
40 |
-
print(f"API Error: {e}")
|
41 |
-
return self._get_sample_data()
|
42 |
-
|
43 |
-
def _get_state_zip(self, state_code):
|
44 |
-
"""Get a representative zip code for a state"""
|
45 |
-
state_zips = {
|
46 |
-
'CA': '90210', 'NY': '10001', 'TX': '73301', 'FL': '33101',
|
47 |
-
'IL': '60601', 'PA': '19101', 'OH': '43215', 'GA': '30301',
|
48 |
-
'NC': '27601', 'MI': '48201', 'NJ': '07001', 'VA': '23219',
|
49 |
-
'WA': '98101', 'AZ': '85001', 'MA': '02101', 'TN': '37201',
|
50 |
-
'IN': '46201', 'MO': '63101', 'MD': '21201', 'WI': '53201'
|
51 |
}
|
52 |
-
return state_zips.get(state_code, '90210')
|
53 |
|
54 |
-
def
|
55 |
-
"""
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
"LocalTimeZone": "PST",
|
61 |
-
"ReportingArea": "Los Angeles-South Coast Air Basin",
|
62 |
-
"StateCode": "CA",
|
63 |
-
"Latitude": 34.0522,
|
64 |
-
"Longitude": -118.2437,
|
65 |
-
"ParameterName": "PM2.5",
|
66 |
-
"AQI": 65,
|
67 |
-
"Category": {"Number": 2, "Name": "Moderate"}
|
68 |
-
},
|
69 |
-
{
|
70 |
-
"DateObserved": "2025-06-29",
|
71 |
-
"HourObserved": 14,
|
72 |
-
"LocalTimeZone": "EST",
|
73 |
-
"ReportingArea": "New York",
|
74 |
-
"StateCode": "NY",
|
75 |
-
"Latitude": 40.7128,
|
76 |
-
"Longitude": -74.0060,
|
77 |
-
"ParameterName": "Ozone",
|
78 |
-
"AQI": 45,
|
79 |
-
"Category": {"Number": 1, "Name": "Good"}
|
80 |
-
},
|
81 |
-
{
|
82 |
-
"DateObserved": "2025-06-29",
|
83 |
-
"HourObserved": 14,
|
84 |
-
"LocalTimeZone": "CST",
|
85 |
-
"ReportingArea": "Chicago",
|
86 |
-
"StateCode": "IL",
|
87 |
-
"Latitude": 41.8781,
|
88 |
-
"Longitude": -87.6298,
|
89 |
-
"ParameterName": "PM2.5",
|
90 |
-
"AQI": 85,
|
91 |
-
"Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"}
|
92 |
-
},
|
93 |
-
{
|
94 |
-
"DateObserved": "2025-06-29",
|
95 |
-
"HourObserved": 14,
|
96 |
-
"LocalTimeZone": "MST",
|
97 |
-
"ReportingArea": "Phoenix",
|
98 |
-
"StateCode": "AZ",
|
99 |
-
"Latitude": 33.4484,
|
100 |
-
"Longitude": -112.0740,
|
101 |
-
"ParameterName": "Ozone",
|
102 |
-
"AQI": 95,
|
103 |
-
"Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"}
|
104 |
-
},
|
105 |
-
{
|
106 |
-
"DateObserved": "2025-06-29",
|
107 |
-
"HourObserved": 14,
|
108 |
-
"LocalTimeZone": "PST",
|
109 |
-
"ReportingArea": "Seattle",
|
110 |
-
"StateCode": "WA",
|
111 |
-
"Latitude": 47.6062,
|
112 |
-
"Longitude": -122.3321,
|
113 |
-
"ParameterName": "PM2.5",
|
114 |
-
"AQI": 25,
|
115 |
-
"Category": {"Number": 1, "Name": "Good"}
|
116 |
-
},
|
117 |
-
{
|
118 |
-
"DateObserved": "2025-06-29",
|
119 |
-
"HourObserved": 14,
|
120 |
-
"LocalTimeZone": "EST",
|
121 |
-
"ReportingArea": "Miami",
|
122 |
-
"StateCode": "FL",
|
123 |
-
"Latitude": 25.7617,
|
124 |
-
"Longitude": -80.1918,
|
125 |
-
"ParameterName": "Ozone",
|
126 |
-
"AQI": 55,
|
127 |
-
"Category": {"Number": 2, "Name": "Moderate"}
|
128 |
-
}
|
129 |
-
]
|
130 |
-
|
131 |
-
def get_aqi_color(aqi):
|
132 |
-
"""Return color based on AQI value"""
|
133 |
-
if aqi <= 50:
|
134 |
-
return "#00E400" # Green
|
135 |
-
elif aqi <= 100:
|
136 |
-
return "#FFFF00" # Yellow
|
137 |
-
elif aqi <= 150:
|
138 |
-
return "#FF7E00" # Orange
|
139 |
-
elif aqi <= 200:
|
140 |
-
return "#FF0000" # Red
|
141 |
-
elif aqi <= 300:
|
142 |
-
return "#8F3F97" # Purple
|
143 |
-
else:
|
144 |
-
return "#7E0023" # Maroon
|
145 |
-
|
146 |
-
def create_air_quality_map(data):
|
147 |
-
"""Create an interactive map with air quality data"""
|
148 |
-
if not data:
|
149 |
-
# Create empty map centered on US
|
150 |
-
m = folium.Map(location=[39.8283, -98.5795], zoom_start=4)
|
151 |
-
folium.Marker(
|
152 |
-
[39.8283, -98.5795],
|
153 |
-
popup="No data available. Please add your AirNow API key.",
|
154 |
-
icon=folium.Icon(color='red')
|
155 |
-
).add_to(m)
|
156 |
-
return m._repr_html_()
|
157 |
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
center_lat = sum(lats) / len(lats)
|
162 |
-
center_lon = sum(lons) / len(lons)
|
163 |
|
164 |
-
|
165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
|
176 |
-
|
|
|
|
|
|
|
|
|
177 |
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
186 |
</div>
|
187 |
"""
|
|
|
188 |
|
189 |
-
|
190 |
-
location=[lat, lon],
|
191 |
-
radius=8,
|
192 |
-
popup=folium.Popup(popup_html, max_width=250),
|
193 |
-
color='black',
|
194 |
-
weight=1,
|
195 |
-
fill=True,
|
196 |
-
fillColor=color,
|
197 |
-
fillOpacity=0.8,
|
198 |
-
tooltip=f"{area}: AQI {aqi}"
|
199 |
-
).add_to(m)
|
200 |
-
|
201 |
-
# Add legend
|
202 |
-
legend_html = '''
|
203 |
-
<div style="position: fixed;
|
204 |
-
top: 10px; right: 10px; width: 160px; height: 140px;
|
205 |
-
background-color: white; border:2px solid grey; z-index:9999;
|
206 |
-
font-size:14px; padding: 10px">
|
207 |
-
<h4>AQI Legend</h4>
|
208 |
-
<p><i class="fa fa-circle" style="color:#00E400"></i> Good (0-50)</p>
|
209 |
-
<p><i class="fa fa-circle" style="color:#FFFF00"></i> Moderate (51-100)</p>
|
210 |
-
<p><i class="fa fa-circle" style="color:#FF7E00"></i> Unhealthy for Sensitive (101-150)</p>
|
211 |
-
<p><i class="fa fa-circle" style="color:#FF0000"></i> Unhealthy (151-200)</p>
|
212 |
-
<p><i class="fa fa-circle" style="color:#8F3F97"></i> Very Unhealthy (201-300)</p>
|
213 |
-
</div>
|
214 |
-
'''
|
215 |
-
m.get_root().html.add_child(folium.Element(legend_html))
|
216 |
|
217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
218 |
|
219 |
-
|
220 |
-
|
221 |
-
if not data:
|
222 |
-
return None
|
223 |
-
|
224 |
-
df = pd.DataFrame(data)
|
225 |
-
df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce')
|
226 |
-
|
227 |
-
# Create bar chart
|
228 |
-
fig = px.bar(df,
|
229 |
-
x='ReportingArea',
|
230 |
-
y='AQI',
|
231 |
-
color='AQI',
|
232 |
-
color_continuous_scale='RdYlGn_r',
|
233 |
-
title='Air Quality Index by Location',
|
234 |
-
labels={'AQI': 'Air Quality Index', 'ReportingArea': 'Location'})
|
235 |
-
|
236 |
-
fig.update_layout(xaxis_tickangle=-45, height=500)
|
237 |
-
fig.update_traces(hovertemplate='<b>%{x}</b><br>AQI: %{y}<extra></extra>')
|
238 |
-
|
239 |
-
return fig
|
240 |
|
241 |
-
def
|
242 |
-
"""
|
243 |
-
if not
|
244 |
-
return "
|
245 |
-
|
246 |
-
df = pd.DataFrame(data)
|
247 |
-
df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce')
|
248 |
|
249 |
-
|
250 |
-
|
251 |
-
max_aqi = df['AQI'].max()
|
252 |
-
min_aqi = df['AQI'].min()
|
253 |
|
254 |
-
#
|
255 |
-
|
256 |
-
category_counts = categories.value_counts()
|
257 |
-
|
258 |
-
summary = f"""
|
259 |
-
## Air Quality Summary
|
260 |
-
|
261 |
-
**Total Monitoring Stations:** {total_stations}
|
262 |
-
**Average AQI:** {avg_aqi:.1f}
|
263 |
-
**Highest AQI:** {max_aqi}
|
264 |
-
**Lowest AQI:** {min_aqi}
|
265 |
-
|
266 |
-
### Air Quality Categories:
|
267 |
-
"""
|
268 |
-
|
269 |
-
for category, count in category_counts.items():
|
270 |
-
percentage = (count / total_stations) * 100
|
271 |
-
summary += f"- **{category}:** {count} stations ({percentage:.1f}%)\n"
|
272 |
-
|
273 |
-
return summary
|
274 |
-
|
275 |
-
def update_data():
|
276 |
-
"""Main function to fetch and display air quality data"""
|
277 |
-
monitor = AirQualityMonitor()
|
278 |
-
data = monitor.get_current_observations()
|
279 |
|
280 |
-
|
281 |
-
|
282 |
-
summary = get_air_quality_summary(data)
|
283 |
|
284 |
-
return map_html,
|
285 |
|
286 |
# Create Gradio interface
|
287 |
-
with gr.Blocks(title="Air Quality
|
288 |
-
gr.Markdown(
|
289 |
-
|
290 |
-
|
291 |
-
with gr.Row():
|
292 |
-
gr.Markdown("""
|
293 |
-
This application displays real-time air quality data from over 2,000 monitoring stations
|
294 |
-
across the United States. The data includes Air Quality Index (AQI) values for various
|
295 |
-
pollutants like PM2.5, PM10, Ozone, and more.
|
296 |
|
297 |
-
|
298 |
-
1. Get a free API key from [AirNow API](https://docs.airnowapi.org/)
|
299 |
-
2. Replace `YOUR_API_KEY_HERE` in the code with your actual API key
|
300 |
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
|
307 |
with gr.Row():
|
308 |
-
with gr.Column(scale=
|
309 |
-
|
|
|
|
|
|
|
|
|
|
|
310 |
with gr.Column(scale=1):
|
311 |
-
|
312 |
|
313 |
-
|
314 |
-
chart_output = gr.Plot(label="AQI Chart")
|
315 |
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
## About Air Quality Index (AQI)
|
320 |
|
321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
322 |
|
323 |
-
|
324 |
-
- **
|
325 |
-
- **
|
326 |
-
- **
|
327 |
-
- **Very Unhealthy (201-300)**: 🟣 Health alert for everyone
|
328 |
-
- **Hazardous (301+)**: 🟤 Health emergency
|
329 |
|
330 |
-
|
331 |
-
|
|
|
|
|
|
|
|
|
332 |
|
333 |
-
#
|
334 |
-
|
335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
336 |
|
|
|
337 |
if __name__ == "__main__":
|
338 |
-
demo.launch()
|
|
|
1 |
import gradio as gr
|
2 |
import requests
|
|
|
3 |
import folium
|
|
|
4 |
import json
|
5 |
+
import time
|
6 |
+
import os
|
7 |
+
from typing import Dict, List, Optional, Tuple
|
8 |
+
import pandas as pd
|
9 |
|
10 |
+
class AirQualityMapper:
|
11 |
+
"""Class to handle AirNow API interactions and map generation"""
|
12 |
+
|
13 |
def __init__(self):
|
14 |
+
self.base_url = "https://www.airnowapi.org"
|
15 |
+
self.aqi_colors = {
|
16 |
+
"Good": "#00E400",
|
17 |
+
"Moderate": "#FFFF00",
|
18 |
+
"Unhealthy for Sensitive Groups": "#FF7E00",
|
19 |
+
"Unhealthy": "#FF0000",
|
20 |
+
"Very Unhealthy": "#8F3F97",
|
21 |
+
"Hazardous": "#7E0023"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
}
|
23 |
+
self.aqi_ranges = {
|
24 |
+
(0, 50): "Good",
|
25 |
+
(51, 100): "Moderate",
|
26 |
+
(101, 150): "Unhealthy for Sensitive Groups",
|
27 |
+
(151, 200): "Unhealthy",
|
28 |
+
(201, 300): "Very Unhealthy",
|
29 |
+
(301, 500): "Hazardous"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
}
|
|
|
31 |
|
32 |
+
def get_aqi_category(self, aqi_value: int) -> str:
|
33 |
+
"""Get AQI category based on value"""
|
34 |
+
for (min_val, max_val), category in self.aqi_ranges.items():
|
35 |
+
if min_val <= aqi_value <= max_val:
|
36 |
+
return category
|
37 |
+
return "Unknown"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
|
39 |
+
def get_aqi_color(self, category: str) -> str:
|
40 |
+
"""Get color for AQI category"""
|
41 |
+
return self.aqi_colors.get(category, "#808080")
|
|
|
|
|
42 |
|
43 |
+
def fetch_airnow_data(self, api_key: str) -> Tuple[List[Dict], str]:
|
44 |
+
"""
|
45 |
+
Fetch air quality data from AirNow API
|
46 |
+
Returns: (data_list, status_message)
|
47 |
+
"""
|
48 |
+
if not api_key or api_key.strip() == "":
|
49 |
+
return [], "❌ Please enter a valid AirNow API key"
|
50 |
+
|
51 |
+
try:
|
52 |
+
# Get data for major US cities and regions
|
53 |
+
# We'll use a comprehensive list of state capitals and major cities
|
54 |
+
locations = [
|
55 |
+
("90210", "California"), ("10001", "New York"), ("60601", "Illinois"),
|
56 |
+
("75201", "Texas"), ("33101", "Florida"), ("30301", "Georgia"),
|
57 |
+
("98101", "Washington"), ("97201", "Oregon"), ("80201", "Colorado"),
|
58 |
+
("85001", "Arizona"), ("89101", "Nevada"), ("84101", "Utah"),
|
59 |
+
("59601", "Montana"), ("58501", "North Dakota"), ("57501", "South Dakota"),
|
60 |
+
("68501", "Nebraska"), ("66601", "Kansas"), ("73101", "Oklahoma"),
|
61 |
+
("55101", "Minnesota"), ("50301", "Iowa"), ("65101", "Missouri"),
|
62 |
+
("72201", "Arkansas"), ("70801", "Louisiana"), ("39201", "Mississippi"),
|
63 |
+
("35201", "Alabama"), ("37201", "Tennessee"), ("40601", "Kentucky"),
|
64 |
+
("25301", "West Virginia"), ("23219", "Virginia"), ("27601", "North Carolina"),
|
65 |
+
("29201", "South Carolina"), ("32301", "Florida"), ("01501", "Massachusetts"),
|
66 |
+
("06101", "Connecticut"), ("02901", "Rhode Island"), ("03301", "New Hampshire"),
|
67 |
+
("05601", "Vermont"), ("04330", "Maine"), ("19901", "Delaware"),
|
68 |
+
("21201", "Maryland"), ("17101", "Pennsylvania"), ("07001", "New Jersey"),
|
69 |
+
("12201", "New York"), ("43215", "Ohio"), ("46201", "Indiana"),
|
70 |
+
("48601", "Michigan"), ("53201", "Wisconsin"), ("99501", "Alaska"),
|
71 |
+
("96801", "Hawaii")
|
72 |
+
]
|
73 |
+
|
74 |
+
all_data = []
|
75 |
+
|
76 |
+
for zipcode, state in locations:
|
77 |
+
try:
|
78 |
+
# Current observations endpoint
|
79 |
+
url = f"{self.base_url}/aq/observation/zipCode/current/"
|
80 |
+
params = {
|
81 |
+
"format": "application/json",
|
82 |
+
"zipCode": zipcode,
|
83 |
+
"distance": 50, # 50 mile radius
|
84 |
+
"API_KEY": api_key
|
85 |
+
}
|
86 |
+
|
87 |
+
response = requests.get(url, params=params, timeout=10)
|
88 |
+
|
89 |
+
if response.status_code == 200:
|
90 |
+
data = response.json()
|
91 |
+
if data: # If data is not empty
|
92 |
+
for observation in data:
|
93 |
+
observation['source_state'] = state
|
94 |
+
observation['source_zipcode'] = zipcode
|
95 |
+
all_data.extend(data)
|
96 |
+
|
97 |
+
# Add delay to respect rate limits
|
98 |
+
time.sleep(0.5)
|
99 |
+
|
100 |
+
except requests.exceptions.RequestException as e:
|
101 |
+
continue # Skip this location and continue with others
|
102 |
+
|
103 |
+
if not all_data:
|
104 |
+
return [], "⚠️ No air quality data found. Please check your API key or try again later."
|
105 |
+
|
106 |
+
# Remove duplicates based on reporting area
|
107 |
+
seen_areas = set()
|
108 |
+
unique_data = []
|
109 |
+
for item in all_data:
|
110 |
+
area_key = (item.get('ReportingArea', ''), item.get('StateCode', ''))
|
111 |
+
if area_key not in seen_areas:
|
112 |
+
seen_areas.add(area_key)
|
113 |
+
unique_data.append(item)
|
114 |
+
|
115 |
+
return unique_data, f"✅ Successfully loaded {len(unique_data)} monitoring locations"
|
116 |
+
|
117 |
+
except Exception as e:
|
118 |
+
return [], f"❌ Error fetching data: {str(e)}"
|
119 |
|
120 |
+
def create_map(self, data: List[Dict]) -> str:
|
121 |
+
"""Create an interactive map with air quality data"""
|
122 |
+
if not data:
|
123 |
+
# Create a basic US map if no data
|
124 |
+
m = folium.Map(location=[39.8283, -98.5795], zoom_start=4)
|
125 |
+
folium.Marker(
|
126 |
+
[39.8283, -98.5795],
|
127 |
+
popup="No data available. Please check your API key.",
|
128 |
+
icon=folium.Icon(color='red', icon='info-sign')
|
129 |
+
).add_to(m)
|
130 |
+
return m._repr_html_()
|
131 |
+
|
132 |
+
# Calculate center point of all data
|
133 |
+
lats = [item['Latitude'] for item in data if 'Latitude' in item]
|
134 |
+
lons = [item['Longitude'] for item in data if 'Longitude' in item]
|
135 |
|
136 |
+
if lats and lons:
|
137 |
+
center_lat = sum(lats) / len(lats)
|
138 |
+
center_lon = sum(lons) / len(lons)
|
139 |
+
else:
|
140 |
+
center_lat, center_lon = 39.8283, -98.5795 # Center of US
|
141 |
|
142 |
+
# Create map
|
143 |
+
m = folium.Map(location=[center_lat, center_lon], zoom_start=4)
|
144 |
+
|
145 |
+
# Add markers for each monitoring location
|
146 |
+
for item in data:
|
147 |
+
try:
|
148 |
+
lat = item.get('Latitude')
|
149 |
+
lon = item.get('Longitude')
|
150 |
+
aqi = item.get('AQI', 0)
|
151 |
+
parameter = item.get('ParameterName', 'Unknown')
|
152 |
+
area = item.get('ReportingArea', 'Unknown Area')
|
153 |
+
state = item.get('StateCode', 'Unknown')
|
154 |
+
category = item.get('Category', {}).get('Name', self.get_aqi_category(aqi))
|
155 |
+
|
156 |
+
if lat is None or lon is None:
|
157 |
+
continue
|
158 |
+
|
159 |
+
# Get color based on AQI category
|
160 |
+
color = self.get_aqi_color(category)
|
161 |
+
|
162 |
+
# Create popup content
|
163 |
+
popup_content = f"""
|
164 |
+
<div style="width: 200px;">
|
165 |
+
<h4>{area}, {state}</h4>
|
166 |
+
<p><b>AQI:</b> {aqi} ({category})</p>
|
167 |
+
<p><b>Parameter:</b> {parameter}</p>
|
168 |
+
<p><b>Location:</b> {lat:.3f}, {lon:.3f}</p>
|
169 |
+
<p><b>Last Updated:</b> {item.get('DateObserved', 'Unknown')} {item.get('HourObserved', '')}:00</p>
|
170 |
+
</div>
|
171 |
+
"""
|
172 |
+
|
173 |
+
# Determine marker color based on AQI
|
174 |
+
if aqi <= 50:
|
175 |
+
marker_color = 'green'
|
176 |
+
elif aqi <= 100:
|
177 |
+
marker_color = 'yellow'
|
178 |
+
elif aqi <= 150:
|
179 |
+
marker_color = 'orange'
|
180 |
+
elif aqi <= 200:
|
181 |
+
marker_color = 'red'
|
182 |
+
elif aqi <= 300:
|
183 |
+
marker_color = 'purple'
|
184 |
+
else:
|
185 |
+
marker_color = 'darkred'
|
186 |
+
|
187 |
+
# Add marker
|
188 |
+
folium.Marker(
|
189 |
+
[lat, lon],
|
190 |
+
popup=folium.Popup(popup_content, max_width=250),
|
191 |
+
tooltip=f"{area}: AQI {aqi}",
|
192 |
+
icon=folium.Icon(color=marker_color, icon='cloud')
|
193 |
+
).add_to(m)
|
194 |
+
|
195 |
+
except Exception as e:
|
196 |
+
continue # Skip problematic markers
|
197 |
+
|
198 |
+
# Add legend
|
199 |
+
legend_html = """
|
200 |
+
<div style="position: fixed;
|
201 |
+
bottom: 50px; left: 50px; width: 150px; height: 180px;
|
202 |
+
background-color: white; border:2px solid grey; z-index:9999;
|
203 |
+
font-size:14px; padding: 10px">
|
204 |
+
<h4>AQI Legend</h4>
|
205 |
+
<p><i class="fa fa-circle" style="color:green"></i> Good (0-50)</p>
|
206 |
+
<p><i class="fa fa-circle" style="color:yellow"></i> Moderate (51-100)</p>
|
207 |
+
<p><i class="fa fa-circle" style="color:orange"></i> Unhealthy for Sensitive (101-150)</p>
|
208 |
+
<p><i class="fa fa-circle" style="color:red"></i> Unhealthy (151-200)</p>
|
209 |
+
<p><i class="fa fa-circle" style="color:purple"></i> Very Unhealthy (201-300)</p>
|
210 |
+
<p><i class="fa fa-circle" style="color:darkred"></i> Hazardous (301+)</p>
|
211 |
</div>
|
212 |
"""
|
213 |
+
m.get_root().html.add_child(folium.Element(legend_html))
|
214 |
|
215 |
+
return m._repr_html_()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
216 |
|
217 |
+
def create_data_table(self, data: List[Dict]) -> pd.DataFrame:
|
218 |
+
"""Create a data table from the air quality data"""
|
219 |
+
if not data:
|
220 |
+
return pd.DataFrame()
|
221 |
+
|
222 |
+
# Extract relevant columns
|
223 |
+
table_data = []
|
224 |
+
for item in data:
|
225 |
+
table_data.append({
|
226 |
+
'Reporting Area': item.get('ReportingArea', 'Unknown'),
|
227 |
+
'State': item.get('StateCode', 'Unknown'),
|
228 |
+
'AQI': item.get('AQI', 0),
|
229 |
+
'Category': item.get('Category', {}).get('Name', self.get_aqi_category(item.get('AQI', 0))),
|
230 |
+
'Parameter': item.get('ParameterName', 'Unknown'),
|
231 |
+
'Date': item.get('DateObserved', 'Unknown'),
|
232 |
+
'Hour': item.get('HourObserved', 'Unknown'),
|
233 |
+
'Latitude': item.get('Latitude', 'Unknown'),
|
234 |
+
'Longitude': item.get('Longitude', 'Unknown')
|
235 |
+
})
|
236 |
+
|
237 |
+
df = pd.DataFrame(table_data)
|
238 |
+
return df.sort_values('AQI', ascending=False)
|
239 |
|
240 |
+
# Initialize the mapper
|
241 |
+
mapper = AirQualityMapper()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
242 |
|
243 |
+
def update_map(api_key: str):
|
244 |
+
"""Update the map with fresh air quality data"""
|
245 |
+
if not api_key.strip():
|
246 |
+
return "Please enter your AirNow API key above.", pd.DataFrame()
|
|
|
|
|
|
|
247 |
|
248 |
+
# Fetch data
|
249 |
+
data, status = mapper.fetch_airnow_data(api_key)
|
|
|
|
|
250 |
|
251 |
+
# Create map
|
252 |
+
map_html = mapper.create_map(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
253 |
|
254 |
+
# Create data table
|
255 |
+
df = mapper.create_data_table(data)
|
|
|
256 |
|
257 |
+
return map_html, df
|
258 |
|
259 |
# Create Gradio interface
|
260 |
+
with gr.Blocks(title="AirNow Air Quality Sensor Map", theme=gr.themes.Soft()) as demo:
|
261 |
+
gr.Markdown(
|
262 |
+
"""
|
263 |
+
# 🌬️ AirNow Air Quality Sensor Map
|
|
|
|
|
|
|
|
|
|
|
264 |
|
265 |
+
This interactive map displays real-time air quality data from EPA's AirNow network of over 2,000 monitoring stations across the United States.
|
|
|
|
|
266 |
|
267 |
+
## How to use:
|
268 |
+
1. **Get an API Key**: Register for a free API key at [docs.airnowapi.org](https://docs.airnowapi.org/)
|
269 |
+
2. **Enter your API key** in the field below
|
270 |
+
3. **Click "Load Air Quality Data"** to fetch current readings
|
271 |
+
4. **Explore the map**: Click on markers to see detailed information about each monitoring station
|
272 |
+
|
273 |
+
## About the Data:
|
274 |
+
- Data is updated hourly from state, local, tribal, and federal air quality agencies
|
275 |
+
- Colors indicate Air Quality Index (AQI) levels from Good (green) to Hazardous (dark red)
|
276 |
+
- AQI values tell you how clean or polluted the air is and associated health effects
|
277 |
+
|
278 |
+
**⚠️ Note**: This data is preliminary and should not be used for regulatory decisions. For official data, visit [EPA's AirData](https://www.epa.gov/outdoor-air-quality-data).
|
279 |
+
"""
|
280 |
+
)
|
281 |
|
282 |
with gr.Row():
|
283 |
+
with gr.Column(scale=3):
|
284 |
+
api_key_input = gr.Textbox(
|
285 |
+
label="AirNow API Key",
|
286 |
+
placeholder="Enter your AirNow API key here...",
|
287 |
+
type="password",
|
288 |
+
info="Get your free API key at docs.airnowapi.org"
|
289 |
+
)
|
290 |
with gr.Column(scale=1):
|
291 |
+
load_button = gr.Button("Load Air Quality Data", variant="primary", size="lg")
|
292 |
|
293 |
+
status_text = gr.Markdown("Enter your API key and click 'Load Air Quality Data' to begin.")
|
|
|
294 |
|
295 |
+
with gr.Tabs():
|
296 |
+
with gr.TabItem("Interactive Map"):
|
297 |
+
map_output = gr.HTML(label="Air Quality Map", height=600)
|
|
|
298 |
|
299 |
+
with gr.TabItem("Data Table"):
|
300 |
+
data_table = gr.Dataframe(
|
301 |
+
label="Air Quality Monitoring Stations",
|
302 |
+
height=500,
|
303 |
+
interactive=False
|
304 |
+
)
|
305 |
+
|
306 |
+
gr.Markdown(
|
307 |
+
"""
|
308 |
+
## AQI Health Guidelines:
|
309 |
+
|
310 |
+
- **Good (0-50)**: Air quality is satisfactory for everyone
|
311 |
+
- **Moderate (51-100)**: Air quality is acceptable for most people
|
312 |
+
- **Unhealthy for Sensitive Groups (101-150)**: Members of sensitive groups may experience health effects
|
313 |
+
- **Unhealthy (151-200)**: Everyone may begin to experience health effects
|
314 |
+
- **Very Unhealthy (201-300)**: Health warnings of emergency conditions
|
315 |
+
- **Hazardous (301+)**: Health alert - everyone may experience serious health effects
|
316 |
|
317 |
+
## Data Sources:
|
318 |
+
- **AirNow API**: Real-time air quality data from EPA's monitoring network
|
319 |
+
- **Monitoring Agencies**: 120+ local, state, tribal, and federal government agencies
|
320 |
+
- **Update Frequency**: Hourly observations, daily forecasts
|
|
|
|
|
321 |
|
322 |
+
## Links:
|
323 |
+
- [AirNow.gov](https://www.airnow.gov) - Official air quality information
|
324 |
+
- [AirNow API Documentation](https://docs.airnowapi.org/) - API documentation and registration
|
325 |
+
- [EPA AirData](https://www.epa.gov/outdoor-air-quality-data) - Official regulatory air quality data
|
326 |
+
"""
|
327 |
+
)
|
328 |
|
329 |
+
# Set up event handler
|
330 |
+
load_button.click(
|
331 |
+
fn=update_map,
|
332 |
+
inputs=[api_key_input],
|
333 |
+
outputs=[map_output, data_table]
|
334 |
+
).then(
|
335 |
+
fn=lambda: "Map updated with latest air quality data! 🌍",
|
336 |
+
outputs=[status_text]
|
337 |
+
)
|
338 |
|
339 |
+
# Launch the app
|
340 |
if __name__ == "__main__":
|
341 |
+
demo.launch(share=True)
|