nakas commited on
Commit
fc10f59
·
verified ·
1 Parent(s): 0d26a21

Update air_quality_map.py

Browse files
Files changed (1) hide show
  1. 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
- # If we couldn't parse the response format, return empty list
133
- print(f"Unexpected response format for monitors: {type(data)}")
134
- return []
 
 
 
 
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
- if county_code:
243
- params["county"] = county_code
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
- return data["Data"]
255
- else:
256
- print(f"Unexpected response format for AQI data: {type(data)}")
257
- return []
 
 
 
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
- monitors = self.get_monitors(state_code, county_code, parameter_code)
 
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
- aqi_results = self.get_latest_aqi(state_code, county_code, parameter_code)
 
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 or item['date_local'] > aqi_data[site_id]['date_local']:
290
- aqi_data[site_id] = item
 
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
- # If we have AQI data for this monitor, set the color based on AQI category
301
- if site_id in aqi_data:
302
- aqi_value = aqi_data[site_id].get('aqi', 0)
303
- if aqi_value:
304
- aqi_category = self.get_aqi_category(aqi_value)
 
 
 
 
 
 
 
 
 
 
 
305
  color = self.aqi_categories.get(aqi_category, "blue")
306
- aqi_info = f"<br>Latest AQI: {aqi_value} ({aqi_category})"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
- # Create popup content
309
  popup_content = f"""
310
- <b>{row['local_site_name']}</b><br>
311
- Parameter: {row['parameter_name']}<br>
312
- Site ID: {site_id}<br>
313
- Latitude: {row['latitude']}, Longitude: {row['longitude']}{aqi_info}
 
 
 
 
 
 
 
314
  """
315
 
 
 
 
316
  # Add marker to cluster
317
  folium.Marker(
318
  location=[row["latitude"], row["longitude"]],
319
- popup=folium.Popup(popup_content, max_width=300),
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.aqi_categories.items():
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
- # Combine map and legend HTML
609
- html_content = f"""
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 at
627
- [https://aqs.epa.gov/aqsweb/documents/data_api.html](https://aqs.epa.gov/aqsweb/documents/data_api.html)
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
  """)