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

Update air_quality_map.py

Browse files
Files changed (1) hide show
  1. air_quality_map.py +213 -49
air_quality_map.py CHANGED
@@ -14,6 +14,7 @@ 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",
@@ -98,9 +99,61 @@ class AirQualityApp:
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)
@@ -154,8 +207,9 @@ class AirQualityApp:
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:
@@ -195,8 +249,9 @@ class AirQualityApp:
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
202
  if not EMAIL or not API_KEY:
@@ -241,7 +296,20 @@ class AirQualityApp:
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:
244
- return [] # We don't have mock AQI data for simplicity
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  # Convert state code to numeric format for API
247
  api_state_code = state_code
@@ -293,7 +361,7 @@ class AirQualityApp:
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)
@@ -306,29 +374,32 @@ class AirQualityApp:
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']}"
329
- if site_id not in aqi_data:
330
- aqi_data[site_id] = []
331
- aqi_data[site_id].append(item)
 
 
 
332
 
333
  # Add markers for each monitoring station
334
  for _, row in df.iterrows():
@@ -415,15 +486,18 @@ class AirQualityApp:
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"""
428
  legend_html = """
429
  <div style="padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: white; margin-top: 10px;">
@@ -442,21 +516,95 @@ class AirQualityApp:
442
  return legend_html
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:
@@ -488,6 +636,7 @@ class AirQualityApp:
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 = [
@@ -675,8 +824,9 @@ class AirQualityApp:
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):
@@ -684,8 +834,8 @@ def create_air_quality_map_ui():
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:
@@ -696,27 +846,35 @@ def create_air_quality_map_ui():
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.
@@ -747,12 +905,18 @@ def create_air_quality_map_ui():
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(
@@ -762,9 +926,9 @@ def create_air_quality_map_ui():
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
 
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",
 
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"""
112
+ import random
113
+ from datetime import datetime, timedelta
114
+
115
+ aqi_data = []
116
+
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)
 
207
  except Exception as e:
208
  print(f"Error fetching monitors: {e}")
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:
 
249
  except Exception as e:
250
  print(f"Error fetching counties: {e}")
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:
 
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
 
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)
 
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():
 
486
  icon=folium.Icon(color=color, icon="cloud"),
487
  ).add_to(marker_cluster)
488
 
489
+ # Return map HTML, legend HTML, and data for the separate panel
490
  map_html = m._repr_html_()
 
 
491
  legend_html = self.create_legend_html()
492
 
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
  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)
523
+ if aqi <= 50:
524
+ return "Good"
525
+ elif aqi <= 100:
526
+ return "Moderate"
527
+ elif aqi <= 150:
528
+ return "Unhealthy for Sensitive Groups"
529
+ elif aqi <= 200:
530
+ return "Unhealthy"
531
+ elif aqi <= 300:
532
+ return "Very Unhealthy"
533
+ else:
534
+ return "Hazardous"
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),
546
+ reverse=True)
547
+
548
+ # Group by location to show the latest readings for each site
549
+ site_data = {}
550
+ for item in sorted_data:
551
+ site_id = f"{item.get('state_code', '')}-{item.get('county_code', '')}-{item.get('site_number', '')}"
552
+ param = item.get('parameter_code', '')
553
+ key = f"{site_id}-{param}"
554
+
555
+ if key not in site_data:
556
+ site_data[key] = item
557
+
558
+ # Create HTML table
559
+ html = """
560
+ <div style="max-height: 500px; overflow-y: auto;">
561
+ <h3>Latest Air Quality Readings</h3>
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>
568
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Category</th>
569
+ </tr>
570
+ """
571
+
572
+ # Add rows for each site's latest readings
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')
584
+ category = self.get_aqi_category(aqi_value)
585
+
586
+ # Get appropriate color for the AQI category
587
+ category_color = self.aqi_legend_colors.get(category, "#cccccc")
588
+
589
+ row_style = ' style="background-color: #f9f9f9;"' if i % 2 == 0 else ''
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>
596
+ <td style="padding: 8px; text-align: left; border: 1px solid #ddd; background-color: {category_color};">{category}</td>
597
+ </tr>
598
+ """
599
+
600
+ html += """
601
+ </table>
602
+ </div>
603
+ """
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:
 
636
  numeric_state_code = state_code_mapping.get(state_code, "01") # Default to "01" if not found
637
  else:
638
  numeric_state_code = state_code
639
+
640
  # Sample data for California
641
  if state_code == "CA" or numeric_state_code == "06":
642
  monitors = [
 
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
  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
841
  if county and ":" in county:
 
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):
853
+ # Process map HTML
854
+ map_html = f"""
855
  <div>
856
  {result["map"]}
857
  {result["legend"]}
858
  </div>
859
  """
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
+
867
+ return map_html, data_html
868
  else:
869
  # Return error message or whatever was returned
870
+ error_message = result if isinstance(result, str) else "An error occurred"
871
+ return error_message, "<p>No data available</p>"
872
 
873
  # Create the 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.
 
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:
913
+ with gr.TabItem("Map"):
914
+ # HTML component to display the map
915
  map_html = gr.HTML(label="Air Quality Monitoring Stations Map")
916
+
917
+ with gr.TabItem("Air Quality Data"):
918
+ # HTML component to display the air quality data
919
+ data_html = gr.HTML(label="Air Quality Readings")
920
 
921
  # Set up event handlers
922
  state_dropdown.change(
 
926
  )
927
 
928
  map_button.click(
929
+ fn=show_map_and_data,
930
  inputs=[state_dropdown, county_dropdown, parameter_dropdown],
931
+ outputs=[map_html, data_html]
932
  )
933
 
934
  return interface