nakas commited on
Commit
752fda7
·
1 Parent(s): 27e66a4

Create air_quality_map.py

Browse files
Files changed (1) hide show
  1. air_quality_map.py +681 -0
air_quality_map.py ADDED
@@ -0,0 +1,681 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import pandas as pd
4
+ import folium
5
+ from folium.plugins import MarkerCluster
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"
12
+ EMAIL = os.environ.get("EPA_AQS_EMAIL", "") # Get from environment variable
13
+ API_KEY = os.environ.get("EPA_AQS_API_KEY", "") # Get from environment variable
14
+
15
+ class AirQualityApp:
16
+ def __init__(self):
17
+ self.states = {
18
+ "AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas",
19
+ "CA": "California", "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware",
20
+ "FL": "Florida", "GA": "Georgia", "HI": "Hawaii", "ID": "Idaho",
21
+ "IL": "Illinois", "IN": "Indiana", "IA": "Iowa", "KS": "Kansas",
22
+ "KY": "Kentucky", "LA": "Louisiana", "ME": "Maine", "MD": "Maryland",
23
+ "MA": "Massachusetts", "MI": "Michigan", "MN": "Minnesota", "MS": "Mississippi",
24
+ "MO": "Missouri", "MT": "Montana", "NE": "Nebraska", "NV": "Nevada",
25
+ "NH": "New Hampshire", "NJ": "New Jersey", "NM": "New Mexico", "NY": "New York",
26
+ "NC": "North Carolina", "ND": "North Dakota", "OH": "Ohio", "OK": "Oklahoma",
27
+ "OR": "Oregon", "PA": "Pennsylvania", "RI": "Rhode Island", "SC": "South Carolina",
28
+ "SD": "South Dakota", "TN": "Tennessee", "TX": "Texas", "UT": "Utah",
29
+ "VT": "Vermont", "VA": "Virginia", "WA": "Washington", "WV": "West Virginia",
30
+ "WI": "Wisconsin", "WY": "Wyoming", "DC": "District of Columbia"
31
+ }
32
+
33
+ # Mapping from two-letter state codes to numeric state codes for API
34
+ self.state_code_mapping = {
35
+ "AL": "01", "AK": "02", "AZ": "04", "AR": "05",
36
+ "CA": "06", "CO": "08", "CT": "09", "DE": "10",
37
+ "FL": "12", "GA": "13", "HI": "15", "ID": "16",
38
+ "IL": "17", "IN": "18", "IA": "19", "KS": "20",
39
+ "KY": "21", "LA": "22", "ME": "23", "MD": "24",
40
+ "MA": "25", "MI": "26", "MN": "27", "MS": "28",
41
+ "MO": "29", "MT": "30", "NE": "31", "NV": "32",
42
+ "NH": "33", "NJ": "34", "NM": "35", "NY": "36",
43
+ "NC": "37", "ND": "38", "OH": "39", "OK": "40",
44
+ "OR": "41", "PA": "42", "RI": "44", "SC": "45",
45
+ "SD": "46", "TN": "47", "TX": "48", "UT": "49",
46
+ "VT": "50", "VA": "51", "WA": "53", "WV": "54",
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
55
+ "Unhealthy": "#ff0000", # Red
56
+ "Very Unhealthy": "#99004c", # Purple
57
+ "Hazardous": "#7e0023" # Maroon
58
+ }
59
+
60
+ # Sample county data for demo
61
+ self.mock_counties = {
62
+ "CA": [
63
+ {"code": "037", "value": "Los Angeles"},
64
+ {"code": "067", "value": "Sacramento"},
65
+ {"code": "073", "value": "San Diego"},
66
+ {"code": "075", "value": "San Francisco"}
67
+ ],
68
+ "NY": [
69
+ {"code": "061", "value": "New York"},
70
+ {"code": "047", "value": "Kings (Brooklyn)"},
71
+ {"code": "081", "value": "Queens"},
72
+ {"code": "005", "value": "Bronx"}
73
+ ],
74
+ "TX": [
75
+ {"code": "201", "value": "Harris (Houston)"},
76
+ {"code": "113", "value": "Dallas"},
77
+ {"code": "029", "value": "Bexar (San Antonio)"},
78
+ {"code": "453", "value": "Travis (Austin)"}
79
+ ]
80
+ }
81
+
82
+ # Sample parameters for demo
83
+ self.mock_parameters = [
84
+ {"code": "88101", "value_represented": "PM2.5 - Local Conditions"},
85
+ {"code": "44201", "value_represented": "Ozone"},
86
+ {"code": "42401", "value_represented": "Sulfur dioxide"},
87
+ {"code": "42101", "value_represented": "Carbon monoxide"},
88
+ {"code": "42602", "value_represented": "Nitrogen dioxide"},
89
+ {"code": "81102", "value_represented": "PM10 - Local Conditions"}
90
+ ]
91
+
92
+ def get_monitors(self, state_code, county_code=None, parameter_code=None):
93
+ """Fetch monitoring stations for a given state and optional county"""
94
+ # If we don't have API credentials, use mock data
95
+ if not EMAIL or not API_KEY:
96
+ return self.mock_get_monitors(state_code, county_code, parameter_code)
97
+
98
+ # Convert state code to numeric format for API
99
+ api_state_code = state_code
100
+ if len(state_code) == 2 and state_code in self.state_code_mapping:
101
+ api_state_code = self.state_code_mapping[state_code]
102
+
103
+ # API endpoint for monitoring sites
104
+ endpoint = f"{EPA_AQS_API_BASE_URL}/monitors/byState"
105
+
106
+ params = {
107
+ "email": EMAIL,
108
+ "key": API_KEY,
109
+ "state": api_state_code,
110
+ "bdate": "20240101", # Beginning date (YYYYMMDD)
111
+ "edate": "20240414", # End date (YYYYMMDD)
112
+ }
113
+
114
+ if county_code:
115
+ params["county"] = county_code
116
+
117
+ if parameter_code:
118
+ params["param"] = parameter_code
119
+
120
+ try:
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"""
141
+ # If we don't have API credentials, use mock data
142
+ if not EMAIL or not API_KEY:
143
+ return self.mock_get_counties(state_code)
144
+
145
+ # Convert state code to numeric format for API
146
+ api_state_code = state_code
147
+ if len(state_code) == 2 and state_code in self.state_code_mapping:
148
+ api_state_code = self.state_code_mapping[state_code]
149
+
150
+ endpoint = f"{EPA_AQS_API_BASE_URL}/list/countiesByState"
151
+
152
+ params = {
153
+ "email": EMAIL,
154
+ "key": API_KEY,
155
+ "state": api_state_code
156
+ }
157
+
158
+ try:
159
+ response = requests.get(endpoint, params=params)
160
+ data = response.json()
161
+
162
+ # Handle the specific response structure we observed
163
+ counties = []
164
+ if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
165
+ counties = data["Data"]
166
+
167
+ # Format as "code: name" for dropdown
168
+ result = []
169
+ for c in counties:
170
+ code = c.get("code")
171
+ value = c.get("value_represented")
172
+ if code and value:
173
+ result.append(f"{code}: {value}")
174
+
175
+ return result
176
+ except Exception as e:
177
+ print(f"Error fetching counties: {e}")
178
+ return []
179
+
180
+ def get_parameters(self):
181
+ """Fetch available parameter codes (pollutants)"""
182
+ # If we don't have API credentials, use mock data
183
+ if not EMAIL or not API_KEY:
184
+ return self.mock_get_parameters()
185
+
186
+ endpoint = f"{EPA_AQS_API_BASE_URL}/list/parametersByClass"
187
+
188
+ params = {
189
+ "email": EMAIL,
190
+ "key": API_KEY,
191
+ "pc": "CRITERIA" # Filter to criteria pollutants
192
+ }
193
+
194
+ try:
195
+ response = requests.get(endpoint, params=params)
196
+ data = response.json()
197
+
198
+ # Handle the specific response structure we observed
199
+ parameters = []
200
+ if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
201
+ parameters = data["Data"]
202
+
203
+ # Format as "code: name" for dropdown
204
+ result = []
205
+ for p in parameters:
206
+ code = p.get("code")
207
+ value = p.get("value_represented")
208
+ if not code:
209
+ code = p.get("parameter_code")
210
+ if not value:
211
+ value = p.get("parameter_name")
212
+
213
+ if code and value:
214
+ result.append(f"{code}: {value}")
215
+
216
+ return result
217
+ except Exception as e:
218
+ print(f"Error fetching parameters: {e}")
219
+ return []
220
+
221
+ def get_latest_aqi(self, state_code, county_code=None, parameter_code=None):
222
+ """Fetch the latest AQI data for monitors"""
223
+ # If we don't have API credentials, use mock data
224
+ if not EMAIL or not API_KEY:
225
+ return [] # We don't have mock AQI data for simplicity
226
+
227
+ # Convert state code to numeric format for API
228
+ api_state_code = state_code
229
+ if len(state_code) == 2 and state_code in self.state_code_mapping:
230
+ api_state_code = self.state_code_mapping[state_code]
231
+
232
+ endpoint = f"{EPA_AQS_API_BASE_URL}/dailyData/byState"
233
+
234
+ params = {
235
+ "email": EMAIL,
236
+ "key": API_KEY,
237
+ "state": api_state_code,
238
+ "bdate": "20240314", # Beginning date (YYYYMMDD) - last 30 days
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
247
+
248
+ try:
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."
268
+
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()
275
+
276
+ # Create a map with a specific width and height - make it bigger
277
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=7, width='100%', height=700)
278
+
279
+ # Add a marker cluster
280
+ marker_cluster = MarkerCluster().add_to(m)
281
+
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():
294
+ site_id = f"{row['state_code']}-{row['county_code']}-{row['site_number']}"
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
+
323
+ # Return map HTML and legend HTML separately
324
+ map_html = m._repr_html_()
325
+
326
+ # Create legend HTML outside the map
327
+ legend_html = self.create_legend_html()
328
+
329
+ return {"map": map_html, "legend": legend_html}
330
+
331
+ def create_legend_html(self):
332
+ """Create the HTML for the AQI legend"""
333
+ legend_html = """
334
+ <div style="padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: white; margin-top: 10px;">
335
+ <h4 style="margin-top: 0;">AQI Categories</h4>
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
+
343
+ legend_html += """
344
+ </div>
345
+ </div>
346
+ """
347
+ return legend_html
348
+
349
+ def get_aqi_category(self, aqi_value):
350
+ """Determine AQI category based on value"""
351
+ aqi = int(aqi_value)
352
+ if aqi <= 50:
353
+ return "Good"
354
+ elif aqi <= 100:
355
+ return "Moderate"
356
+ elif aqi <= 150:
357
+ return "Unhealthy for Sensitive Groups"
358
+ elif aqi <= 200:
359
+ return "Unhealthy"
360
+ elif aqi <= 300:
361
+ return "Very Unhealthy"
362
+ else:
363
+ return "Hazardous"
364
+
365
+ def mock_get_counties(self, state_code):
366
+ """Return mock county data for the specified state"""
367
+ if state_code in self.mock_counties:
368
+ counties = self.mock_counties[state_code]
369
+ return [f"{c['code']}: {c['value']}" for c in counties]
370
+ else:
371
+ # Return generic counties for other states
372
+ return [
373
+ "001: County 1",
374
+ "002: County 2",
375
+ "003: County 3",
376
+ "004: County 4"
377
+ ]
378
+
379
+ def mock_get_parameters(self):
380
+ """Return mock parameter data"""
381
+ return [f"{p['code']}: {p['value_represented']}" for p in self.mock_parameters]
382
+
383
+ def mock_get_monitors(self, state_code, county_code=None, parameter_code=None):
384
+ """Mock function to return sample data for development"""
385
+ # Get state code in proper format
386
+ if len(state_code) == 2:
387
+ # Convert 2-letter state code to numeric format for mock data
388
+ state_code_mapping = {
389
+ "CA": "06",
390
+ "NY": "36",
391
+ "TX": "48"
392
+ }
393
+ numeric_state_code = state_code_mapping.get(state_code, "01") # Default to "01" if not found
394
+ else:
395
+ numeric_state_code = state_code
396
+ # Sample data for California
397
+ if state_code == "CA" or numeric_state_code == "06":
398
+ monitors = [
399
+ {
400
+ "state_code": "06",
401
+ "county_code": "037",
402
+ "site_number": "0001",
403
+ "parameter_code": "88101",
404
+ "parameter_name": "PM2.5 - Local Conditions",
405
+ "poc": 1,
406
+ "latitude": 34.0667,
407
+ "longitude": -118.2275,
408
+ "local_site_name": "Los Angeles - North Main Street",
409
+ "address": "1630 North Main Street",
410
+ "city_name": "Los Angeles",
411
+ "cbsa_name": "Los Angeles-Long Beach-Anaheim",
412
+ "date_established": "1998-01-01",
413
+ "last_sample_date": "2024-04-10"
414
+ },
415
+ {
416
+ "state_code": "06",
417
+ "county_code": "037",
418
+ "site_number": "0002",
419
+ "parameter_code": "44201",
420
+ "parameter_name": "Ozone",
421
+ "poc": 1,
422
+ "latitude": 34.0667,
423
+ "longitude": -118.2275,
424
+ "local_site_name": "Los Angeles - North Main Street",
425
+ "address": "1630 North Main Street",
426
+ "city_name": "Los Angeles",
427
+ "cbsa_name": "Los Angeles-Long Beach-Anaheim",
428
+ "date_established": "1998-01-01",
429
+ "last_sample_date": "2024-04-10"
430
+ },
431
+ {
432
+ "state_code": "06",
433
+ "county_code": "067",
434
+ "site_number": "0010",
435
+ "parameter_code": "88101",
436
+ "parameter_name": "PM2.5 - Local Conditions",
437
+ "poc": 1,
438
+ "latitude": 38.5661,
439
+ "longitude": -121.4926,
440
+ "local_site_name": "Sacramento - T Street",
441
+ "address": "1309 T Street",
442
+ "city_name": "Sacramento",
443
+ "cbsa_name": "Sacramento-Roseville",
444
+ "date_established": "1999-03-01",
445
+ "last_sample_date": "2024-04-10"
446
+ },
447
+ {
448
+ "state_code": "06",
449
+ "county_code": "073",
450
+ "site_number": "0005",
451
+ "parameter_code": "88101",
452
+ "parameter_name": "PM2.5 - Local Conditions",
453
+ "poc": 1,
454
+ "latitude": 32.7333,
455
+ "longitude": -117.1500,
456
+ "local_site_name": "San Diego - Beardsley Street",
457
+ "address": "1110 Beardsley Street",
458
+ "city_name": "San Diego",
459
+ "cbsa_name": "San Diego-Carlsbad",
460
+ "date_established": "1999-04-15",
461
+ "last_sample_date": "2024-04-10"
462
+ }
463
+ ]
464
+ # Sample data for New York
465
+ elif state_code == "NY" or numeric_state_code == "36":
466
+ monitors = [
467
+ {
468
+ "state_code": "36",
469
+ "county_code": "061",
470
+ "site_number": "0010",
471
+ "parameter_code": "88101",
472
+ "parameter_name": "PM2.5 - Local Conditions",
473
+ "poc": 1,
474
+ "latitude": 40.7159,
475
+ "longitude": -73.9876,
476
+ "local_site_name": "New York - PS 59",
477
+ "address": "228 East 57th Street",
478
+ "city_name": "New York",
479
+ "cbsa_name": "New York-Newark-Jersey City",
480
+ "date_established": "1999-07-15",
481
+ "last_sample_date": "2024-04-10"
482
+ },
483
+ {
484
+ "state_code": "36",
485
+ "county_code": "061",
486
+ "site_number": "0079",
487
+ "parameter_code": "44201",
488
+ "parameter_name": "Ozone",
489
+ "poc": 1,
490
+ "latitude": 40.8160,
491
+ "longitude": -73.9510,
492
+ "local_site_name": "New York - IS 52",
493
+ "address": "681 Kelly Street",
494
+ "city_name": "Bronx",
495
+ "cbsa_name": "New York-Newark-Jersey City",
496
+ "date_established": "1998-01-01",
497
+ "last_sample_date": "2024-04-10"
498
+ }
499
+ ]
500
+ # Sample data for Texas
501
+ elif state_code == "TX" or numeric_state_code == "48":
502
+ monitors = [
503
+ {
504
+ "state_code": "48",
505
+ "county_code": "201",
506
+ "site_number": "0024",
507
+ "parameter_code": "88101",
508
+ "parameter_name": "PM2.5 - Local Conditions",
509
+ "poc": 1,
510
+ "latitude": 29.7349,
511
+ "longitude": -95.3063,
512
+ "local_site_name": "Houston - Clinton Drive",
513
+ "address": "9525 Clinton Drive",
514
+ "city_name": "Houston",
515
+ "cbsa_name": "Houston-The Woodlands-Sugar Land",
516
+ "date_established": "1997-09-01",
517
+ "last_sample_date": "2024-04-10"
518
+ },
519
+ {
520
+ "state_code": "48",
521
+ "county_code": "113",
522
+ "site_number": "0050",
523
+ "parameter_code": "44201",
524
+ "parameter_name": "Ozone",
525
+ "poc": 1,
526
+ "latitude": 32.8198,
527
+ "longitude": -96.8602,
528
+ "local_site_name": "Dallas - Hinton Street",
529
+ "address": "1415 Hinton Street",
530
+ "city_name": "Dallas",
531
+ "cbsa_name": "Dallas-Fort Worth-Arlington",
532
+ "date_established": "1998-01-01",
533
+ "last_sample_date": "2024-04-10"
534
+ }
535
+ ]
536
+ else:
537
+ # Default data for other states - generate some random monitors
538
+ monitors = [
539
+ {
540
+ "state_code": state_code,
541
+ "county_code": "001",
542
+ "site_number": "0001",
543
+ "parameter_code": "88101",
544
+ "parameter_name": "PM2.5 - Local Conditions",
545
+ "poc": 1,
546
+ "latitude": 40.0 + float(ord(state_code[0])) / 10,
547
+ "longitude": -90.0 - float(ord(state_code[1])) / 10,
548
+ "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 1",
549
+ "address": "123 Main Street",
550
+ "city_name": "City 1",
551
+ "cbsa_name": f"{self.states.get(state_code, 'Unknown')} Metro Area",
552
+ "date_established": "2000-01-01",
553
+ "last_sample_date": "2024-04-10"
554
+ },
555
+ {
556
+ "state_code": state_code,
557
+ "county_code": "002",
558
+ "site_number": "0002",
559
+ "parameter_code": "44201",
560
+ "parameter_name": "Ozone",
561
+ "poc": 1,
562
+ "latitude": 40.5 + float(ord(state_code[0])) / 10,
563
+ "longitude": -90.5 - float(ord(state_code[1])) / 10,
564
+ "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 2",
565
+ "address": "456 Oak Street",
566
+ "city_name": "City 2",
567
+ "cbsa_name": f"{self.states.get(state_code, 'Unknown')} Metro Area",
568
+ "date_established": "2000-01-01",
569
+ "last_sample_date": "2024-04-10"
570
+ }
571
+ ]
572
+
573
+ # Filter by county if provided
574
+ if county_code:
575
+ monitors = [m for m in monitors if m["county_code"] == county_code]
576
+
577
+ # Filter by parameter if provided
578
+ if parameter_code:
579
+ monitors = [m for m in monitors if m["parameter_code"] == parameter_code]
580
+
581
+ return monitors
582
+
583
+ def create_air_quality_map_ui():
584
+ """Create the Gradio interface for the Air Quality Map application"""
585
+ app = AirQualityApp()
586
+
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"""
594
+ # Extract code from county string if provided
595
+ county_code = None
596
+ if county and ":" in county:
597
+ county_code = county.split(":")[0].strip()
598
+
599
+ # Extract code from parameter string if provided
600
+ parameter_code = None
601
+ if parameter and ":" in parameter:
602
+ parameter_code = parameter.split(":")[0].strip()
603
+
604
+ # Generate the map
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
619
+
620
+ # Create the UI
621
+ with gr.Blocks(title="Air Quality Monitoring Stations") as interface:
622
+ gr.Markdown("# NOAA Air Quality Monitoring Stations Map")
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
+ """)
632
+
633
+ with gr.Row():
634
+ with gr.Column(scale=1):
635
+ # State dropdown with default value
636
+ state_dropdown = gr.Dropdown(
637
+ choices=list(app.states.keys()),
638
+ label="Select State",
639
+ value="CA"
640
+ )
641
+
642
+ # County dropdown with mock counties for the default state
643
+ county_dropdown = gr.Dropdown(
644
+ choices=app.mock_get_counties("CA"),
645
+ label="Select County (Optional)",
646
+ allow_custom_value=True
647
+ )
648
+
649
+ # Parameter dropdown (pollutant type)
650
+ parameter_dropdown = gr.Dropdown(
651
+ choices=app.mock_get_parameters(),
652
+ label="Select Pollutant (Optional)",
653
+ allow_custom_value=True
654
+ )
655
+
656
+ # Button to generate map
657
+ map_button = gr.Button("Show Map")
658
+
659
+ # HTML component to display the map in a larger column
660
+ with gr.Column(scale=3):
661
+ map_html = gr.HTML(label="Air Quality Monitoring Stations Map")
662
+
663
+ # Set up event handlers
664
+ state_dropdown.change(
665
+ fn=update_counties,
666
+ inputs=state_dropdown,
667
+ outputs=county_dropdown
668
+ )
669
+
670
+ map_button.click(
671
+ fn=show_map,
672
+ inputs=[state_dropdown, county_dropdown, parameter_dropdown],
673
+ outputs=map_html
674
+ )
675
+
676
+ return interface
677
+
678
+ # Create and launch the app
679
+ if __name__ == "__main__":
680
+ air_quality_map_ui = create_air_quality_map_ui()
681
+ air_quality_map_ui.launch()