nakas commited on
Commit
3e6675e
·
verified ·
1 Parent(s): 9f0a0b1

Update air_quality_map.py

Browse files
Files changed (1) hide show
  1. air_quality_map.py +486 -297
air_quality_map.py CHANGED
@@ -6,7 +6,6 @@ from folium.plugins import MarkerCluster
6
  import tempfile
7
  import os
8
  import json
9
- from branca.element import Figure, JavascriptLink, MacroElement
10
 
11
  # Get API credentials from environment variables
12
  EPA_AQS_API_BASE_URL = "https://aqs.epa.gov/data/api"
@@ -48,9 +47,6 @@ class AirQualityApp:
48
  "WI": "55", "WY": "56", "DC": "11"
49
  }
50
 
51
- # Reverse mapping from numeric to two-letter state codes
52
- self.numeric_to_state_code = {v: k for k, v in self.state_code_mapping.items()}
53
-
54
  # AQI categories with their corresponding colors - using only valid Folium icon colors
55
  self.aqi_categories = {
56
  "Good": "green",
@@ -71,6 +67,28 @@ class AirQualityApp:
71
  "Hazardous": "#7e0023" # Maroon
72
  }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  # Sample parameters for demo
75
  self.mock_parameters = [
76
  {"code": "88101", "value_represented": "PM2.5 - Local Conditions"},
@@ -80,122 +98,104 @@ class AirQualityApp:
80
  {"code": "42602", "value_represented": "Nitrogen dioxide"},
81
  {"code": "81102", "value_represented": "PM10 - Local Conditions"}
82
  ]
 
 
 
 
 
 
83
 
84
- # All monitors data cache
85
- self.all_monitors_cache = {}
 
 
 
 
 
86
 
87
- def get_monitors_by_coordinates(self, min_lat, max_lat, min_lon, max_lon, parameter_code=None):
88
- """Fetch monitoring stations based on coordinate bounds"""
 
 
 
 
 
89
 
90
- # For demo/development without API credentials, use mock data
91
- if not EMAIL or not API_KEY:
92
- return self.mock_get_monitors_by_coordinates(min_lat, max_lat, min_lon, max_lon, parameter_code)
93
-
94
- # Determine which states are in the bounding box
95
- # This requires a more complex spatial algorithm in real implementation
96
- # For simplicity, we'll use a predefined mapping based on state centers
97
- states_in_bounds = self.get_states_in_bounds(min_lat, max_lat, min_lon, max_lon)
98
-
99
- all_monitors = []
100
- for state_code in states_in_bounds:
101
- # Convert state code to numeric format for API
102
- api_state_code = state_code
103
- if len(state_code) == 2 and state_code in self.state_code_mapping:
104
- api_state_code = self.state_code_mapping[state_code]
105
-
106
- # Check if we have cached data for this state
107
- cache_key = f"{api_state_code}_{parameter_code or 'all'}"
108
- if cache_key in self.all_monitors_cache:
109
- monitors = self.all_monitors_cache[cache_key]
110
- else:
111
- # API endpoint for monitoring sites
112
- endpoint = f"{EPA_AQS_API_BASE_URL}/monitors/byState"
113
-
114
- params = {
115
- "email": EMAIL,
116
- "key": API_KEY,
117
- "state": api_state_code,
118
- "bdate": "20240101", # Beginning date (YYYYMMDD)
119
- "edate": "20240414", # End date (YYYYMMDD)
120
- }
121
-
122
- if parameter_code:
123
- params["param"] = parameter_code
124
-
125
- try:
126
- response = requests.get(endpoint, params=params)
127
- data = response.json()
128
-
129
- # Handle the specific response structure
130
- if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
131
- monitors = data["Data"]
132
- elif isinstance(data, dict) and "Header" in data and isinstance(data["Header"], list):
133
- if len(data["Header"]) > 0 and data["Header"][0].get("status") == "Success":
134
- monitors = data.get("Data", [])
135
- else:
136
- print(f"Header does not contain success status: {data['Header']}")
137
- monitors = []
138
- else:
139
- print(f"Unexpected response format for monitors")
140
- monitors = []
141
-
142
- # Cache this data for future use
143
- self.all_monitors_cache[cache_key] = monitors
144
- except Exception as e:
145
- print(f"Error fetching monitors for {state_code}: {e}")
146
- monitors = []
147
 
148
- # Filter monitors based on the coordinate bounds
149
- bounds_filtered_monitors = []
150
- for monitor in monitors:
151
- lat = float(monitor.get("latitude", 0))
152
- lon = float(monitor.get("longitude", 0))
153
- if min_lat <= lat <= max_lat and min_lon <= lon <= max_lon:
154
- bounds_filtered_monitors.append(monitor)
155
 
156
- all_monitors.extend(bounds_filtered_monitors)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- return all_monitors
159
-
160
- def get_states_in_bounds(self, min_lat, max_lat, min_lon, max_lon):
161
- """
162
- Determine which states have area within the coordinate bounds.
163
- This is a simplified approach and would need a more sophisticated spatial algorithm
164
- for accurate results in a production environment.
165
- """
166
- # Approximate state centers (simplified for demo purposes)
167
- state_centers = {
168
- "AL": (32.8067, -86.7911), "AK": (64.2008, -149.4937), "AZ": (34.0489, -111.0937),
169
- "AR": (34.9513, -92.3809), "CA": (36.7783, -119.4179), "CO": (39.5501, -105.7821),
170
- "CT": (41.6032, -73.0877), "DE": (38.9108, -75.5277), "FL": (27.9944, -81.7603),
171
- "GA": (32.1656, -82.9001), "HI": (19.8968, -155.5828), "ID": (44.0682, -114.7420),
172
- "IL": (40.6331, -89.3985), "IN": (40.2672, -86.1349), "IA": (41.8780, -93.0977),
173
- "KS": (39.0119, -98.4842), "KY": (37.8393, -84.2700), "LA": (30.9843, -91.9623),
174
- "ME": (45.2538, -69.4455), "MD": (39.0458, -76.6413), "MA": (42.4072, -71.3824),
175
- "MI": (44.3148, -85.6024), "MN": (46.7296, -94.6859), "MS": (32.7546, -89.6783),
176
- "MO": (38.5767, -92.1735), "MT": (46.8797, -110.3626), "NE": (41.4925, -99.9018),
177
- "NV": (38.8026, -116.4194), "NH": (43.1939, -71.5724), "NJ": (40.0583, -74.4057),
178
- "NM": (34.5199, -105.8701), "NY": (43.0000, -75.0000), "NC": (35.7596, -79.0193),
179
- "ND": (47.5515, -101.0020), "OH": (40.4173, -82.9071), "OK": (35.4676, -97.5164),
180
- "OR": (44.5720, -122.0709), "PA": (40.5908, -77.2098), "RI": (41.6809, -71.5118),
181
- "SC": (33.8361, -81.1637), "SD": (44.3668, -100.3538), "TN": (35.7478, -86.6923),
182
- "TX": (31.0545, -97.5635), "UT": (39.3210, -111.0937), "VT": (44.5588, -72.5778),
183
- "VA": (37.4316, -78.6569), "WA": (47.7511, -120.7401), "WV": (38.4912, -80.9545),
184
- "WI": (43.7844, -88.7879), "WY": (43.0759, -107.2903), "DC": (38.9072, -77.0369)
185
- }
186
 
187
- # Buffer size in degrees to account for state size
188
- buffer = 2.0
189
 
190
- # Find states with centers within or near the bounds
191
- states_in_bounds = []
192
- for state_code, (lat, lon) in state_centers.items():
193
- # Check if state center is close to bounds (including buffer)
194
- if (min_lat - buffer) <= lat <= (max_lat + buffer) and (min_lon - buffer) <= lon <= (max_lon + buffer):
195
- states_in_bounds.append(state_code)
196
 
197
- return states_in_bounds
198
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  def get_parameters(self):
200
  """Fetch available parameter codes (pollutants)"""
201
  # If we don't have API credentials, use mock data
@@ -237,7 +237,7 @@ class AirQualityApp:
237
  print(f"Error fetching parameters: {e}")
238
  return []
239
 
240
- def get_latest_aqi(self, state_code, parameter_code=None):
241
  """Fetch the latest AQI data for monitors"""
242
  # If we don't have API credentials, use mock data
243
  if not EMAIL or not API_KEY:
@@ -257,6 +257,9 @@ class AirQualityApp:
257
  "bdate": "20240314", # Beginning date (YYYYMMDD) - last 30 days
258
  "edate": "20240414", # End date (YYYYMMDD) - current date
259
  }
 
 
 
260
 
261
  if parameter_code:
262
  params["param"] = parameter_code
@@ -265,52 +268,61 @@ class AirQualityApp:
265
  response = requests.get(endpoint, params=params)
266
  data = response.json()
267
 
 
 
 
 
 
268
  # Handle the specific response structure we observed
269
  aqi_data = []
270
  if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
271
  aqi_data = data["Data"]
 
 
 
 
272
 
273
  return aqi_data
274
  except Exception as e:
275
  print(f"Error fetching AQI data: {e}")
276
  return []
277
 
278
- def create_map(self, center_lat=39.8, center_lon=-98.5, zoom_start=4, parameter_code=None):
279
- """
280
- Create a map centered on the United States with auto-loading functionality
 
281
 
282
- Args:
283
- center_lat: Initial center latitude (default: center of US)
284
- center_lon: Initial center longitude (default: center of US)
285
- zoom_start: Initial zoom level (default: 4, showing most of US)
286
- parameter_code: Optional parameter code to filter by
287
- """
288
- # Create a map with a specific width and height
289
- m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom_start, width='100%', height=700)
290
 
291
- # Add custom JavaScript to handle map events and load data
292
- BoundEventHandler = self.create_bound_event_handler()
293
- m.add_child(BoundEventHandler())
294
 
295
- # We'll start with some initial data to show (e.g., for the entire US view)
296
- # For a real implementation, this might be just the major monitoring stations
297
- # For this demo, we'll use California as an example
298
- if EMAIL and API_KEY:
299
- # For real API, we'd calculate bounds and get actual monitors
300
- min_lat, max_lat = 32.0, 42.0 # Approximate bounds for California
301
- min_lon, max_lon = -125.0, -114.0
302
- initial_monitors = self.get_monitors_by_coordinates(min_lat, max_lat, min_lon, max_lon, parameter_code)
303
- else:
304
- # For demo without API, use mock data for California
305
- initial_monitors = self.mock_get_monitors_by_coordinates(32.0, 42.0, -125.0, -114.0, parameter_code)
 
 
 
 
 
306
 
307
- # Add marker cluster for the initial set
308
  marker_cluster = MarkerCluster().add_to(m)
309
 
310
- # Get AQI data for California as an example
311
  aqi_data = {}
312
  if EMAIL and API_KEY:
313
- aqi_results = self.get_latest_aqi("CA", parameter_code)
 
314
  # Create a lookup dictionary by site ID
315
  for item in aqi_results:
316
  site_id = f"{item['state_code']}-{item['county_code']}-{item['site_number']}"
@@ -318,23 +330,6 @@ class AirQualityApp:
318
  aqi_data[site_id] = []
319
  aqi_data[site_id].append(item)
320
 
321
- # Add markers for each initial monitoring station
322
- self.add_markers_to_cluster(marker_cluster, initial_monitors, aqi_data)
323
-
324
- # Return map HTML and legend HTML separately
325
- map_html = m._repr_html_()
326
-
327
- # Create legend HTML outside the map
328
- legend_html = self.create_legend_html()
329
-
330
- return {"map": map_html, "legend": legend_html}
331
-
332
- def add_markers_to_cluster(self, marker_cluster, monitors, aqi_data):
333
- """Add markers for the given monitors to the marker cluster"""
334
- df = pd.DataFrame(monitors)
335
- if df.empty:
336
- return
337
-
338
  # Add markers for each monitoring station
339
  for _, row in df.iterrows():
340
  site_id = f"{row['state_code']}-{row['county_code']}-{row['site_number']}"
@@ -398,14 +393,14 @@ class AirQualityApp:
398
  # Create popup content with detailed information
399
  popup_content = f"""
400
  <div style="min-width: 300px;">
401
- <h3>{row.get('local_site_name', 'Unknown Site')}</h3>
402
  <p><strong>Site ID:</strong> {site_id}</p>
403
  <p><strong>Address:</strong> {row.get('address', 'N/A')}</p>
404
  <p><strong>City:</strong> {row.get('city_name', 'N/A')}</p>
405
  <p><strong>County:</strong> {row.get('county_name', 'N/A')}</p>
406
  <p><strong>State:</strong> {row.get('state_name', 'N/A')}</p>
407
- <p><strong>Parameter:</strong> {row.get('parameter_name', 'N/A')}</p>
408
- <p><strong>Coordinates:</strong> {row.get('latitude', 'N/A')}, {row.get('longitude', 'N/A')}</p>
409
  {aqi_readings_html}
410
  </div>
411
  """
@@ -419,6 +414,14 @@ class AirQualityApp:
419
  popup=popup,
420
  icon=folium.Icon(color=color, icon="cloud"),
421
  ).add_to(marker_cluster)
 
 
 
 
 
 
 
 
422
 
423
  def create_legend_html(self):
424
  """Create the HTML for the AQI legend"""
@@ -440,147 +443,333 @@ class AirQualityApp:
440
 
441
  def get_aqi_category(self, aqi_value):
442
  """Determine AQI category based on value"""
443
- try:
444
- aqi = int(aqi_value)
445
- if aqi <= 50:
446
- return "Good"
447
- elif aqi <= 100:
448
- return "Moderate"
449
- elif aqi <= 150:
450
- return "Unhealthy for Sensitive Groups"
451
- elif aqi <= 200:
452
- return "Unhealthy"
453
- elif aqi <= 300:
454
- return "Very Unhealthy"
455
- else:
456
- return "Hazardous"
457
- except (ValueError, TypeError):
458
- return "Unknown"
 
 
 
 
 
 
 
 
 
 
 
459
 
460
  def mock_get_parameters(self):
461
  """Return mock parameter data"""
462
  return [f"{p['code']}: {p['value_represented']}" for p in self.mock_parameters]
463
 
464
- def mock_get_monitors_by_coordinates(self, min_lat, max_lat, min_lon, max_lon, parameter_code=None):
465
- """
466
- Mock function to return sample data for development,
467
- filtering by coordinate bounds
468
- """
469
- # Sample monitors data for different regions
470
- all_monitors = []
471
-
472
- # California monitors (West Coast)
473
- ca_monitors = [
474
- {
475
- "state_code": "06",
476
- "county_code": "037",
477
- "site_number": "0001",
478
- "parameter_code": "88101",
479
- "parameter_name": "PM2.5 - Local Conditions",
480
- "poc": 1,
481
- "latitude": 34.0667,
482
- "longitude": -118.2275,
483
- "local_site_name": "Los Angeles - North Main Street",
484
- "address": "1630 North Main Street",
485
- "city_name": "Los Angeles",
486
- "county_name": "Los Angeles",
487
- "state_name": "California",
488
- "cbsa_name": "Los Angeles-Long Beach-Anaheim",
489
- "date_established": "1998-01-01",
490
- "last_sample_date": "2024-04-10"
491
- },
492
- {
493
- "state_code": "06",
494
- "county_code": "037",
495
- "site_number": "0002",
496
- "parameter_code": "44201",
497
- "parameter_name": "Ozone",
498
- "poc": 1,
499
- "latitude": 34.0667,
500
- "longitude": -118.2275,
501
- "local_site_name": "Los Angeles - North Main Street",
502
- "address": "1630 North Main Street",
503
- "city_name": "Los Angeles",
504
- "county_name": "Los Angeles",
505
- "state_name": "California",
506
- "cbsa_name": "Los Angeles-Long Beach-Anaheim",
507
- "date_established": "1998-01-01",
508
- "last_sample_date": "2024-04-10"
509
- },
510
- {
511
- "state_code": "06",
512
- "county_code": "067",
513
- "site_number": "0010",
514
- "parameter_code": "88101",
515
- "parameter_name": "PM2.5 - Local Conditions",
516
- "poc": 1,
517
- "latitude": 38.5661,
518
- "longitude": -121.4926,
519
- "local_site_name": "Sacramento - T Street",
520
- "address": "1309 T Street",
521
- "city_name": "Sacramento",
522
- "county_name": "Sacramento",
523
- "state_name": "California",
524
- "cbsa_name": "Sacramento-Roseville",
525
- "date_established": "1999-03-01",
526
- "last_sample_date": "2024-04-10"
527
- },
528
- {
529
- "state_code": "06",
530
- "county_code": "073",
531
- "site_number": "0005",
532
- "parameter_code": "88101",
533
- "parameter_name": "PM2.5 - Local Conditions",
534
- "poc": 1,
535
- "latitude": 32.7333,
536
- "longitude": -117.1500,
537
- "local_site_name": "San Diego - Beardsley Street",
538
- "address": "1110 Beardsley Street",
539
- "city_name": "San Diego",
540
- "county_name": "San Diego",
541
- "state_name": "California",
542
- "cbsa_name": "San Diego-Carlsbad",
543
- "date_established": "1999-04-15",
544
- "last_sample_date": "2024-04-10"
545
  }
546
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
 
548
- # New York monitors (East Coast)
549
- ny_monitors = [
550
- {
551
- "state_code": "36",
552
- "county_code": "061",
553
- "site_number": "0010",
554
- "parameter_code": "88101",
555
- "parameter_name": "PM2.5 - Local Conditions",
556
- "poc": 1,
557
- "latitude": 40.7159,
558
- "longitude": -73.9876,
559
- "local_site_name": "New York - PS 59",
560
- "address": "228 East 57th Street",
561
- "city_name": "New York",
562
- "county_name": "New York",
563
- "state_name": "New York",
564
- "cbsa_name": "New York-Newark-Jersey City",
565
- "date_established": "1999-07-15",
566
- "last_sample_date": "2024-04-10"
567
- },
568
- {
569
- "state_code": "36",
570
- "county_code": "061",
571
- "site_number": "0079",
572
- "parameter_code": "44201",
573
- "parameter_name": "Ozone",
574
- "poc": 1,
575
- "latitude": 40.8160,
576
- "longitude": -73.9510,
577
- "local_site_name": "New York - IS 52",
578
- "address": "681 Kelly Street",
579
- "city_name": "Bronx",
580
- "county_name": "Bronx",
581
- "state_name": "New York",
582
- "cbsa_name": "New York-Newark-Jersey City",
583
- "date_established": "1998-01-01",
584
- "last_sample_date": "2024-04-10"
585
- }
586
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import tempfile
7
  import os
8
  import json
 
9
 
10
  # Get API credentials from environment variables
11
  EPA_AQS_API_BASE_URL = "https://aqs.epa.gov/data/api"
 
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",
 
67
  "Hazardous": "#7e0023" # Maroon
68
  }
69
 
70
+ # Sample county data for demo
71
+ self.mock_counties = {
72
+ "CA": [
73
+ {"code": "037", "value": "Los Angeles"},
74
+ {"code": "067", "value": "Sacramento"},
75
+ {"code": "073", "value": "San Diego"},
76
+ {"code": "075", "value": "San Francisco"}
77
+ ],
78
+ "NY": [
79
+ {"code": "061", "value": "New York"},
80
+ {"code": "047", "value": "Kings (Brooklyn)"},
81
+ {"code": "081", "value": "Queens"},
82
+ {"code": "005", "value": "Bronx"}
83
+ ],
84
+ "TX": [
85
+ {"code": "201", "value": "Harris (Houston)"},
86
+ {"code": "113", "value": "Dallas"},
87
+ {"code": "029", "value": "Bexar (San Antonio)"},
88
+ {"code": "453", "value": "Travis (Austin)"}
89
+ ]
90
+ }
91
+
92
  # Sample parameters for demo
93
  self.mock_parameters = [
94
  {"code": "88101", "value_represented": "PM2.5 - Local Conditions"},
 
98
  {"code": "42602", "value_represented": "Nitrogen dioxide"},
99
  {"code": "81102", "value_represented": "PM10 - Local Conditions"}
100
  ]
101
+
102
+ def get_monitors(self, state_code, county_code=None, parameter_code=None):
103
+ """Fetch monitoring stations for a given state and optional county"""
104
+ # If we don't have API credentials, use mock data
105
+ if not EMAIL or not API_KEY:
106
+ return self.mock_get_monitors(state_code, county_code, parameter_code)
107
 
108
+ # Convert state code to numeric format for API
109
+ api_state_code = state_code
110
+ if len(state_code) == 2 and state_code in self.state_code_mapping:
111
+ api_state_code = self.state_code_mapping[state_code]
112
+
113
+ # API endpoint for monitoring sites
114
+ endpoint = f"{EPA_AQS_API_BASE_URL}/monitors/byState"
115
 
116
+ params = {
117
+ "email": EMAIL,
118
+ "key": API_KEY,
119
+ "state": api_state_code,
120
+ "bdate": "20240101", # Beginning date (YYYYMMDD)
121
+ "edate": "20240414", # End date (YYYYMMDD)
122
+ }
123
 
124
+ if county_code:
125
+ params["county"] = county_code
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
+ if parameter_code:
128
+ params["param"] = parameter_code
129
+
130
+ try:
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"""
160
+ # If we don't have API credentials, use mock data
161
+ if not EMAIL or not API_KEY:
162
+ return self.mock_get_counties(state_code)
163
 
164
+ # Convert state code to numeric format for API
165
+ api_state_code = state_code
166
+ if len(state_code) == 2 and state_code in self.state_code_mapping:
167
+ api_state_code = self.state_code_mapping[state_code]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ endpoint = f"{EPA_AQS_API_BASE_URL}/list/countiesByState"
 
170
 
171
+ params = {
172
+ "email": EMAIL,
173
+ "key": API_KEY,
174
+ "state": api_state_code
175
+ }
 
176
 
177
+ try:
178
+ response = requests.get(endpoint, params=params)
179
+ data = response.json()
180
+
181
+ # Handle the specific response structure we observed
182
+ counties = []
183
+ if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
184
+ counties = data["Data"]
185
+
186
+ # Format as "code: name" for dropdown
187
+ result = []
188
+ for c in counties:
189
+ code = c.get("code")
190
+ value = c.get("value_represented")
191
+ if code and value:
192
+ result.append(f"{code}: {value}")
193
+
194
+ return result
195
+ except Exception as e:
196
+ print(f"Error fetching counties: {e}")
197
+ return []
198
+
199
  def get_parameters(self):
200
  """Fetch available parameter codes (pollutants)"""
201
  # If we don't have API credentials, use mock data
 
237
  print(f"Error fetching parameters: {e}")
238
  return []
239
 
240
+ def get_latest_aqi(self, state_code, county_code=None, parameter_code=None):
241
  """Fetch the latest AQI data for monitors"""
242
  # If we don't have API credentials, use mock data
243
  if not EMAIL or not API_KEY:
 
257
  "bdate": "20240314", # Beginning date (YYYYMMDD) - last 30 days
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."
 
 
 
 
 
 
297
 
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()
314
+
315
+ # Create a map with a specific width and height - make it bigger
316
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=7, width='100%', height=700)
317
 
318
+ # Add a marker cluster
319
  marker_cluster = MarkerCluster().add_to(m)
320
 
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']}"
 
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():
335
  site_id = f"{row['state_code']}-{row['county_code']}-{row['site_number']}"
 
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
  """
 
414
  popup=popup,
415
  icon=folium.Icon(color=color, icon="cloud"),
416
  ).add_to(marker_cluster)
417
+
418
+ # Return map HTML and legend HTML separately
419
+ map_html = m._repr_html_()
420
+
421
+ # Create legend HTML outside the map
422
+ legend_html = self.create_legend_html()
423
+
424
+ return {"map": map_html, "legend": legend_html}
425
 
426
  def create_legend_html(self):
427
  """Create the HTML for the AQI legend"""
 
443
 
444
  def get_aqi_category(self, aqi_value):
445
  """Determine AQI category based on value"""
446
+ aqi = int(aqi_value)
447
+ if aqi <= 50:
448
+ return "Good"
449
+ elif aqi <= 100:
450
+ return "Moderate"
451
+ elif aqi <= 150:
452
+ return "Unhealthy for Sensitive Groups"
453
+ elif aqi <= 200:
454
+ return "Unhealthy"
455
+ elif aqi <= 300:
456
+ return "Very Unhealthy"
457
+ else:
458
+ return "Hazardous"
459
+
460
+ def mock_get_counties(self, state_code):
461
+ """Return mock county data for the specified state"""
462
+ if state_code in self.mock_counties:
463
+ counties = self.mock_counties[state_code]
464
+ return [f"{c['code']}: {c['value']}" for c in counties]
465
+ else:
466
+ # Return generic counties for other states
467
+ return [
468
+ "001: County 1",
469
+ "002: County 2",
470
+ "003: County 3",
471
+ "004: County 4"
472
+ ]
473
 
474
  def mock_get_parameters(self):
475
  """Return mock parameter data"""
476
  return [f"{p['code']}: {p['value_represented']}" for p in self.mock_parameters]
477
 
478
+ def mock_get_monitors(self, state_code, county_code=None, parameter_code=None):
479
+ """Mock function to return sample data for development"""
480
+ # Get state code in proper format
481
+ if len(state_code) == 2:
482
+ # Convert 2-letter state code to numeric format for mock data
483
+ state_code_mapping = {
484
+ "CA": "06",
485
+ "NY": "36",
486
+ "TX": "48"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  }
488
+ numeric_state_code = state_code_mapping.get(state_code, "01") # Default to "01" if not found
489
+ else:
490
+ numeric_state_code = state_code
491
+ # Sample data for California
492
+ if state_code == "CA" or numeric_state_code == "06":
493
+ monitors = [
494
+ {
495
+ "state_code": "06",
496
+ "county_code": "037",
497
+ "site_number": "0001",
498
+ "parameter_code": "88101",
499
+ "parameter_name": "PM2.5 - Local Conditions",
500
+ "poc": 1,
501
+ "latitude": 34.0667,
502
+ "longitude": -118.2275,
503
+ "local_site_name": "Los Angeles - North Main Street",
504
+ "address": "1630 North Main Street",
505
+ "city_name": "Los Angeles",
506
+ "cbsa_name": "Los Angeles-Long Beach-Anaheim",
507
+ "date_established": "1998-01-01",
508
+ "last_sample_date": "2024-04-10"
509
+ },
510
+ {
511
+ "state_code": "06",
512
+ "county_code": "037",
513
+ "site_number": "0002",
514
+ "parameter_code": "44201",
515
+ "parameter_name": "Ozone",
516
+ "poc": 1,
517
+ "latitude": 34.0667,
518
+ "longitude": -118.2275,
519
+ "local_site_name": "Los Angeles - North Main Street",
520
+ "address": "1630 North Main Street",
521
+ "city_name": "Los Angeles",
522
+ "cbsa_name": "Los Angeles-Long Beach-Anaheim",
523
+ "date_established": "1998-01-01",
524
+ "last_sample_date": "2024-04-10"
525
+ },
526
+ {
527
+ "state_code": "06",
528
+ "county_code": "067",
529
+ "site_number": "0010",
530
+ "parameter_code": "88101",
531
+ "parameter_name": "PM2.5 - Local Conditions",
532
+ "poc": 1,
533
+ "latitude": 38.5661,
534
+ "longitude": -121.4926,
535
+ "local_site_name": "Sacramento - T Street",
536
+ "address": "1309 T Street",
537
+ "city_name": "Sacramento",
538
+ "cbsa_name": "Sacramento-Roseville",
539
+ "date_established": "1999-03-01",
540
+ "last_sample_date": "2024-04-10"
541
+ },
542
+ {
543
+ "state_code": "06",
544
+ "county_code": "073",
545
+ "site_number": "0005",
546
+ "parameter_code": "88101",
547
+ "parameter_name": "PM2.5 - Local Conditions",
548
+ "poc": 1,
549
+ "latitude": 32.7333,
550
+ "longitude": -117.1500,
551
+ "local_site_name": "San Diego - Beardsley Street",
552
+ "address": "1110 Beardsley Street",
553
+ "city_name": "San Diego",
554
+ "cbsa_name": "San Diego-Carlsbad",
555
+ "date_established": "1999-04-15",
556
+ "last_sample_date": "2024-04-10"
557
+ }
558
+ ]
559
+ # Sample data for New York
560
+ elif state_code == "NY" or numeric_state_code == "36":
561
+ monitors = [
562
+ {
563
+ "state_code": "36",
564
+ "county_code": "061",
565
+ "site_number": "0010",
566
+ "parameter_code": "88101",
567
+ "parameter_name": "PM2.5 - Local Conditions",
568
+ "poc": 1,
569
+ "latitude": 40.7159,
570
+ "longitude": -73.9876,
571
+ "local_site_name": "New York - PS 59",
572
+ "address": "228 East 57th Street",
573
+ "city_name": "New York",
574
+ "cbsa_name": "New York-Newark-Jersey City",
575
+ "date_established": "1999-07-15",
576
+ "last_sample_date": "2024-04-10"
577
+ },
578
+ {
579
+ "state_code": "36",
580
+ "county_code": "061",
581
+ "site_number": "0079",
582
+ "parameter_code": "44201",
583
+ "parameter_name": "Ozone",
584
+ "poc": 1,
585
+ "latitude": 40.8160,
586
+ "longitude": -73.9510,
587
+ "local_site_name": "New York - IS 52",
588
+ "address": "681 Kelly Street",
589
+ "city_name": "Bronx",
590
+ "cbsa_name": "New York-Newark-Jersey City",
591
+ "date_established": "1998-01-01",
592
+ "last_sample_date": "2024-04-10"
593
+ }
594
+ ]
595
+ # Sample data for Texas
596
+ elif state_code == "TX" or numeric_state_code == "48":
597
+ monitors = [
598
+ {
599
+ "state_code": "48",
600
+ "county_code": "201",
601
+ "site_number": "0024",
602
+ "parameter_code": "88101",
603
+ "parameter_name": "PM2.5 - Local Conditions",
604
+ "poc": 1,
605
+ "latitude": 29.7349,
606
+ "longitude": -95.3063,
607
+ "local_site_name": "Houston - Clinton Drive",
608
+ "address": "9525 Clinton Drive",
609
+ "city_name": "Houston",
610
+ "cbsa_name": "Houston-The Woodlands-Sugar Land",
611
+ "date_established": "1997-09-01",
612
+ "last_sample_date": "2024-04-10"
613
+ },
614
+ {
615
+ "state_code": "48",
616
+ "county_code": "113",
617
+ "site_number": "0050",
618
+ "parameter_code": "44201",
619
+ "parameter_name": "Ozone",
620
+ "poc": 1,
621
+ "latitude": 32.8198,
622
+ "longitude": -96.8602,
623
+ "local_site_name": "Dallas - Hinton Street",
624
+ "address": "1415 Hinton Street",
625
+ "city_name": "Dallas",
626
+ "cbsa_name": "Dallas-Fort Worth-Arlington",
627
+ "date_established": "1998-01-01",
628
+ "last_sample_date": "2024-04-10"
629
+ }
630
+ ]
631
+ else:
632
+ # Default data for other states - generate some random monitors
633
+ monitors = [
634
+ {
635
+ "state_code": state_code,
636
+ "county_code": "001",
637
+ "site_number": "0001",
638
+ "parameter_code": "88101",
639
+ "parameter_name": "PM2.5 - Local Conditions",
640
+ "poc": 1,
641
+ "latitude": 40.0 + float(ord(state_code[0])) / 10,
642
+ "longitude": -90.0 - float(ord(state_code[1])) / 10,
643
+ "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 1",
644
+ "address": "123 Main Street",
645
+ "city_name": "City 1",
646
+ "cbsa_name": f"{self.states.get(state_code, 'Unknown')} Metro Area",
647
+ "date_established": "2000-01-01",
648
+ "last_sample_date": "2024-04-10"
649
+ },
650
+ {
651
+ "state_code": state_code,
652
+ "county_code": "002",
653
+ "site_number": "0002",
654
+ "parameter_code": "44201",
655
+ "parameter_name": "Ozone",
656
+ "poc": 1,
657
+ "latitude": 40.5 + float(ord(state_code[0])) / 10,
658
+ "longitude": -90.5 - float(ord(state_code[1])) / 10,
659
+ "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 2",
660
+ "address": "456 Oak Street",
661
+ "city_name": "City 2",
662
+ "cbsa_name": f"{self.states.get(state_code, 'Unknown')} Metro Area",
663
+ "date_established": "2000-01-01",
664
+ "last_sample_date": "2024-04-10"
665
+ }
666
+ ]
667
 
668
+ # Filter by county if provided
669
+ if county_code:
670
+ monitors = [m for m in monitors if m["county_code"] == county_code]
671
+
672
+ # Filter by parameter if provided
673
+ if parameter_code:
674
+ monitors = [m for m in monitors if m["parameter_code"] == parameter_code]
675
+
676
+ return monitors
677
+
678
+ def create_air_quality_map_ui():
679
+ """Create the Gradio interface for the Air Quality Map application"""
680
+ app = AirQualityApp()
681
+
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"""
689
+ # Extract code from county string if provided
690
+ county_code = None
691
+ if county and ":" in county:
692
+ county_code = county.split(":")[0].strip()
693
+
694
+ # Extract code from parameter string if provided
695
+ parameter_code = None
696
+ if parameter and ":" in parameter:
697
+ parameter_code = parameter.split(":")[0].strip()
698
+
699
+ # Generate the map
700
+ result = app.create_map(state, county_code, parameter_code)
701
+
702
+ if isinstance(result, dict):
703
+ # Combine map and legend HTML
704
+ html_content = f"""
705
+ <div>
706
+ {result["map"]}
707
+ {result["legend"]}
708
+ </div>
709
+ """
710
+ return html_content
711
+ else:
712
+ # Return error message or whatever was returned
713
+ return result
714
+
715
+ # Create the UI
716
+ with gr.Blocks(title="Air Quality Monitoring Stations") as interface:
717
+ gr.Markdown("# NOAA Air Quality Monitoring Stations Map")
718
+ gr.Markdown("""
719
+ This application displays air quality monitoring stations in the United States.
720
+
721
+ **Note:** To use the actual EPA AQS API, you need to register for an API key and set
722
+ `EPA_AQS_EMAIL` and `EPA_AQS_API_KEY` environment variables in your Hugging Face Space.
723
+
724
+ For demonstration without an API key, the app shows sample data for California (CA), New York (NY), and Texas (TX).
725
+ """)
726
+
727
+ with gr.Row():
728
+ with gr.Column(scale=1):
729
+ # State dropdown with default value
730
+ state_dropdown = gr.Dropdown(
731
+ choices=list(app.states.keys()),
732
+ label="Select State",
733
+ value="CA"
734
+ )
735
+
736
+ # County dropdown with mock counties for the default state
737
+ county_dropdown = gr.Dropdown(
738
+ choices=app.mock_get_counties("CA"),
739
+ label="Select County (Optional)",
740
+ allow_custom_value=True
741
+ )
742
+
743
+ # Parameter dropdown (pollutant type)
744
+ parameter_dropdown = gr.Dropdown(
745
+ choices=app.mock_get_parameters(),
746
+ label="Select Pollutant (Optional)",
747
+ allow_custom_value=True
748
+ )
749
+
750
+ # Button to generate map
751
+ map_button = gr.Button("Show Map")
752
+
753
+ # HTML component to display the map in a larger column
754
+ with gr.Column(scale=3):
755
+ map_html = gr.HTML(label="Air Quality Monitoring Stations Map")
756
+
757
+ # Set up event handlers
758
+ state_dropdown.change(
759
+ fn=update_counties,
760
+ inputs=state_dropdown,
761
+ outputs=county_dropdown
762
+ )
763
+
764
+ map_button.click(
765
+ fn=show_map,
766
+ inputs=[state_dropdown, county_dropdown, parameter_dropdown],
767
+ outputs=map_html
768
+ )
769
+
770
+ return interface
771
+
772
+ # Create and launch the app
773
+ if __name__ == "__main__":
774
+ air_quality_map_ui = create_air_quality_map_ui()
775
+ air_quality_map_ui.launch()