nakas commited on
Commit
992fd7b
·
verified ·
1 Parent(s): a822eed

Update air_quality_map.py

Browse files
Files changed (1) hide show
  1. air_quality_map.py +198 -186
air_quality_map.py CHANGED
@@ -241,7 +241,7 @@ 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
@@ -285,20 +285,68 @@ class AirQualityApp:
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)
@@ -306,31 +354,40 @@ 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():
335
  site_id = f"{row['state_code']}-{row['county_code']}-{row['site_number']}"
336
 
@@ -339,12 +396,22 @@ class AirQualityApp:
339
 
340
  # Get AQI data for this station if available
341
  station_aqi_data = aqi_data.get(site_id, [])
342
- latest_aqi = None
343
- aqi_category = None
344
 
345
- # Create a table of pollutant readings if available
346
- aqi_readings_html = ""
 
 
 
 
 
 
 
 
 
 
 
347
 
 
348
  if station_aqi_data:
349
  # Sort by date (most recent first)
350
  station_aqi_data.sort(key=lambda x: x.get('date_local', ''), reverse=True)
@@ -354,74 +421,88 @@ class AirQualityApp:
354
  latest_aqi = station_aqi_data[0].get('aqi')
355
  aqi_category = self.get_aqi_category(latest_aqi)
356
  color = self.aqi_categories.get(aqi_category, "blue")
357
-
358
- # Create a table of readings
359
- aqi_readings_html = """
360
- <h4>Recent Air Quality Readings</h4>
361
- <table style="width:100%; border-collapse: collapse; margin-top: 10px;">
362
- <tr style="background-color: #f2f2f2;">
363
- <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Date</th>
364
- <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Pollutant</th>
365
- <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">AQI</th>
366
- <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Category</th>
367
- </tr>
368
- """
369
-
370
- # Add up to 10 most recent readings
371
- for i, reading in enumerate(station_aqi_data[:10]):
372
- date = reading.get('date_local', 'N/A')
373
- pollutant = reading.get('parameter_name', 'N/A')
374
- aqi_value = reading.get('aqi', 'N/A')
375
- category = self.get_aqi_category(aqi_value) if aqi_value and aqi_value != 'N/A' else 'N/A'
376
-
377
- row_style = ' style="background-color: #f2f2f2;"' if i % 2 == 0 else ''
378
- aqi_readings_html += f"""
379
- <tr{row_style}>
380
- <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{date}</td>
381
- <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{pollutant}</td>
382
- <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{aqi_value}</td>
383
- <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{category}</td>
384
- </tr>
385
- """
386
-
387
- aqi_readings_html += "</table>"
388
-
389
- # If there are more readings than what we showed
390
- if len(station_aqi_data) > 10:
391
- aqi_readings_html += f"<p><em>Showing 10 of {len(station_aqi_data)} readings</em></p>"
392
 
393
- # Create popup content with detailed information
394
  popup_content = f"""
395
- <div style="min-width: 300px;">
396
- <h3>{row['local_site_name']}</h3>
397
  <p><strong>Site ID:</strong> {site_id}</p>
398
- <p><strong>Address:</strong> {row.get('address', 'N/A')}</p>
399
- <p><strong>City:</strong> {row.get('city_name', 'N/A')}</p>
400
- <p><strong>County:</strong> {row.get('county_name', 'N/A')}</p>
401
- <p><strong>State:</strong> {row.get('state_name', 'N/A')}</p>
402
- <p><strong>Parameter:</strong> {row['parameter_name']}</p>
403
- <p><strong>Coordinates:</strong> {row['latitude']}, {row['longitude']}</p>
404
- {aqi_readings_html}
405
  </div>
406
  """
407
 
408
- # Create a larger popup for detailed data
409
- popup = folium.Popup(popup_content, max_width=500)
410
-
411
- # Add marker to cluster
412
  folium.Marker(
413
  location=[row["latitude"], row["longitude"]],
414
  popup=popup,
415
  icon=folium.Icon(color=color, icon="cloud"),
416
  ).add_to(marker_cluster)
417
 
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,19 +524,22 @@ class AirQualityApp:
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"""
@@ -488,6 +572,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 = [
@@ -503,6 +588,8 @@ class AirQualityApp:
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"
@@ -519,6 +606,8 @@ class AirQualityApp:
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"
@@ -535,6 +624,8 @@ class AirQualityApp:
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"
@@ -551,6 +642,8 @@ class AirQualityApp:
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"
@@ -571,6 +664,8 @@ class AirQualityApp:
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"
@@ -587,6 +682,8 @@ class AirQualityApp:
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"
@@ -607,6 +704,8 @@ class AirQualityApp:
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"
@@ -623,6 +722,8 @@ class AirQualityApp:
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"
@@ -643,6 +744,8 @@ class AirQualityApp:
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"
@@ -659,6 +762,8 @@ class AirQualityApp:
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"
@@ -673,97 +778,4 @@ class AirQualityApp:
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
- # Return the combined HTML
704
- return result["map"]
705
- else:
706
- # Return error message or whatever was returned
707
- return result
708
-
709
- # Create the UI
710
- with gr.Blocks(title="Air Quality Monitoring Stations") as interface:
711
- gr.Markdown("# NOAA Air Quality Monitoring Stations Map")
712
- gr.Markdown("""
713
- This application displays air quality monitoring stations in the United States.
714
-
715
- **Note:** To use the actual EPA AQS API, you need to register for an API key and set
716
- `EPA_AQS_EMAIL` and `EPA_AQS_API_KEY` environment variables in your Hugging Face Space.
717
-
718
- For demonstration without an API key, the app shows sample data for California (CA), New York (NY), and Texas (TX).
719
- """)
720
-
721
- with gr.Row():
722
- with gr.Column(scale=1):
723
- # State dropdown with default value
724
- state_dropdown = gr.Dropdown(
725
- choices=list(app.states.keys()),
726
- label="Select State",
727
- value="CA"
728
- )
729
-
730
- # County dropdown with mock counties for the default state
731
- county_dropdown = gr.Dropdown(
732
- choices=app.mock_get_counties("CA"),
733
- label="Select County (Optional)",
734
- allow_custom_value=True
735
- )
736
-
737
- # Parameter dropdown (pollutant type)
738
- parameter_dropdown = gr.Dropdown(
739
- choices=app.mock_get_parameters(),
740
- label="Select Pollutant (Optional)",
741
- allow_custom_value=True
742
- )
743
-
744
- # Button to generate map
745
- map_button = gr.Button("Show Map")
746
-
747
- # HTML component to display the map in a larger column
748
- with gr.Column(scale=3):
749
- map_html = gr.HTML(label="Air Quality Monitoring Stations Map")
750
-
751
- # Set up event handlers
752
- state_dropdown.change(
753
- fn=update_counties,
754
- inputs=state_dropdown,
755
- outputs=county_dropdown
756
- )
757
-
758
- map_button.click(
759
- fn=show_map,
760
- inputs=[state_dropdown, county_dropdown, parameter_dropdown],
761
- outputs=map_html
762
- )
763
-
764
- return interface
765
-
766
- # Create and launch the app
767
- if __name__ == "__main__":
768
- air_quality_map_ui = create_air_quality_map_ui()
769
- air_quality_map_ui.launch()
 
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 self.mock_get_aqi_data(state_code, county_code, parameter_code)
245
 
246
  # Convert state code to numeric format for API
247
  api_state_code = state_code
 
285
  return aqi_data
286
  except Exception as e:
287
  print(f"Error fetching AQI data: {e}")
288
+ return self.mock_get_aqi_data(state_code, county_code, parameter_code)
289
 
290
+ def mock_get_aqi_data(self, state_code, county_code=None, parameter_code=None):
291
+ """Generate mock AQI data for demonstration"""
292
+ # Get monitors first
293
+ monitors = self.mock_get_monitors(state_code, county_code, parameter_code)
294
+
295
+ # Create mock AQI data for each monitor
296
+ mock_aqi_data = []
297
+
298
+ for monitor in monitors:
299
+ # Create 5 days of mock readings for each monitor
300
+ for i in range(5):
301
+ # Base AQI value depends on the parameter
302
+ base_aqi = 0
303
+ if monitor.get("parameter_code") == "88101": # PM2.5
304
+ base_aqi = 35
305
+ elif monitor.get("parameter_code") == "44201": # Ozone
306
+ base_aqi = 45
307
+ elif monitor.get("parameter_code") == "42401": # SO2
308
+ base_aqi = 25
309
+ else:
310
+ base_aqi = 30
311
+
312
+ # Vary the AQI by +/- 20 points randomly
313
+ import random
314
+ aqi_value = max(0, min(300, base_aqi + random.randint(-20, 20)))
315
+
316
+ # Date is "2024-04-XX" where XX starts from 10 and goes back
317
+ date = f"2024-04-{14-i:02d}"
318
+
319
+ mock_aqi_data.append({
320
+ "state_code": monitor.get("state_code"),
321
+ "county_code": monitor.get("county_code"),
322
+ "site_number": monitor.get("site_number"),
323
+ "parameter_code": monitor.get("parameter_code"),
324
+ "parameter_name": monitor.get("parameter_name"),
325
+ "date_local": date,
326
+ "aqi": aqi_value,
327
+ "category": self.get_aqi_category(aqi_value),
328
+ "city_name": monitor.get("city_name", "Unknown"),
329
+ "local_site_name": monitor.get("local_site_name", "Unknown")
330
+ })
331
+
332
+ return mock_aqi_data
333
+
334
+ def create_map_and_data(self, state_code, county_code=None, parameter_code=None):
335
+ """Create a map with air quality monitoring stations and separate data for display"""
336
+ # Get monitors (don't pass county_code to API)
337
  monitors = self.get_monitors(state_code, parameter_code=parameter_code)
338
 
339
  if not monitors:
340
+ return {
341
+ "map": "No monitoring stations found for the selected criteria.",
342
+ "data_html": "",
343
+ "legend": ""
344
+ }
345
 
346
  # Convert to DataFrame for easier manipulation
347
  df = pd.DataFrame(monitors)
348
 
349
+ # Filter by county if provided - after getting the monitors
350
  if county_code:
351
  print(f"Filtering by county_code: {county_code}")
352
  county_code_str = str(county_code)
 
354
  print(f"After filtering, {len(df)} monitors remain")
355
 
356
  if len(df) == 0:
357
+ return {
358
+ "map": "No monitoring stations found for the selected county.",
359
+ "data_html": "",
360
+ "legend": ""
361
+ }
362
 
363
  # Create a map centered on the mean latitude and longitude
364
  center_lat = df["latitude"].mean()
365
  center_lon = df["longitude"].mean()
366
 
367
+ # Create a map with a specific width and height
368
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=7, width='100%', height=600)
369
 
370
  # Add a marker cluster
371
  marker_cluster = MarkerCluster().add_to(m)
372
 
373
+ # Get latest AQI data
 
374
  if EMAIL and API_KEY:
 
375
  aqi_results = self.get_latest_aqi(state_code, parameter_code=parameter_code)
376
+ else:
377
+ aqi_results = self.mock_get_aqi_data(state_code, county_code, parameter_code)
378
+
379
+ # Create a lookup dictionary by site ID
380
+ aqi_data = {}
381
+ for item in aqi_results:
382
+ site_id = f"{item['state_code']}-{item['county_code']}-{item['site_number']}"
383
+ if site_id not in aqi_data:
384
+ aqi_data[site_id] = []
385
+ aqi_data[site_id].append(item)
386
+
387
+ # Initialize a list to collect all air quality readings for display in UI
388
+ all_readings = []
389
+
390
+ # Add markers for each monitoring station with minimal popup content
391
  for _, row in df.iterrows():
392
  site_id = f"{row['state_code']}-{row['county_code']}-{row['site_number']}"
393
 
 
396
 
397
  # Get AQI data for this station if available
398
  station_aqi_data = aqi_data.get(site_id, [])
 
 
399
 
400
+ # Add all readings from this station to the collected data
401
+ for reading in station_aqi_data:
402
+ all_readings.append({
403
+ "site_id": site_id,
404
+ "site_name": row['local_site_name'],
405
+ "city": row.get('city_name', 'N/A'),
406
+ "county": row.get('county_name', 'N/A'),
407
+ "state": row.get('state_name', 'N/A'),
408
+ "date": reading.get('date_local', 'N/A'),
409
+ "pollutant": reading.get('parameter_name', 'N/A'),
410
+ "aqi": reading.get('aqi', 'N/A'),
411
+ "category": self.get_aqi_category(reading.get('aqi', 0)) if reading.get('aqi') else 'N/A'
412
+ })
413
 
414
+ # Set marker color based on latest AQI if available
415
  if station_aqi_data:
416
  # Sort by date (most recent first)
417
  station_aqi_data.sort(key=lambda x: x.get('date_local', ''), reverse=True)
 
421
  latest_aqi = station_aqi_data[0].get('aqi')
422
  aqi_category = self.get_aqi_category(latest_aqi)
423
  color = self.aqi_categories.get(aqi_category, "blue")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
+ # Create simple popup content with just station name and link to data
426
  popup_content = f"""
427
+ <div>
428
+ <h4>{row['local_site_name']}</h4>
429
  <p><strong>Site ID:</strong> {site_id}</p>
430
+ <p>See detailed data in the panel below.</p>
 
 
 
 
 
 
431
  </div>
432
  """
433
 
434
+ # Add marker to cluster with simple popup
435
+ popup = folium.Popup(popup_content, max_width=300)
 
 
436
  folium.Marker(
437
  location=[row["latitude"], row["longitude"]],
438
  popup=popup,
439
  icon=folium.Icon(color=color, icon="cloud"),
440
  ).add_to(marker_cluster)
441
 
442
+ # Sort all readings by date (most recent first)
443
+ all_readings.sort(key=lambda x: x.get('date', ''), reverse=True)
444
+
445
+ # Convert all readings to HTML table for display in UI
446
+ data_html = self.create_readings_table_html(all_readings)
447
+
448
+ # Return map HTML, data HTML, and legend HTML separately
449
+ return {
450
+ "map": m._repr_html_(),
451
+ "data_html": data_html,
452
+ "legend": self.create_legend_html()
453
+ }
454
+
455
+ def create_readings_table_html(self, readings):
456
+ """Create an HTML table of air quality readings"""
457
+ if not readings:
458
+ return "<p>No air quality data available for the selected criteria.</p>"
459
+
460
+ html = """
461
+ <div style="max-height: 500px; overflow-y: auto;">
462
+ <h3>Air Quality Readings</h3>
463
+ <table style="width:100%; border-collapse: collapse; margin-top: 10px;">
464
+ <tr style="background-color: #f2f2f2; position: sticky; top: 0;">
465
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Station</th>
466
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Location</th>
467
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Date</th>
468
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Pollutant</th>
469
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">AQI</th>
470
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Category</th>
471
+ </tr>
472
+ """
473
+
474
+ for i, reading in enumerate(readings):
475
+ # Get background color for AQI category
476
+ category = reading.get('category', 'N/A')
477
+ bg_color = self.aqi_legend_colors.get(category, "#ffffff")
478
+
479
+ # For better readability, use a lighter version of the color
480
+ if category != 'Good' and category != 'N/A':
481
+ # Add alpha transparency to the color
482
+ bg_color = bg_color + "40" # 40 is 25% opacity in hex
483
+
484
+ # Alternate row colors for better readability
485
+ row_style = ' style="background-color: #f9f9f9;"' if i % 2 == 0 else ''
486
+
487
+ location = f"{reading.get('city', 'N/A')}, {reading.get('state', 'N/A')}"
488
+
489
+ html += f"""
490
+ <tr{row_style}>
491
+ <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{reading.get('site_name', 'N/A')}</td>
492
+ <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{location}</td>
493
+ <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{reading.get('date', 'N/A')}</td>
494
+ <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{reading.get('pollutant', 'N/A')}</td>
495
+ <td style="padding: 8px; text-align: left; border: 1px solid #ddd;">{reading.get('aqi', 'N/A')}</td>
496
+ <td style="padding: 8px; text-align: left; border: 1px solid #ddd; background-color: {bg_color};">{category}</td>
497
+ </tr>
498
+ """
499
 
500
+ html += """
501
+ </table>
502
+ </div>
503
+ """
504
 
505
+ return html
506
 
507
  def create_legend_html(self):
508
  """Create the HTML for the AQI legend"""
 
524
 
525
  def get_aqi_category(self, aqi_value):
526
  """Determine AQI category based on value"""
527
+ try:
528
+ aqi = int(aqi_value)
529
+ if aqi <= 50:
530
+ return "Good"
531
+ elif aqi <= 100:
532
+ return "Moderate"
533
+ elif aqi <= 150:
534
+ return "Unhealthy for Sensitive Groups"
535
+ elif aqi <= 200:
536
+ return "Unhealthy"
537
+ elif aqi <= 300:
538
+ return "Very Unhealthy"
539
+ else:
540
+ return "Hazardous"
541
+ except (ValueError, TypeError):
542
+ return "N/A"
543
 
544
  def mock_get_counties(self, state_code):
545
  """Return mock county data for the specified state"""
 
572
  numeric_state_code = state_code_mapping.get(state_code, "01") # Default to "01" if not found
573
  else:
574
  numeric_state_code = state_code
575
+
576
  # Sample data for California
577
  if state_code == "CA" or numeric_state_code == "06":
578
  monitors = [
 
588
  "local_site_name": "Los Angeles - North Main Street",
589
  "address": "1630 North Main Street",
590
  "city_name": "Los Angeles",
591
+ "county_name": "Los Angeles",
592
+ "state_name": "California",
593
  "cbsa_name": "Los Angeles-Long Beach-Anaheim",
594
  "date_established": "1998-01-01",
595
  "last_sample_date": "2024-04-10"
 
606
  "local_site_name": "Los Angeles - North Main Street",
607
  "address": "1630 North Main Street",
608
  "city_name": "Los Angeles",
609
+ "county_name": "Los Angeles",
610
+ "state_name": "California",
611
  "cbsa_name": "Los Angeles-Long Beach-Anaheim",
612
  "date_established": "1998-01-01",
613
  "last_sample_date": "2024-04-10"
 
624
  "local_site_name": "Sacramento - T Street",
625
  "address": "1309 T Street",
626
  "city_name": "Sacramento",
627
+ "county_name": "Sacramento",
628
+ "state_name": "California",
629
  "cbsa_name": "Sacramento-Roseville",
630
  "date_established": "1999-03-01",
631
  "last_sample_date": "2024-04-10"
 
642
  "local_site_name": "San Diego - Beardsley Street",
643
  "address": "1110 Beardsley Street",
644
  "city_name": "San Diego",
645
+ "county_name": "San Diego",
646
+ "state_name": "California",
647
  "cbsa_name": "San Diego-Carlsbad",
648
  "date_established": "1999-04-15",
649
  "last_sample_date": "2024-04-10"
 
664
  "local_site_name": "New York - PS 59",
665
  "address": "228 East 57th Street",
666
  "city_name": "New York",
667
+ "county_name": "New York",
668
+ "state_name": "New York",
669
  "cbsa_name": "New York-Newark-Jersey City",
670
  "date_established": "1999-07-15",
671
  "last_sample_date": "2024-04-10"
 
682
  "local_site_name": "New York - IS 52",
683
  "address": "681 Kelly Street",
684
  "city_name": "Bronx",
685
+ "county_name": "Bronx",
686
+ "state_name": "New York",
687
  "cbsa_name": "New York-Newark-Jersey City",
688
  "date_established": "1998-01-01",
689
  "last_sample_date": "2024-04-10"
 
704
  "local_site_name": "Houston - Clinton Drive",
705
  "address": "9525 Clinton Drive",
706
  "city_name": "Houston",
707
+ "county_name": "Harris",
708
+ "state_name": "Texas",
709
  "cbsa_name": "Houston-The Woodlands-Sugar Land",
710
  "date_established": "1997-09-01",
711
  "last_sample_date": "2024-04-10"
 
722
  "local_site_name": "Dallas - Hinton Street",
723
  "address": "1415 Hinton Street",
724
  "city_name": "Dallas",
725
+ "county_name": "Dallas",
726
+ "state_name": "Texas",
727
  "cbsa_name": "Dallas-Fort Worth-Arlington",
728
  "date_established": "1998-01-01",
729
  "last_sample_date": "2024-04-10"
 
744
  "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 1",
745
  "address": "123 Main Street",
746
  "city_name": "City 1",
747
+ "county_name": "County 1",
748
+ "state_name": self.states.get(state_code, "Unknown"),
749
  "cbsa_name": f"{self.states.get(state_code, 'Unknown')} Metro Area",
750
  "date_established": "2000-01-01",
751
  "last_sample_date": "2024-04-10"
 
762
  "local_site_name": f"{self.states.get(state_code, 'Unknown')} - Station 2",
763
  "address": "456 Oak Street",
764
  "city_name": "City 2",
765
+ "county_name": "County 2",
766
+ "state_name": self.states.get(state_code, "Unknown"),
767
  "cbsa_name": f"{self.states.get(state_code, 'Unknown')} Metro Area",
768
  "date_established": "2000-01-01",
769
  "last_sample_date": "2024-04-10"
 
778
  if parameter_code:
779
  monitors = [m for m in monitors if m["parameter_code"] == parameter_code]
780
 
781
+ return monitors