nakas commited on
Commit
34cb6f5
·
verified ·
1 Parent(s): b15ce75

Update air_quality_map.py

Browse files
Files changed (1) hide show
  1. air_quality_map.py +294 -165
air_quality_map.py CHANGED
@@ -6,6 +6,8 @@ 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"
@@ -14,7 +16,6 @@ API_KEY = os.environ.get("EPA_AQS_API_KEY", "") # Get from environment variable
14
 
15
  class AirQualityApp:
16
  def __init__(self):
17
- # [Keep all your existing initialization code the same]
18
  self.states = {
19
  "AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas",
20
  "CA": "California", "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware",
@@ -67,45 +68,67 @@ class AirQualityApp:
67
  "Very Unhealthy": "#99004c", # Purple
68
  "Hazardous": "#7e0023" # Maroon
69
  }
 
 
 
 
70
 
71
- # Sample county data for demo
72
- self.mock_counties = {
73
- "CA": [
74
- {"code": "037", "value": "Los Angeles"},
75
- {"code": "067", "value": "Sacramento"},
76
- {"code": "073", "value": "San Diego"},
77
- {"code": "075", "value": "San Francisco"}
78
- ],
79
- "NY": [
80
- {"code": "061", "value": "New York"},
81
- {"code": "047", "value": "Kings (Brooklyn)"},
82
- {"code": "081", "value": "Queens"},
83
- {"code": "005", "value": "Bronx"}
84
- ],
85
- "TX": [
86
- {"code": "201", "value": "Harris (Houston)"},
87
- {"code": "113", "value": "Dallas"},
88
- {"code": "029", "value": "Bexar (San Antonio)"},
89
- {"code": "453", "value": "Travis (Austin)"}
90
- ]
91
- }
92
 
93
- # Sample parameters for demo
94
- self.mock_parameters = [
95
- {"code": "88101", "value_represented": "PM2.5 - Local Conditions"},
96
- {"code": "44201", "value_represented": "Ozone"},
97
- {"code": "42401", "value_represented": "Sulfur dioxide"},
98
- {"code": "42101", "value_represented": "Carbon monoxide"},
99
- {"code": "42602", "value_represented": "Nitrogen dioxide"},
100
- {"code": "81102", "value_represented": "PM10 - Local Conditions"}
101
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- # Mock air quality data for demo
104
- self.mock_aqi_data = {
105
- "CA": self._generate_mock_aqi_data("CA"),
106
- "NY": self._generate_mock_aqi_data("NY"),
107
- "TX": self._generate_mock_aqi_data("TX")
108
- }
 
 
 
 
 
 
 
 
 
 
109
 
110
  def _generate_mock_aqi_data(self, state_code):
111
  """Generate mock AQI data for a state"""
@@ -117,43 +140,80 @@ class AirQualityApp:
117
  # Get numeric state code
118
  numeric_state_code = self.state_code_mapping.get(state_code, "01")
119
 
120
- # Generate data for the most recent 7 days
121
- for days_ago in range(7):
122
- # Generate date
123
- date = (datetime.now() - timedelta(days=days_ago)).strftime("%Y-%m-%d")
124
-
125
- # Generate random data for each county in the state
126
- counties = self.mock_counties.get(state_code, [{"code": "001", "value": "County 1"}])
127
- for county in counties:
128
- county_code = county["code"]
129
 
130
- # Generate data for each parameter
131
- for param in self.mock_parameters:
132
- param_code = param["code"]
133
- param_name = param["value_represented"]
 
 
134
 
135
- # Generate a few site numbers per county
136
- for site_num in range(1, 4):
137
- site_number = f"{site_num:04d}"
138
-
139
- # Generate random AQI value (between 0 and 300)
140
- aqi_value = random.randint(0, 300)
141
-
142
- aqi_data.append({
143
- "state_code": numeric_state_code,
144
- "county_code": county_code,
145
- "site_number": site_number,
146
- "parameter_code": param_code,
147
- "parameter_name": param_name,
148
- "date_local": date,
149
- "aqi": aqi_value
150
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  return aqi_data
153
 
154
- # [Keep all your existing methods the same]
155
  def get_monitors(self, state_code, county_code=None, parameter_code=None):
156
- # [Same as original]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  # If we don't have API credentials, use mock data
158
  if not EMAIL or not API_KEY:
159
  return self.mock_get_monitors(state_code, county_code, parameter_code)
@@ -184,12 +244,7 @@ class AirQualityApp:
184
  response = requests.get(endpoint, params=params)
185
  data = response.json()
186
 
187
- # Add detailed debugging
188
- print(f"API Response Keys: {list(data.keys()) if isinstance(data, dict) else 'Not a dictionary'}")
189
- if isinstance(data, dict) and "Header" in data:
190
- print(f"Header type: {type(data['Header'])}, content: {data['Header'][:100]}...")
191
-
192
- # Handle the specific response structure we observed
193
  if isinstance(data, dict):
194
  if "Data" in data and isinstance(data["Data"], list):
195
  return data["Data"]
@@ -209,7 +264,6 @@ class AirQualityApp:
209
  return self.mock_get_monitors(state_code, county_code, parameter_code)
210
 
211
  def get_counties(self, state_code):
212
- # [Same as original]
213
  """Fetch counties for a given state"""
214
  # If we don't have API credentials, use mock data
215
  if not EMAIL or not API_KEY:
@@ -251,7 +305,6 @@ class AirQualityApp:
251
  return []
252
 
253
  def get_parameters(self):
254
- # [Same as original]
255
  """Fetch available parameter codes (pollutants)"""
256
  # If we don't have API credentials, use mock data
257
  if not EMAIL or not API_KEY:
@@ -294,22 +347,24 @@ class AirQualityApp:
294
 
295
  def get_latest_aqi(self, state_code, county_code=None, parameter_code=None):
296
  """Fetch the latest AQI data for monitors"""
297
- # If we don't have API credentials, use mock data
298
- if not EMAIL or not API_KEY:
299
- # Use mock data for demo
300
- if state_code in self.mock_aqi_data:
301
- aqi_data = self.mock_aqi_data[state_code]
302
-
303
- # Filter by county if provided
304
- if county_code:
305
- aqi_data = [item for item in aqi_data if item.get('county_code') == county_code]
306
 
307
- # Filter by parameter if provided
308
- if parameter_code:
309
- aqi_data = [item for item in aqi_data if item.get('parameter_code') == parameter_code]
310
 
311
- return aqi_data
312
- return []
 
 
 
 
313
 
314
  # Convert state code to numeric format for API
315
  api_state_code = state_code
@@ -336,11 +391,6 @@ class AirQualityApp:
336
  response = requests.get(endpoint, params=params)
337
  data = response.json()
338
 
339
- # Add detailed debugging
340
- print(f"AQI API Response Keys: {list(data.keys()) if isinstance(data, dict) else 'Not a dictionary'}")
341
- if isinstance(data, dict) and "Header" in data:
342
- print(f"AQI Header type: {type(data['Header'])}, content: {data['Header'][:100]}...")
343
-
344
  # Handle the specific response structure we observed
345
  aqi_data = []
346
  if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
@@ -355,51 +405,69 @@ class AirQualityApp:
355
  print(f"Error fetching AQI data: {e}")
356
  return []
357
 
358
- def create_map(self, state_code, county_code=None, parameter_code=None):
359
- """Create a map with air quality monitoring stations"""
360
- # IMPORTANT: We don't pass county_code to get_monitors anymore since the API doesn't support it
361
- monitors = self.get_monitors(state_code, parameter_code=parameter_code)
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
- if not monitors:
364
  return {"map": "No monitoring stations found for the selected criteria.", "legend": "", "data": None}
365
 
366
  # Convert to DataFrame for easier manipulation
367
- df = pd.DataFrame(monitors)
368
 
369
- # Now filter by county if provided - do this AFTER getting the monitors
370
- if county_code:
371
- print(f"Filtering by county_code: {county_code}")
372
- county_code_str = str(county_code)
373
- df = df[df['county_code'].astype(str) == county_code_str]
374
- print(f"After filtering, {len(df)} monitors remain")
375
-
376
- if len(df) == 0:
377
- return {"map": "No monitoring stations found for the selected county.", "legend": "", "data": None}
378
-
379
- # Create a map centered on the mean latitude and longitude
380
- center_lat = df["latitude"].mean()
381
- center_lon = df["longitude"].mean()
382
 
383
- # Create a map with a specific width and height - make it bigger
384
- m = folium.Map(location=[center_lat, center_lon], zoom_start=7, width='100%', height=500)
385
 
386
  # Add a marker cluster
387
  marker_cluster = MarkerCluster().add_to(m)
388
 
389
- # Get latest AQI data if credentials are provided
390
- aqi_data = {}
391
- all_aqi_readings = []
392
 
393
- # Get AQI data either from API or mock data
394
- aqi_results = self.get_latest_aqi(state_code, county_code, parameter_code)
395
-
396
- # Create a lookup dictionary by site ID
397
- for item in aqi_results:
398
- site_id = f"{item['state_code']}-{item['county_code']}-{item['site_number']}"
399
- if site_id not in aqi_data:
400
- aqi_data[site_id] = []
401
- aqi_data[site_id].append(item)
402
- all_aqi_readings.append(item)
 
 
 
 
 
 
 
 
403
 
404
  # Add markers for each monitoring station
405
  for _, row in df.iterrows():
@@ -409,7 +477,7 @@ class AirQualityApp:
409
  color = "blue"
410
 
411
  # Get AQI data for this station if available
412
- station_aqi_data = aqi_data.get(site_id, [])
413
  latest_aqi = None
414
  aqi_category = None
415
 
@@ -464,14 +532,14 @@ class AirQualityApp:
464
  # Create popup content with detailed information
465
  popup_content = f"""
466
  <div style="min-width: 300px;">
467
- <h3>{row['local_site_name']}</h3>
468
  <p><strong>Site ID:</strong> {site_id}</p>
469
  <p><strong>Address:</strong> {row.get('address', 'N/A')}</p>
470
  <p><strong>City:</strong> {row.get('city_name', 'N/A')}</p>
471
  <p><strong>County:</strong> {row.get('county_name', 'N/A')}</p>
472
- <p><strong>State:</strong> {row.get('state_name', 'N/A')}</p>
473
- <p><strong>Parameter:</strong> {row['parameter_name']}</p>
474
- <p><strong>Coordinates:</strong> {row['latitude']}, {row['longitude']}</p>
475
  {aqi_readings_html}
476
  </div>
477
  """
@@ -493,11 +561,10 @@ class AirQualityApp:
493
  return {
494
  "map": map_html,
495
  "legend": legend_html,
496
- "data": all_aqi_readings
497
  }
498
 
499
  def create_legend_html(self):
500
- # [Same as original]
501
  """Create the HTML for the AQI legend"""
502
  legend_html = """
503
  <div style="padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: white; margin-top: 10px;">
@@ -516,7 +583,6 @@ class AirQualityApp:
516
  return legend_html
517
 
518
  def get_aqi_category(self, aqi_value):
519
- # [Same as original]
520
  """Determine AQI category based on value"""
521
  try:
522
  aqi = int(aqi_value)
@@ -535,11 +601,25 @@ class AirQualityApp:
535
  except (ValueError, TypeError):
536
  return "Unknown"
537
 
538
- def format_air_quality_data_table(self, aqi_data):
539
  """Format air quality data as an HTML table for display"""
540
  if not aqi_data or len(aqi_data) == 0:
541
  return "<p>No air quality data available for the selected criteria.</p>"
542
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  # Sort by date (most recent first) and then by AQI value (highest first)
544
  sorted_data = sorted(aqi_data,
545
  key=lambda x: (x.get('date_local', ''), -int(x.get('aqi', 0)) if x.get('aqi') and str(x.get('aqi')).isdigit() else 0),
@@ -562,6 +642,8 @@ class AirQualityApp:
562
  <table style="width:100%; border-collapse: collapse;">
563
  <tr style="background-color: #f2f2f2; position: sticky; top: 0;">
564
  <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Date</th>
 
 
565
  <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Location</th>
566
  <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Pollutant</th>
567
  <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">AQI</th>
@@ -573,11 +655,18 @@ class AirQualityApp:
573
  for i, item in enumerate(site_data.values()):
574
  date = item.get('date_local', 'N/A')
575
 
576
- # Try to get a nice location name
577
  state_code = item.get('state_code', 'N/A')
 
 
 
 
 
 
 
578
  county_code = item.get('county_code', 'N/A')
579
  site_number = item.get('site_number', 'N/A')
580
- location = f"Site {state_code}-{county_code}-{site_number}"
581
 
582
  pollutant = item.get('parameter_name', 'N/A')
583
  aqi_value = item.get('aqi', 'N/A')
@@ -590,6 +679,8 @@ class AirQualityApp:
590
  html += f"""
591
  <tr{row_style}>
592
  <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{date}</td>
 
 
593
  <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{location}</td>
594
  <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{pollutant}</td>
595
  <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{aqi_value}</td>
@@ -604,11 +695,32 @@ class AirQualityApp:
604
 
605
  return html
606
 
607
- # [Keep all your mock data methods the same]
608
  def mock_get_counties(self, state_code):
609
  """Return mock county data for the specified state"""
610
- if state_code in self.mock_counties:
611
- counties = self.mock_counties[state_code]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  return [f"{c['code']}: {c['value']}" for c in counties]
613
  else:
614
  # Return generic counties for other states
@@ -621,7 +733,17 @@ class AirQualityApp:
621
 
622
  def mock_get_parameters(self):
623
  """Return mock parameter data"""
624
- return [f"{p['code']}: {p['value_represented']}" for p in self.mock_parameters]
 
 
 
 
 
 
 
 
 
 
625
 
626
  def mock_get_monitors(self, state_code, county_code=None, parameter_code=None):
627
  """Mock function to return sample data for development"""
@@ -787,8 +909,8 @@ class AirQualityApp:
787
  "parameter_code": "88101",
788
  "parameter_name": "PM2.5 - Local Conditions",
789
  "poc": 1,
790
- "latitude": 40.0 + float(ord(state_code[0])) / 10,
791
- "longitude": -90.0 - float(ord(state_code[1])) / 10,
792
  "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 1",
793
  "address": "123 Main Street",
794
  "city_name": "City 1",
@@ -803,8 +925,8 @@ class AirQualityApp:
803
  "parameter_code": "44201",
804
  "parameter_name": "Ozone",
805
  "poc": 1,
806
- "latitude": 40.5 + float(ord(state_code[0])) / 10,
807
- "longitude": -90.5 - float(ord(state_code[1])) / 10,
808
  "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 2",
809
  "address": "456 Oak Street",
810
  "city_name": "City 2",
@@ -824,9 +946,9 @@ class AirQualityApp:
824
 
825
  return monitors
826
 
827
- # Create the UI with the new Air Quality Data panel
828
  def create_air_quality_map_ui():
829
- """Create the Gradio interface for the Air Quality Map application with a separate data panel"""
830
  app = AirQualityApp()
831
 
832
  def update_counties(state_code):
@@ -834,7 +956,7 @@ def create_air_quality_map_ui():
834
  counties = app.get_counties(state_code)
835
  return gr.Dropdown(choices=counties)
836
 
837
- def show_map_and_data(state, county=None, parameter=None):
838
  """Callback to generate and display both the map and the air quality data"""
839
  # Extract code from county string if provided
840
  county_code = None
@@ -846,7 +968,7 @@ def create_air_quality_map_ui():
846
  if parameter and ":" in parameter:
847
  parameter_code = parameter.split(":")[0].strip()
848
 
849
- # Generate the map and get data
850
  result = app.create_map(state, county_code, parameter_code)
851
 
852
  if isinstance(result, dict):
@@ -860,7 +982,7 @@ def create_air_quality_map_ui():
860
 
861
  # Process air quality data for the separate panel
862
  if result["data"]:
863
- data_html = app.format_air_quality_data_table(result["data"])
864
  else:
865
  data_html = "<p>No air quality data available for the selected criteria.</p>"
866
 
@@ -874,39 +996,39 @@ def create_air_quality_map_ui():
874
  with gr.Blocks(title="Air Quality Monitoring Stations") as interface:
875
  gr.Markdown("# NOAA Air Quality Monitoring Stations Map")
876
  gr.Markdown("""
877
- This application displays air quality monitoring stations in the United States and shows current air quality readings.
878
 
879
  **Note:** To use the actual EPA AQS API, you need to register for an API key and set
880
  `EPA_AQS_EMAIL` and `EPA_AQS_API_KEY` environment variables in your Hugging Face Space.
881
 
882
- For demonstration without an API key, the app shows sample data for California (CA), New York (NY), and Texas (TX).
883
  """)
884
 
885
  with gr.Row():
886
  with gr.Column(scale=1):
887
- # State dropdown with default value
888
  state_dropdown = gr.Dropdown(
889
- choices=list(app.states.keys()),
890
- label="Select State",
891
- value="CA"
892
  )
893
 
894
- # County dropdown with mock counties for the default state
895
  county_dropdown = gr.Dropdown(
896
- choices=app.mock_get_counties("CA"),
897
- label="Select County (Optional)",
898
  allow_custom_value=True
899
  )
900
 
901
  # Parameter dropdown (pollutant type)
902
  parameter_dropdown = gr.Dropdown(
903
  choices=app.mock_get_parameters(),
904
- label="Select Pollutant (Optional)",
905
  allow_custom_value=True
906
  )
907
 
908
- # Button to generate map and data
909
- map_button = gr.Button("Show Map and Air Quality Data")
910
 
911
  # Create two tabs for the map and data
912
  with gr.Tabs() as tabs:
@@ -930,6 +1052,13 @@ def create_air_quality_map_ui():
930
  inputs=[state_dropdown, county_dropdown, parameter_dropdown],
931
  outputs=[map_html, data_html]
932
  )
 
 
 
 
 
 
 
933
 
934
  return interface
935
 
 
6
  import tempfile
7
  import os
8
  import json
9
+ import time
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
11
 
12
  # Get API credentials from environment variables
13
  EPA_AQS_API_BASE_URL = "https://aqs.epa.gov/data/api"
 
16
 
17
  class AirQualityApp:
18
  def __init__(self):
 
19
  self.states = {
20
  "AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas",
21
  "CA": "California", "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware",
 
68
  "Very Unhealthy": "#99004c", # Purple
69
  "Hazardous": "#7e0023" # Maroon
70
  }
71
+
72
+ # Cache for storing monitored data
73
+ self.all_monitors_cache = {}
74
+ self.all_aqi_data_cache = {}
75
 
76
+ # Load data on initialization
77
+ print("Initializing and loading all monitors data...")
78
+ self.load_all_monitors()
79
+ print("Loading AQI data...")
80
+ self.load_all_aqi_data()
81
+ print("Initialization complete.")
82
+
83
+ def load_all_monitors(self):
84
+ """Load monitors data for all states"""
85
+ # If we don't have API credentials, use mock data
86
+ if not EMAIL or not API_KEY:
87
+ for state_code in self.states.keys():
88
+ self.all_monitors_cache[state_code] = self.mock_get_monitors(state_code)
89
+ return
 
 
 
 
 
 
 
90
 
91
+ # With API credentials, load data for all states using multithreading
92
+ with ThreadPoolExecutor(max_workers=5) as executor:
93
+ future_to_state = {executor.submit(self.get_monitors, state_code): state_code for state_code in self.states.keys()}
94
+ for future in as_completed(future_to_state):
95
+ state_code = future_to_state[future]
96
+ try:
97
+ result = future.result()
98
+ self.all_monitors_cache[state_code] = result
99
+ print(f"Loaded {len(result)} monitors for {state_code}")
100
+ except Exception as e:
101
+ print(f"Error loading monitors for {state_code}: {e}")
102
+ # Fall back to mock data
103
+ self.all_monitors_cache[state_code] = self.mock_get_monitors(state_code)
104
+
105
+ # Sleep briefly to avoid overwhelming the API
106
+ time.sleep(0.5)
107
+
108
+ def load_all_aqi_data(self):
109
+ """Load AQI data for all states"""
110
+ # If we don't have API credentials, use mock data
111
+ if not EMAIL or not API_KEY:
112
+ for state_code in self.states.keys():
113
+ self.all_aqi_data_cache[state_code] = self._generate_mock_aqi_data(state_code)
114
+ return
115
 
116
+ # With API credentials, load data for all states using multithreading
117
+ with ThreadPoolExecutor(max_workers=5) as executor:
118
+ future_to_state = {executor.submit(self.get_latest_aqi, state_code): state_code for state_code in self.states.keys()}
119
+ for future in as_completed(future_to_state):
120
+ state_code = future_to_state[future]
121
+ try:
122
+ result = future.result()
123
+ self.all_aqi_data_cache[state_code] = result
124
+ print(f"Loaded {len(result)} AQI readings for {state_code}")
125
+ except Exception as e:
126
+ print(f"Error loading AQI data for {state_code}: {e}")
127
+ # Fall back to mock data
128
+ self.all_aqi_data_cache[state_code] = self._generate_mock_aqi_data(state_code)
129
+
130
+ # Sleep briefly to avoid overwhelming the API
131
+ time.sleep(0.5)
132
 
133
  def _generate_mock_aqi_data(self, state_code):
134
  """Generate mock AQI data for a state"""
 
140
  # Get numeric state code
141
  numeric_state_code = self.state_code_mapping.get(state_code, "01")
142
 
143
+ # Make mock data for our standard states
144
+ if state_code in ["CA", "NY", "TX"]:
145
+ # Generate data for the most recent 7 days
146
+ for days_ago in range(7):
147
+ # Generate date
148
+ date = (datetime.now() - timedelta(days=days_ago)).strftime("%Y-%m-%d")
149
+
150
+ # Get monitors for this state from cache
151
+ monitors = self.all_monitors_cache.get(state_code, self.mock_get_monitors(state_code))
152
 
153
+ # Generate AQI data for each monitor
154
+ for monitor in monitors:
155
+ county_code = monitor.get("county_code", "001")
156
+ site_number = monitor.get("site_number", "0001")
157
+ parameter_code = monitor.get("parameter_code", "88101")
158
+ parameter_name = monitor.get("parameter_name", "PM2.5 - Local Conditions")
159
 
160
+ # Generate random AQI value (between 0 and 300)
161
+ aqi_value = random.randint(0, 300)
162
+
163
+ aqi_data.append({
164
+ "state_code": numeric_state_code,
165
+ "county_code": county_code,
166
+ "site_number": site_number,
167
+ "parameter_code": parameter_code,
168
+ "parameter_name": parameter_name,
169
+ "date_local": date,
170
+ "aqi": aqi_value
171
+ })
172
+ else:
173
+ # For other states, generate minimal data
174
+ # Current date
175
+ date = datetime.now().strftime("%Y-%m-%d")
176
+
177
+ # Make 2 fake monitors with random AQI values
178
+ aqi_data.append({
179
+ "state_code": numeric_state_code,
180
+ "county_code": "001",
181
+ "site_number": "0001",
182
+ "parameter_code": "88101",
183
+ "parameter_name": "PM2.5 - Local Conditions",
184
+ "date_local": date,
185
+ "aqi": random.randint(0, 300)
186
+ })
187
+
188
+ aqi_data.append({
189
+ "state_code": numeric_state_code,
190
+ "county_code": "001",
191
+ "site_number": "0002",
192
+ "parameter_code": "44201",
193
+ "parameter_name": "Ozone",
194
+ "date_local": date,
195
+ "aqi": random.randint(0, 300)
196
+ })
197
 
198
  return aqi_data
199
 
 
200
  def get_monitors(self, state_code, county_code=None, parameter_code=None):
201
+ """Fetch monitoring stations for a given state and optional county"""
202
+ # Check cache first
203
+ if state_code in self.all_monitors_cache:
204
+ monitors = self.all_monitors_cache[state_code]
205
+
206
+ # Filter by county if provided
207
+ if county_code:
208
+ monitors = [m for m in monitors if m.get("county_code") == county_code]
209
+
210
+ # Filter by parameter if provided
211
+ if parameter_code:
212
+ monitors = [m for m in monitors if m.get("parameter_code") == parameter_code]
213
+
214
+ return monitors
215
+
216
+ # If not in cache, fetch from API
217
  # If we don't have API credentials, use mock data
218
  if not EMAIL or not API_KEY:
219
  return self.mock_get_monitors(state_code, county_code, parameter_code)
 
244
  response = requests.get(endpoint, params=params)
245
  data = response.json()
246
 
247
+ # Handle the specific response structure
 
 
 
 
 
248
  if isinstance(data, dict):
249
  if "Data" in data and isinstance(data["Data"], list):
250
  return data["Data"]
 
264
  return self.mock_get_monitors(state_code, county_code, parameter_code)
265
 
266
  def get_counties(self, state_code):
 
267
  """Fetch counties for a given state"""
268
  # If we don't have API credentials, use mock data
269
  if not EMAIL or not API_KEY:
 
305
  return []
306
 
307
  def get_parameters(self):
 
308
  """Fetch available parameter codes (pollutants)"""
309
  # If we don't have API credentials, use mock data
310
  if not EMAIL or not API_KEY:
 
347
 
348
  def get_latest_aqi(self, state_code, county_code=None, parameter_code=None):
349
  """Fetch the latest AQI data for monitors"""
350
+ # Check cache first
351
+ if state_code in self.all_aqi_data_cache:
352
+ aqi_data = self.all_aqi_data_cache[state_code]
353
+
354
+ # Filter by county if provided
355
+ if county_code:
356
+ aqi_data = [item for item in aqi_data if item.get('county_code') == county_code]
 
 
357
 
358
+ # Filter by parameter if provided
359
+ if parameter_code:
360
+ aqi_data = [item for item in aqi_data if item.get('parameter_code') == parameter_code]
361
 
362
+ return aqi_data
363
+
364
+ # If not in cache, fetch from API
365
+ # If we don't have API credentials, use mock data
366
+ if not EMAIL or not API_KEY:
367
+ return self._generate_mock_aqi_data(state_code)
368
 
369
  # Convert state code to numeric format for API
370
  api_state_code = state_code
 
391
  response = requests.get(endpoint, params=params)
392
  data = response.json()
393
 
 
 
 
 
 
394
  # Handle the specific response structure we observed
395
  aqi_data = []
396
  if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], list):
 
405
  print(f"Error fetching AQI data: {e}")
406
  return []
407
 
408
+ def create_map(self, focus_state=None, county_code=None, parameter_code=None):
409
+ """Create a map with air quality monitoring stations for all states"""
410
+ # Get all monitors - either focused on a state or all states
411
+ all_monitors = []
412
+
413
+ if focus_state:
414
+ # Get monitors just for the focused state
415
+ monitors = self.get_monitors(focus_state, county_code, parameter_code)
416
+ if monitors:
417
+ all_monitors.extend(monitors)
418
+ else:
419
+ # Get all monitors from all states
420
+ for state_code in self.states.keys():
421
+ monitors = self.get_monitors(state_code)
422
+ if monitors:
423
+ all_monitors.extend(monitors)
424
 
425
+ if not all_monitors:
426
  return {"map": "No monitoring stations found for the selected criteria.", "legend": "", "data": None}
427
 
428
  # Convert to DataFrame for easier manipulation
429
+ df = pd.DataFrame(all_monitors)
430
 
431
+ # Create a map centered on the continental US
432
+ if focus_state:
433
+ # Center on the focused state
434
+ center_lat = df["latitude"].mean()
435
+ center_lon = df["longitude"].mean()
436
+ zoom_start = 7
437
+ else:
438
+ # Center on continental US
439
+ center_lat = 39.8283
440
+ center_lon = -98.5795
441
+ zoom_start = 4
 
 
442
 
443
+ # Create a map with a specific width and height
444
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom_start, width='100%', height=700)
445
 
446
  # Add a marker cluster
447
  marker_cluster = MarkerCluster().add_to(m)
448
 
449
+ # Get all AQI data
450
+ all_aqi_data = []
451
+ aqi_data_by_site = {}
452
 
453
+ # Process AQI data for each state
454
+ for state_code in self.states.keys():
455
+ # Skip states we don't need if focusing on a specific state
456
+ if focus_state and state_code != focus_state:
457
+ continue
458
+
459
+ # Get AQI data for this state
460
+ state_aqi_data = self.get_latest_aqi(state_code, county_code, parameter_code)
461
+
462
+ if state_aqi_data:
463
+ all_aqi_data.extend(state_aqi_data)
464
+
465
+ # Create a lookup dictionary by site ID
466
+ for item in state_aqi_data:
467
+ site_id = f"{item['state_code']}-{item['county_code']}-{item['site_number']}"
468
+ if site_id not in aqi_data_by_site:
469
+ aqi_data_by_site[site_id] = []
470
+ aqi_data_by_site[site_id].append(item)
471
 
472
  # Add markers for each monitoring station
473
  for _, row in df.iterrows():
 
477
  color = "blue"
478
 
479
  # Get AQI data for this station if available
480
+ station_aqi_data = aqi_data_by_site.get(site_id, [])
481
  latest_aqi = None
482
  aqi_category = None
483
 
 
532
  # Create popup content with detailed information
533
  popup_content = f"""
534
  <div style="min-width: 300px;">
535
+ <h3>{row.get('local_site_name', 'Monitoring Station')}</h3>
536
  <p><strong>Site ID:</strong> {site_id}</p>
537
  <p><strong>Address:</strong> {row.get('address', 'N/A')}</p>
538
  <p><strong>City:</strong> {row.get('city_name', 'N/A')}</p>
539
  <p><strong>County:</strong> {row.get('county_name', 'N/A')}</p>
540
+ <p><strong>State:</strong> {row.get('state_name', self.states.get(row.get('state_code', ''), 'Unknown'))}</p>
541
+ <p><strong>Parameter:</strong> {row.get('parameter_name', 'N/A')}</p>
542
+ <p><strong>Coordinates:</strong> {row.get('latitude', 'N/A')}, {row.get('longitude', 'N/A')}</p>
543
  {aqi_readings_html}
544
  </div>
545
  """
 
561
  return {
562
  "map": map_html,
563
  "legend": legend_html,
564
+ "data": all_aqi_data
565
  }
566
 
567
  def create_legend_html(self):
 
568
  """Create the HTML for the AQI legend"""
569
  legend_html = """
570
  <div style="padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: white; margin-top: 10px;">
 
583
  return legend_html
584
 
585
  def get_aqi_category(self, aqi_value):
 
586
  """Determine AQI category based on value"""
587
  try:
588
  aqi = int(aqi_value)
 
601
  except (ValueError, TypeError):
602
  return "Unknown"
603
 
604
+ def format_air_quality_data_table(self, aqi_data, state_filter=None, county_filter=None):
605
  """Format air quality data as an HTML table for display"""
606
  if not aqi_data or len(aqi_data) == 0:
607
  return "<p>No air quality data available for the selected criteria.</p>"
608
 
609
+ # Filter by state if provided
610
+ if state_filter:
611
+ # Convert state code if needed
612
+ if len(state_filter) == 2:
613
+ state_filter = self.state_code_mapping.get(state_filter, state_filter)
614
+ aqi_data = [item for item in aqi_data if item.get('state_code') == state_filter]
615
+
616
+ # Filter by county if provided
617
+ if county_filter:
618
+ aqi_data = [item for item in aqi_data if item.get('county_code') == county_filter]
619
+
620
+ if not aqi_data or len(aqi_data) == 0:
621
+ return "<p>No air quality data available for the selected criteria.</p>"
622
+
623
  # Sort by date (most recent first) and then by AQI value (highest first)
624
  sorted_data = sorted(aqi_data,
625
  key=lambda x: (x.get('date_local', ''), -int(x.get('aqi', 0)) if x.get('aqi') and str(x.get('aqi')).isdigit() else 0),
 
642
  <table style="width:100%; border-collapse: collapse;">
643
  <tr style="background-color: #f2f2f2; position: sticky; top: 0;">
644
  <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Date</th>
645
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">State</th>
646
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">County</th>
647
  <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Location</th>
648
  <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Pollutant</th>
649
  <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">AQI</th>
 
655
  for i, item in enumerate(site_data.values()):
656
  date = item.get('date_local', 'N/A')
657
 
658
+ # Get state and county names
659
  state_code = item.get('state_code', 'N/A')
660
+ state_name = 'N/A'
661
+ # Reverse lookup state name
662
+ for code, name in self.state_code_mapping.items():
663
+ if name == state_code:
664
+ state_name = self.states.get(code, 'Unknown')
665
+ break
666
+
667
  county_code = item.get('county_code', 'N/A')
668
  site_number = item.get('site_number', 'N/A')
669
+ location = f"Site {site_number}"
670
 
671
  pollutant = item.get('parameter_name', 'N/A')
672
  aqi_value = item.get('aqi', 'N/A')
 
679
  html += f"""
680
  <tr{row_style}>
681
  <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{date}</td>
682
+ <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{state_name}</td>
683
+ <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{county_code}</td>
684
  <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{location}</td>
685
  <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{pollutant}</td>
686
  <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{aqi_value}</td>
 
695
 
696
  return html
697
 
 
698
  def mock_get_counties(self, state_code):
699
  """Return mock county data for the specified state"""
700
+ # Sample county data for demo
701
+ mock_counties = {
702
+ "CA": [
703
+ {"code": "037", "value": "Los Angeles"},
704
+ {"code": "067", "value": "Sacramento"},
705
+ {"code": "073", "value": "San Diego"},
706
+ {"code": "075", "value": "San Francisco"}
707
+ ],
708
+ "NY": [
709
+ {"code": "061", "value": "New York"},
710
+ {"code": "047", "value": "Kings (Brooklyn)"},
711
+ {"code": "081", "value": "Queens"},
712
+ {"code": "005", "value": "Bronx"}
713
+ ],
714
+ "TX": [
715
+ {"code": "201", "value": "Harris (Houston)"},
716
+ {"code": "113", "value": "Dallas"},
717
+ {"code": "029", "value": "Bexar (San Antonio)"},
718
+ {"code": "453", "value": "Travis (Austin)"}
719
+ ]
720
+ }
721
+
722
+ if state_code in mock_counties:
723
+ counties = mock_counties[state_code]
724
  return [f"{c['code']}: {c['value']}" for c in counties]
725
  else:
726
  # Return generic counties for other states
 
733
 
734
  def mock_get_parameters(self):
735
  """Return mock parameter data"""
736
+ # Sample parameters for demo
737
+ mock_parameters = [
738
+ {"code": "88101", "value_represented": "PM2.5 - Local Conditions"},
739
+ {"code": "44201", "value_represented": "Ozone"},
740
+ {"code": "42401", "value_represented": "Sulfur dioxide"},
741
+ {"code": "42101", "value_represented": "Carbon monoxide"},
742
+ {"code": "42602", "value_represented": "Nitrogen dioxide"},
743
+ {"code": "81102", "value_represented": "PM10 - Local Conditions"}
744
+ ]
745
+
746
+ return [f"{p['code']}: {p['value_represented']}" for p in mock_parameters]
747
 
748
  def mock_get_monitors(self, state_code, county_code=None, parameter_code=None):
749
  """Mock function to return sample data for development"""
 
909
  "parameter_code": "88101",
910
  "parameter_name": "PM2.5 - Local Conditions",
911
  "poc": 1,
912
+ "latitude": 40.0 + float(ord(state_code[0]) % 10) / 10,
913
+ "longitude": -90.0 - float(ord(state_code[0]) % 10) / 10,
914
  "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 1",
915
  "address": "123 Main Street",
916
  "city_name": "City 1",
 
925
  "parameter_code": "44201",
926
  "parameter_name": "Ozone",
927
  "poc": 1,
928
+ "latitude": 40.5 + float(ord(state_code[0]) % 10) / 10,
929
+ "longitude": -90.5 - float(ord(state_code[0]) % 10) / 10,
930
  "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 2",
931
  "address": "456 Oak Street",
932
  "city_name": "City 2",
 
946
 
947
  return monitors
948
 
949
+ # Create the new UI with the nationwide map
950
  def create_air_quality_map_ui():
951
+ """Create the Gradio interface for the Air Quality Map application with nationwide data preloaded"""
952
  app = AirQualityApp()
953
 
954
  def update_counties(state_code):
 
956
  counties = app.get_counties(state_code)
957
  return gr.Dropdown(choices=counties)
958
 
959
+ def show_map_and_data(state=None, county=None, parameter=None):
960
  """Callback to generate and display both the map and the air quality data"""
961
  # Extract code from county string if provided
962
  county_code = None
 
968
  if parameter and ":" in parameter:
969
  parameter_code = parameter.split(":")[0].strip()
970
 
971
+ # Generate the map and get data - focus on state if selected
972
  result = app.create_map(state, county_code, parameter_code)
973
 
974
  if isinstance(result, dict):
 
982
 
983
  # Process air quality data for the separate panel
984
  if result["data"]:
985
+ data_html = app.format_air_quality_data_table(result["data"], state, county_code)
986
  else:
987
  data_html = "<p>No air quality data available for the selected criteria.</p>"
988
 
 
996
  with gr.Blocks(title="Air Quality Monitoring Stations") as interface:
997
  gr.Markdown("# NOAA Air Quality Monitoring Stations Map")
998
  gr.Markdown("""
999
+ This application displays air quality monitoring stations across the United States and shows current air quality readings.
1000
 
1001
  **Note:** To use the actual EPA AQS API, you need to register for an API key and set
1002
  `EPA_AQS_EMAIL` and `EPA_AQS_API_KEY` environment variables in your Hugging Face Space.
1003
 
1004
+ For demonstration without an API key, the app shows sample data with more detailed information for California (CA), New York (NY), and Texas (TX).
1005
  """)
1006
 
1007
  with gr.Row():
1008
  with gr.Column(scale=1):
1009
+ # State dropdown with empty default (all states)
1010
  state_dropdown = gr.Dropdown(
1011
+ choices=[""] + list(app.states.keys()),
1012
+ label="Filter by State (Optional)",
1013
+ value=""
1014
  )
1015
 
1016
+ # County dropdown (initially empty)
1017
  county_dropdown = gr.Dropdown(
1018
+ choices=[],
1019
+ label="Filter by County (Optional)",
1020
  allow_custom_value=True
1021
  )
1022
 
1023
  # Parameter dropdown (pollutant type)
1024
  parameter_dropdown = gr.Dropdown(
1025
  choices=app.mock_get_parameters(),
1026
+ label="Filter by Pollutant (Optional)",
1027
  allow_custom_value=True
1028
  )
1029
 
1030
+ # Button to update filters
1031
+ map_button = gr.Button("Update Filters")
1032
 
1033
  # Create two tabs for the map and data
1034
  with gr.Tabs() as tabs:
 
1052
  inputs=[state_dropdown, county_dropdown, parameter_dropdown],
1053
  outputs=[map_html, data_html]
1054
  )
1055
+
1056
+ # Load initial map when the app starts
1057
+ interface.load(
1058
+ fn=show_map_and_data,
1059
+ inputs=None,
1060
+ outputs=[map_html, data_html]
1061
+ )
1062
 
1063
  return interface
1064