Spaces:
Sleeping
Sleeping
Update air_quality_map.py
Browse files- air_quality_map.py +131 -43
air_quality_map.py
CHANGED
@@ -47,8 +47,18 @@ class AirQualityApp:
|
|
47 |
"WI": "55", "WY": "56", "DC": "11"
|
48 |
}
|
49 |
|
50 |
-
# AQI categories with their corresponding colors
|
51 |
self.aqi_categories = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
"Good": "#00e400", # Green
|
53 |
"Moderate": "#ffff00", # Yellow
|
54 |
"Unhealthy for Sensitive Groups": "#ff7e00", # Orange
|
@@ -121,20 +131,29 @@ class AirQualityApp:
|
|
121 |
response = requests.get(endpoint, params=params)
|
122 |
data = response.json()
|
123 |
|
|
|
|
|
|
|
|
|
|
|
124 |
# Handle the specific response structure we observed
|
125 |
if isinstance(data, dict):
|
126 |
if "Data" in data and isinstance(data["Data"], list):
|
127 |
return data["Data"]
|
128 |
elif "Header" in data and isinstance(data["Header"], list):
|
129 |
-
if data["Header"][0].get("status") == "Success":
|
130 |
return data.get("Data", [])
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
|
|
|
|
|
|
|
|
135 |
except Exception as e:
|
136 |
print(f"Error fetching monitors: {e}")
|
137 |
-
return
|
138 |
|
139 |
def get_counties(self, state_code):
|
140 |
"""Fetch counties for a given state"""
|
@@ -239,8 +258,8 @@ class AirQualityApp:
|
|
239 |
"edate": "20240414", # End date (YYYYMMDD) - current date
|
240 |
}
|
241 |
|
242 |
-
|
243 |
-
|
244 |
|
245 |
if parameter_code:
|
246 |
params["param"] = parameter_code
|
@@ -249,19 +268,29 @@ class AirQualityApp:
|
|
249 |
response = requests.get(endpoint, params=params)
|
250 |
data = response.json()
|
251 |
|
|
|
|
|
|
|
|
|
|
|
252 |
# Handle the specific response structure we observed
|
|
|
253 |
if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
|
|
|
|
|
|
258 |
except Exception as e:
|
259 |
print(f"Error fetching AQI data: {e}")
|
260 |
return []
|
261 |
|
262 |
def create_map(self, state_code, county_code=None, parameter_code=None):
|
263 |
"""Create a map with air quality monitoring stations"""
|
264 |
-
|
|
|
265 |
|
266 |
if not monitors:
|
267 |
return "No monitoring stations found for the selected criteria."
|
@@ -269,6 +298,16 @@ class AirQualityApp:
|
|
269 |
# Convert to DataFrame for easier manipulation
|
270 |
df = pd.DataFrame(monitors)
|
271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
272 |
# Create a map centered on the mean latitude and longitude
|
273 |
center_lat = df["latitude"].mean()
|
274 |
center_lon = df["longitude"].mean()
|
@@ -282,12 +321,14 @@ class AirQualityApp:
|
|
282 |
# Get latest AQI data if credentials are provided
|
283 |
aqi_data = {}
|
284 |
if EMAIL and API_KEY:
|
285 |
-
|
|
|
286 |
# Create a lookup dictionary by site ID
|
287 |
for item in aqi_results:
|
288 |
site_id = f"{item['state_code']}-{item['county_code']}-{item['site_number']}"
|
289 |
-
if site_id not in aqi_data
|
290 |
-
aqi_data[site_id] =
|
|
|
291 |
|
292 |
# Add markers for each monitoring station
|
293 |
for _, row in df.iterrows():
|
@@ -295,28 +336,82 @@ class AirQualityApp:
|
|
295 |
|
296 |
# Default marker color is blue
|
297 |
color = "blue"
|
298 |
-
aqi_info = ""
|
299 |
|
300 |
-
#
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
305 |
color = self.aqi_categories.get(aqi_category, "blue")
|
306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
307 |
|
308 |
-
# Create popup content
|
309 |
popup_content = f"""
|
310 |
-
<
|
311 |
-
|
312 |
-
|
313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
314 |
"""
|
315 |
|
|
|
|
|
|
|
316 |
# Add marker to cluster
|
317 |
folium.Marker(
|
318 |
location=[row["latitude"], row["longitude"]],
|
319 |
-
popup=
|
320 |
icon=folium.Icon(color=color, icon="cloud"),
|
321 |
).add_to(marker_cluster)
|
322 |
|
@@ -336,7 +431,7 @@ class AirQualityApp:
|
|
336 |
<div style="display: grid; grid-template-columns: auto 1fr; grid-gap: 5px; align-items: center;">
|
337 |
"""
|
338 |
|
339 |
-
for category, color in self.
|
340 |
legend_html += f'<span style="background-color: {color}; width: 20px; height: 20px; display: inline-block;"></span>'
|
341 |
legend_html += f'<span>{category}</span>'
|
342 |
|
@@ -587,7 +682,7 @@ def create_air_quality_map_ui():
|
|
587 |
def update_counties(state_code):
|
588 |
"""Callback to update counties dropdown when state changes"""
|
589 |
counties = app.get_counties(state_code)
|
590 |
-
return counties
|
591 |
|
592 |
def show_map(state, county=None, parameter=None):
|
593 |
"""Callback to generate and display the map"""
|
@@ -605,14 +700,8 @@ def create_air_quality_map_ui():
|
|
605 |
result = app.create_map(state, county_code, parameter_code)
|
606 |
|
607 |
if isinstance(result, dict):
|
608 |
-
#
|
609 |
-
|
610 |
-
<div>
|
611 |
-
{result["map"]}
|
612 |
-
{result["legend"]}
|
613 |
-
</div>
|
614 |
-
"""
|
615 |
-
return html_content
|
616 |
else:
|
617 |
# Return error message or whatever was returned
|
618 |
return result
|
@@ -623,9 +712,8 @@ def create_air_quality_map_ui():
|
|
623 |
gr.Markdown("""
|
624 |
This application displays air quality monitoring stations in the United States.
|
625 |
|
626 |
-
**Note:** To use the actual EPA AQS API, you need to register for an API key
|
627 |
-
|
628 |
-
and update the EMAIL and API_KEY constants in the code.
|
629 |
|
630 |
For demonstration without an API key, the app shows sample data for California (CA), New York (NY), and Texas (TX).
|
631 |
""")
|
|
|
47 |
"WI": "55", "WY": "56", "DC": "11"
|
48 |
}
|
49 |
|
50 |
+
# AQI categories with their corresponding colors - using only valid Folium icon colors
|
51 |
self.aqi_categories = {
|
52 |
+
"Good": "green",
|
53 |
+
"Moderate": "orange",
|
54 |
+
"Unhealthy for Sensitive Groups": "orange",
|
55 |
+
"Unhealthy": "red",
|
56 |
+
"Very Unhealthy": "purple",
|
57 |
+
"Hazardous": "darkred"
|
58 |
+
}
|
59 |
+
|
60 |
+
# Color mapping for the legend (using original colors for display)
|
61 |
+
self.aqi_legend_colors = {
|
62 |
"Good": "#00e400", # Green
|
63 |
"Moderate": "#ffff00", # Yellow
|
64 |
"Unhealthy for Sensitive Groups": "#ff7e00", # Orange
|
|
|
131 |
response = requests.get(endpoint, params=params)
|
132 |
data = response.json()
|
133 |
|
134 |
+
# Add detailed debugging
|
135 |
+
print(f"API Response Keys: {list(data.keys()) if isinstance(data, dict) else 'Not a dictionary'}")
|
136 |
+
if isinstance(data, dict) and "Header" in data:
|
137 |
+
print(f"Header type: {type(data['Header'])}, content: {data['Header'][:100]}...")
|
138 |
+
|
139 |
# Handle the specific response structure we observed
|
140 |
if isinstance(data, dict):
|
141 |
if "Data" in data and isinstance(data["Data"], list):
|
142 |
return data["Data"]
|
143 |
elif "Header" in data and isinstance(data["Header"], list):
|
144 |
+
if len(data["Header"]) > 0 and data["Header"][0].get("status") == "Success":
|
145 |
return data.get("Data", [])
|
146 |
+
else:
|
147 |
+
print(f"Header does not contain success status: {data['Header']}")
|
148 |
+
# Special case - return mock data if we can't parse the API response
|
149 |
+
print(f"Using mock data instead of API response for state {state_code}")
|
150 |
+
return self.mock_get_monitors(state_code, county_code, parameter_code)
|
151 |
+
else:
|
152 |
+
print(f"Unexpected response format for monitors: {type(data)}")
|
153 |
+
return self.mock_get_monitors(state_code, county_code, parameter_code)
|
154 |
except Exception as e:
|
155 |
print(f"Error fetching monitors: {e}")
|
156 |
+
return self.mock_get_monitors(state_code, county_code, parameter_code)
|
157 |
|
158 |
def get_counties(self, state_code):
|
159 |
"""Fetch counties for a given state"""
|
|
|
258 |
"edate": "20240414", # End date (YYYYMMDD) - current date
|
259 |
}
|
260 |
|
261 |
+
# The county parameter might not be supported here either
|
262 |
+
# We'll filter results by county after getting them
|
263 |
|
264 |
if parameter_code:
|
265 |
params["param"] = parameter_code
|
|
|
268 |
response = requests.get(endpoint, params=params)
|
269 |
data = response.json()
|
270 |
|
271 |
+
# Add detailed debugging
|
272 |
+
print(f"AQI API Response Keys: {list(data.keys()) if isinstance(data, dict) else 'Not a dictionary'}")
|
273 |
+
if isinstance(data, dict) and "Header" in data:
|
274 |
+
print(f"AQI Header type: {type(data['Header'])}, content: {data['Header'][:100]}...")
|
275 |
+
|
276 |
# Handle the specific response structure we observed
|
277 |
+
aqi_data = []
|
278 |
if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
|
279 |
+
aqi_data = data["Data"]
|
280 |
+
|
281 |
+
# Filter by county if provided
|
282 |
+
if county_code and aqi_data:
|
283 |
+
aqi_data = [item for item in aqi_data if item.get('county_code') == county_code]
|
284 |
+
|
285 |
+
return aqi_data
|
286 |
except Exception as e:
|
287 |
print(f"Error fetching AQI data: {e}")
|
288 |
return []
|
289 |
|
290 |
def create_map(self, state_code, county_code=None, parameter_code=None):
|
291 |
"""Create a map with air quality monitoring stations"""
|
292 |
+
# IMPORTANT: We don't pass county_code to get_monitors anymore since the API doesn't support it
|
293 |
+
monitors = self.get_monitors(state_code, parameter_code=parameter_code)
|
294 |
|
295 |
if not monitors:
|
296 |
return "No monitoring stations found for the selected criteria."
|
|
|
298 |
# Convert to DataFrame for easier manipulation
|
299 |
df = pd.DataFrame(monitors)
|
300 |
|
301 |
+
# Now filter by county if provided - do this AFTER getting the monitors
|
302 |
+
if county_code:
|
303 |
+
print(f"Filtering by county_code: {county_code}")
|
304 |
+
county_code_str = str(county_code)
|
305 |
+
df = df[df['county_code'].astype(str) == county_code_str]
|
306 |
+
print(f"After filtering, {len(df)} monitors remain")
|
307 |
+
|
308 |
+
if len(df) == 0:
|
309 |
+
return "No monitoring stations found for the selected county."
|
310 |
+
|
311 |
# Create a map centered on the mean latitude and longitude
|
312 |
center_lat = df["latitude"].mean()
|
313 |
center_lon = df["longitude"].mean()
|
|
|
321 |
# Get latest AQI data if credentials are provided
|
322 |
aqi_data = {}
|
323 |
if EMAIL and API_KEY:
|
324 |
+
# Again, don't pass county_code to API
|
325 |
+
aqi_results = self.get_latest_aqi(state_code, parameter_code=parameter_code)
|
326 |
# Create a lookup dictionary by site ID
|
327 |
for item in aqi_results:
|
328 |
site_id = f"{item['state_code']}-{item['county_code']}-{item['site_number']}"
|
329 |
+
if site_id not in aqi_data:
|
330 |
+
aqi_data[site_id] = []
|
331 |
+
aqi_data[site_id].append(item)
|
332 |
|
333 |
# Add markers for each monitoring station
|
334 |
for _, row in df.iterrows():
|
|
|
336 |
|
337 |
# Default marker color is blue
|
338 |
color = "blue"
|
|
|
339 |
|
340 |
+
# Get AQI data for this station if available
|
341 |
+
station_aqi_data = aqi_data.get(site_id, [])
|
342 |
+
latest_aqi = None
|
343 |
+
aqi_category = None
|
344 |
+
|
345 |
+
# Create a table of pollutant readings if available
|
346 |
+
aqi_readings_html = ""
|
347 |
+
|
348 |
+
if station_aqi_data:
|
349 |
+
# Sort by date (most recent first)
|
350 |
+
station_aqi_data.sort(key=lambda x: x.get('date_local', ''), reverse=True)
|
351 |
+
|
352 |
+
# Get latest AQI for marker color
|
353 |
+
if station_aqi_data[0].get('aqi'):
|
354 |
+
latest_aqi = station_aqi_data[0].get('aqi')
|
355 |
+
aqi_category = self.get_aqi_category(latest_aqi)
|
356 |
color = self.aqi_categories.get(aqi_category, "blue")
|
357 |
+
|
358 |
+
# Create a table of readings
|
359 |
+
aqi_readings_html = """
|
360 |
+
<h4>Recent Air Quality Readings</h4>
|
361 |
+
<table style="width:100%; border-collapse: collapse; margin-top: 10px;">
|
362 |
+
<tr style="background-color: #f2f2f2;">
|
363 |
+
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Date</th>
|
364 |
+
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Pollutant</th>
|
365 |
+
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">AQI</th>
|
366 |
+
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Category</th>
|
367 |
+
</tr>
|
368 |
+
"""
|
369 |
+
|
370 |
+
# Add up to 10 most recent readings
|
371 |
+
for i, reading in enumerate(station_aqi_data[:10]):
|
372 |
+
date = reading.get('date_local', 'N/A')
|
373 |
+
pollutant = reading.get('parameter_name', 'N/A')
|
374 |
+
aqi_value = reading.get('aqi', 'N/A')
|
375 |
+
category = self.get_aqi_category(aqi_value) if aqi_value and aqi_value != 'N/A' else 'N/A'
|
376 |
+
|
377 |
+
row_style = ' style="background-color: #f2f2f2;"' if i % 2 == 0 else ''
|
378 |
+
aqi_readings_html += f"""
|
379 |
+
<tr{row_style}>
|
380 |
+
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{date}</td>
|
381 |
+
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{pollutant}</td>
|
382 |
+
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{aqi_value}</td>
|
383 |
+
<td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{category}</td>
|
384 |
+
</tr>
|
385 |
+
"""
|
386 |
+
|
387 |
+
aqi_readings_html += "</table>"
|
388 |
+
|
389 |
+
# If there are more readings than what we showed
|
390 |
+
if len(station_aqi_data) > 10:
|
391 |
+
aqi_readings_html += f"<p><em>Showing 10 of {len(station_aqi_data)} readings</em></p>"
|
392 |
|
393 |
+
# Create popup content with detailed information
|
394 |
popup_content = f"""
|
395 |
+
<div style="min-width: 300px;">
|
396 |
+
<h3>{row['local_site_name']}</h3>
|
397 |
+
<p><strong>Site ID:</strong> {site_id}</p>
|
398 |
+
<p><strong>Address:</strong> {row.get('address', 'N/A')}</p>
|
399 |
+
<p><strong>City:</strong> {row.get('city_name', 'N/A')}</p>
|
400 |
+
<p><strong>County:</strong> {row.get('county_name', 'N/A')}</p>
|
401 |
+
<p><strong>State:</strong> {row.get('state_name', 'N/A')}</p>
|
402 |
+
<p><strong>Parameter:</strong> {row['parameter_name']}</p>
|
403 |
+
<p><strong>Coordinates:</strong> {row['latitude']}, {row['longitude']}</p>
|
404 |
+
{aqi_readings_html}
|
405 |
+
</div>
|
406 |
"""
|
407 |
|
408 |
+
# Create a larger popup for detailed data
|
409 |
+
popup = folium.Popup(popup_content, max_width=500)
|
410 |
+
|
411 |
# Add marker to cluster
|
412 |
folium.Marker(
|
413 |
location=[row["latitude"], row["longitude"]],
|
414 |
+
popup=popup,
|
415 |
icon=folium.Icon(color=color, icon="cloud"),
|
416 |
).add_to(marker_cluster)
|
417 |
|
|
|
431 |
<div style="display: grid; grid-template-columns: auto 1fr; grid-gap: 5px; align-items: center;">
|
432 |
"""
|
433 |
|
434 |
+
for category, color in self.aqi_legend_colors.items():
|
435 |
legend_html += f'<span style="background-color: {color}; width: 20px; height: 20px; display: inline-block;"></span>'
|
436 |
legend_html += f'<span>{category}</span>'
|
437 |
|
|
|
682 |
def update_counties(state_code):
|
683 |
"""Callback to update counties dropdown when state changes"""
|
684 |
counties = app.get_counties(state_code)
|
685 |
+
return gr.Dropdown(choices=counties)
|
686 |
|
687 |
def show_map(state, county=None, parameter=None):
|
688 |
"""Callback to generate and display the map"""
|
|
|
700 |
result = app.create_map(state, county_code, parameter_code)
|
701 |
|
702 |
if isinstance(result, dict):
|
703 |
+
# Return the combined HTML
|
704 |
+
return result["map"]
|
|
|
|
|
|
|
|
|
|
|
|
|
705 |
else:
|
706 |
# Return error message or whatever was returned
|
707 |
return result
|
|
|
712 |
gr.Markdown("""
|
713 |
This application displays air quality monitoring stations in the United States.
|
714 |
|
715 |
+
**Note:** To use the actual EPA AQS API, you need to register for an API key and set
|
716 |
+
`EPA_AQS_EMAIL` and `EPA_AQS_API_KEY` environment variables in your Hugging Face Space.
|
|
|
717 |
|
718 |
For demonstration without an API key, the app shows sample data for California (CA), New York (NY), and Texas (TX).
|
719 |
""")
|