Spaces:
Sleeping
Sleeping
Update air_quality_map.py
Browse files- air_quality_map.py +6 -385
air_quality_map.py
CHANGED
@@ -561,390 +561,6 @@ class AirQualityApp:
|
|
561 |
"city_name": "New York",
|
562 |
"county_name": "New York",
|
563 |
"state_name": "New York",
|
564 |
-
"cbsa_name": "New York-Newark-Jersey City",
|
565 |
-
"date_established": "1998-01-01",
|
566 |
-
"last_sample_date": "2024-04-10"
|
567 |
-
}
|
568 |
-
]
|
569 |
-
|
570 |
-
# Texas monitors (South)
|
571 |
-
tx_monitors = [
|
572 |
-
{
|
573 |
-
"state_code": "48",
|
574 |
-
"county_code": "201",
|
575 |
-
"site_number": "0024",
|
576 |
-
"parameter_code": "88101",
|
577 |
-
"parameter_name": "PM2.5 - Local Conditions",
|
578 |
-
"poc": 1,
|
579 |
-
"latitude": 29.7349,
|
580 |
-
"longitude": -95.3063,
|
581 |
-
"local_site_name": "Houston - Clinton Drive",
|
582 |
-
"address": "9525 Clinton Drive",
|
583 |
-
"city_name": "Houston",
|
584 |
-
"county_name": "Harris",
|
585 |
-
"state_name": "Texas",
|
586 |
-
"cbsa_name": "Houston-The Woodlands-Sugar Land",
|
587 |
-
"date_established": "1997-09-01",
|
588 |
-
"last_sample_date": "2024-04-10"
|
589 |
-
},
|
590 |
-
{
|
591 |
-
"state_code": "48",
|
592 |
-
"county_code": "113",
|
593 |
-
"site_number": "0050",
|
594 |
-
"parameter_code": "44201",
|
595 |
-
"parameter_name": "Ozone",
|
596 |
-
"poc": 1,
|
597 |
-
"latitude": 32.8198,
|
598 |
-
"longitude": -96.8602,
|
599 |
-
"local_site_name": "Dallas - Hinton Street",
|
600 |
-
"address": "1415 Hinton Street",
|
601 |
-
"city_name": "Dallas",
|
602 |
-
"county_name": "Dallas",
|
603 |
-
"state_name": "Texas",
|
604 |
-
"cbsa_name": "Dallas-Fort Worth-Arlington",
|
605 |
-
"date_established": "1998-01-01",
|
606 |
-
"last_sample_date": "2024-04-10"
|
607 |
-
}
|
608 |
-
]
|
609 |
-
|
610 |
-
# Midwest monitors
|
611 |
-
midwest_monitors = [
|
612 |
-
{
|
613 |
-
"state_code": "17",
|
614 |
-
"county_code": "031",
|
615 |
-
"site_number": "0076",
|
616 |
-
"parameter_code": "88101",
|
617 |
-
"parameter_name": "PM2.5 - Local Conditions",
|
618 |
-
"poc": 1,
|
619 |
-
"latitude": 41.7513,
|
620 |
-
"longitude": -87.7130,
|
621 |
-
"local_site_name": "Chicago - Ashburn",
|
622 |
-
"address": "3535 West 110th Street",
|
623 |
-
"city_name": "Chicago",
|
624 |
-
"county_name": "Cook",
|
625 |
-
"state_name": "Illinois",
|
626 |
-
"cbsa_name": "Chicago-Naperville-Elgin",
|
627 |
-
"date_established": "1999-01-01",
|
628 |
-
"last_sample_date": "2024-04-10"
|
629 |
-
},
|
630 |
-
{
|
631 |
-
"state_code": "26",
|
632 |
-
"county_code": "163",
|
633 |
-
"site_number": "0015",
|
634 |
-
"parameter_code": "88101",
|
635 |
-
"parameter_name": "PM2.5 - Local Conditions",
|
636 |
-
"poc": 1,
|
637 |
-
"latitude": 42.3328,
|
638 |
-
"longitude": -83.1501,
|
639 |
-
"local_site_name": "Detroit - W 7 Mile",
|
640 |
-
"address": "10748 West 7 Mile Road",
|
641 |
-
"city_name": "Detroit",
|
642 |
-
"county_name": "Wayne",
|
643 |
-
"state_name": "Michigan",
|
644 |
-
"cbsa_name": "Detroit-Warren-Dearborn",
|
645 |
-
"date_established": "1999-01-01",
|
646 |
-
"last_sample_date": "2024-04-10"
|
647 |
-
}
|
648 |
-
]
|
649 |
-
|
650 |
-
# Combine all regions
|
651 |
-
all_monitors.extend(ca_monitors)
|
652 |
-
all_monitors.extend(ny_monitors)
|
653 |
-
all_monitors.extend(tx_monitors)
|
654 |
-
all_monitors.extend(midwest_monitors)
|
655 |
-
|
656 |
-
# Filter by coordinates
|
657 |
-
filtered_monitors = []
|
658 |
-
for monitor in all_monitors:
|
659 |
-
lat = float(monitor.get("latitude", 0))
|
660 |
-
lon = float(monitor.get("longitude", 0))
|
661 |
-
if min_lat <= lat <= max_lat and min_lon <= lon <= max_lon:
|
662 |
-
filtered_monitors.append(monitor)
|
663 |
-
|
664 |
-
# Filter by parameter code if provided
|
665 |
-
if parameter_code:
|
666 |
-
filtered_monitors = [m for m in filtered_monitors if m["parameter_code"] == parameter_code]
|
667 |
-
|
668 |
-
return filtered_monitors
|
669 |
-
|
670 |
-
def create_bound_event_handler(self):
|
671 |
-
"""Create a custom JavaScript element to handle map events and load data dynamically"""
|
672 |
-
|
673 |
-
class BoundEventHandler(MacroElement):
|
674 |
-
"""
|
675 |
-
An element that will be added to the map to handle events.
|
676 |
-
This is where we'll add JavaScript to reload markers when the map view changes.
|
677 |
-
"""
|
678 |
-
|
679 |
-
def __init__(self):
|
680 |
-
super(BoundEventHandler, self).__init__()
|
681 |
-
self._name = "BoundEventHandler"
|
682 |
-
|
683 |
-
def render(self, **kwargs):
|
684 |
-
"""Render the JavaScript for handling bounds changes"""
|
685 |
-
figure = self.get_root()
|
686 |
-
assert isinstance(figure, Figure), "You must add this to a folium Figure (map)"
|
687 |
-
|
688 |
-
# Add necessary JavaScript libraries
|
689 |
-
figure.header.add_child(JavascriptLink("https://code.jquery.com/jquery-3.7.1.min.js"))
|
690 |
-
|
691 |
-
# Create JavaScript to handle map bounds changes and load data
|
692 |
-
script = """
|
693 |
-
<script>
|
694 |
-
// Wait for the map to be created and fully loaded
|
695 |
-
$(document).ready(function() {
|
696 |
-
// This function will run once the map is loaded
|
697 |
-
setTimeout(function() {
|
698 |
-
// Get the map instance
|
699 |
-
var map = Object.values(window).find(function(v) {
|
700 |
-
return v && v.layerManager && v.layerManager._map;
|
701 |
-
});
|
702 |
-
|
703 |
-
if (!map) {
|
704 |
-
console.error("Could not find map instance");
|
705 |
-
return;
|
706 |
-
}
|
707 |
-
|
708 |
-
var foliumMap = map.layerManager._map;
|
709 |
-
|
710 |
-
// Function to get data when bounds change
|
711 |
-
function loadDataForBounds() {
|
712 |
-
// Get current map bounds
|
713 |
-
var bounds = foliumMap.getBounds();
|
714 |
-
var minLat = bounds.getSouth();
|
715 |
-
var maxLat = bounds.getNorth();
|
716 |
-
var minLng = bounds.getWest();
|
717 |
-
var maxLng = bounds.getEast();
|
718 |
-
var zoom = foliumMap.getZoom();
|
719 |
-
|
720 |
-
// Only load data if zoom level is high enough
|
721 |
-
// This prevents too many requests when viewing the whole country
|
722 |
-
if (zoom < 6) {
|
723 |
-
console.log("Zoom level too low, not loading data");
|
724 |
-
return;
|
725 |
-
}
|
726 |
-
|
727 |
-
console.log("Map bounds changed. Zoom: " + zoom);
|
728 |
-
console.log("Bounds: " + minLat + ", " + maxLat + ", " + minLng + ", " + maxLng);
|
729 |
-
|
730 |
-
// This part would normally send a request to the server
|
731 |
-
// In this demo, we'll just show a notification
|
732 |
-
|
733 |
-
// Create a notification element
|
734 |
-
var notificationId = "bounds-notification-" + Date.now();
|
735 |
-
var notification = $('<div id="' + notificationId + '" style="position: absolute; z-index: 1000; background-color: white; padding: 10px; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.5); top: 10px; left: 50%; transform: translateX(-50%);">Loading air quality stations within view...</div>');
|
736 |
-
|
737 |
-
// Add it to the map container
|
738 |
-
$(foliumMap.getContainer()).append(notification);
|
739 |
-
|
740 |
-
// Remove it after 2 seconds
|
741 |
-
setTimeout(function() {
|
742 |
-
$("#" + notificationId).fadeOut(500, function() {
|
743 |
-
$(this).remove();
|
744 |
-
});
|
745 |
-
}, 2000);
|
746 |
-
|
747 |
-
// In a real implementation, we would now:
|
748 |
-
// 1. Send a request to the server to get data for these bounds
|
749 |
-
// 2. The server would query the API for the data
|
750 |
-
// 3. The server would return the data
|
751 |
-
// 4. We would add markers to the map
|
752 |
-
}
|
753 |
-
|
754 |
-
// Add event listener for map moveend (happens after pan/zoom)
|
755 |
-
foliumMap.on('moveend', loadDataForBounds);
|
756 |
-
|
757 |
-
console.log("Bounds event handler attached successfully");
|
758 |
-
}, 1000); // Wait 1 second for the map to fully initialize
|
759 |
-
});
|
760 |
-
</script>
|
761 |
-
"""
|
762 |
-
|
763 |
-
figure.script.add_child(folium.Element(script))
|
764 |
-
return self._template.render(**kwargs)
|
765 |
-
|
766 |
-
return BoundEventHandler
|
767 |
-
|
768 |
-
def create_air_quality_map_ui():
|
769 |
-
"""Create the Gradio interface for the Air Quality Map application"""
|
770 |
-
app = AirQualityApp()
|
771 |
-
|
772 |
-
def show_map(parameter=None):
|
773 |
-
"""Callback to generate and display the map"""
|
774 |
-
# Extract code from parameter string if provided
|
775 |
-
parameter_code = None
|
776 |
-
if parameter and ":" in parameter:
|
777 |
-
parameter_code = parameter.split(":")[0].strip()
|
778 |
-
|
779 |
-
# Generate the map
|
780 |
-
result = app.create_map(parameter_code=parameter_code)
|
781 |
-
|
782 |
-
if isinstance(result, dict):
|
783 |
-
# Combine map and legend HTML
|
784 |
-
html_content = f"""
|
785 |
-
<div>
|
786 |
-
{result["map"]}
|
787 |
-
{result["legend"]}
|
788 |
-
</div>
|
789 |
-
"""
|
790 |
-
return html_content
|
791 |
-
else:
|
792 |
-
# Return error message or whatever was returned
|
793 |
-
return result
|
794 |
-
|
795 |
-
# Create the UI
|
796 |
-
with gr.Blocks(title="Air Quality Monitoring Stations") as interface:
|
797 |
-
gr.Markdown("# NOAA Air Quality Monitoring Stations Map")
|
798 |
-
gr.Markdown("""
|
799 |
-
This application displays air quality monitoring stations in the United States.
|
800 |
-
|
801 |
-
**Features:**
|
802 |
-
- The map automatically loads air quality sites as you zoom or pan.
|
803 |
-
- Stations are color-coded based on their most recent Air Quality Index (AQI) reading.
|
804 |
-
- Click on a station marker to view detailed information and recent readings.
|
805 |
-
|
806 |
-
**Note:** To use the actual EPA AQS API, you need to register for an API key and set
|
807 |
-
`EPA_AQS_EMAIL` and `EPA_AQS_API_KEY` environment variables in your Hugging Face Space.
|
808 |
-
|
809 |
-
For demonstration without an API key, the app shows sample data for major US regions.
|
810 |
-
""")
|
811 |
-
|
812 |
-
with gr.Row():
|
813 |
-
with gr.Column(scale=1):
|
814 |
-
# Parameter dropdown (pollutant type)
|
815 |
-
parameter_dropdown = gr.Dropdown(
|
816 |
-
choices=app.mock_get_parameters(),
|
817 |
-
label="Filter by Pollutant (Optional)",
|
818 |
-
allow_custom_value=True
|
819 |
-
)
|
820 |
-
|
821 |
-
# Instructions for using the map
|
822 |
-
gr.Markdown("""
|
823 |
-
### How to use the map:
|
824 |
-
1. Zoom in or pan to an area of interest
|
825 |
-
2. Air quality monitoring stations will automatically load
|
826 |
-
3. Click on markers to view detailed information
|
827 |
-
|
828 |
-
The map will only load stations when zoomed in to avoid
|
829 |
-
displaying too many stations at once.
|
830 |
-
""")
|
831 |
-
|
832 |
-
# Button to refresh map
|
833 |
-
map_button = gr.Button("Refresh Map")
|
834 |
-
|
835 |
-
# HTML component to display the map in a larger column
|
836 |
-
with gr.Column(scale=3):
|
837 |
-
map_html = gr.HTML(label="Air Quality Monitoring Stations Map")
|
838 |
-
|
839 |
-
# Set up event handlers
|
840 |
-
map_button.click(
|
841 |
-
fn=show_map,
|
842 |
-
inputs=[parameter_dropdown],
|
843 |
-
outputs=map_html
|
844 |
-
)
|
845 |
-
|
846 |
-
# Initialize the map on load
|
847 |
-
interface.load(
|
848 |
-
fn=show_map,
|
849 |
-
inputs=[parameter_dropdown],
|
850 |
-
outputs=map_html
|
851 |
-
)
|
852 |
-
|
853 |
-
return interface
|
854 |
-
|
855 |
-
# Creating and launching the app in app.py
|
856 |
-
def create_air_quality_map_ui():
|
857 |
-
"""Create the Gradio interface for the Air Quality Map application"""
|
858 |
-
app = AirQualityApp()
|
859 |
-
|
860 |
-
def show_map(parameter=None):
|
861 |
-
"""Callback to generate and display the map"""
|
862 |
-
# Extract code from parameter string if provided
|
863 |
-
parameter_code = None
|
864 |
-
if parameter and ":" in parameter:
|
865 |
-
parameter_code = parameter.split(":")[0].strip()
|
866 |
-
|
867 |
-
# Generate the map
|
868 |
-
result = app.create_map(parameter_code=parameter_code)
|
869 |
-
|
870 |
-
if isinstance(result, dict):
|
871 |
-
# Combine map and legend HTML
|
872 |
-
html_content = f"""
|
873 |
-
<div>
|
874 |
-
{result["map"]}
|
875 |
-
{result["legend"]}
|
876 |
-
</div>
|
877 |
-
"""
|
878 |
-
return html_content
|
879 |
-
else:
|
880 |
-
# Return error message or whatever was returned
|
881 |
-
return result
|
882 |
-
|
883 |
-
# Create the UI
|
884 |
-
with gr.Blocks(title="Air Quality Monitoring Stations") as interface:
|
885 |
-
gr.Markdown("# Air Quality Monitoring Stations Map")
|
886 |
-
gr.Markdown("""
|
887 |
-
This application displays air quality monitoring stations in the United States.
|
888 |
-
|
889 |
-
**Features:**
|
890 |
-
- The map automatically loads air quality sites as you zoom or pan.
|
891 |
-
- Stations are color-coded based on their most recent Air Quality Index (AQI) reading.
|
892 |
-
- Click on a station marker to view detailed information and recent readings.
|
893 |
-
|
894 |
-
**Note:** To use the actual EPA AQS API, you need to register for an API key and set
|
895 |
-
`EPA_AQS_EMAIL` and `EPA_AQS_API_KEY` environment variables in your Hugging Face Space.
|
896 |
-
|
897 |
-
For demonstration without an API key, the app shows sample data for major US regions.
|
898 |
-
""")
|
899 |
-
|
900 |
-
with gr.Row():
|
901 |
-
with gr.Column(scale=1):
|
902 |
-
# Parameter dropdown (pollutant type)
|
903 |
-
parameter_dropdown = gr.Dropdown(
|
904 |
-
choices=app.mock_get_parameters(),
|
905 |
-
label="Filter by Pollutant (Optional)",
|
906 |
-
allow_custom_value=True
|
907 |
-
)
|
908 |
-
|
909 |
-
# Instructions for using the map
|
910 |
-
gr.Markdown("""
|
911 |
-
### How to use the map:
|
912 |
-
1. Zoom in or pan to an area of interest
|
913 |
-
2. Air quality monitoring stations will automatically load
|
914 |
-
3. Click on markers to view detailed information
|
915 |
-
|
916 |
-
The map will only load stations when zoomed in to avoid
|
917 |
-
displaying too many stations at once.
|
918 |
-
""")
|
919 |
-
|
920 |
-
# Button to refresh map
|
921 |
-
map_button = gr.Button("Refresh Map")
|
922 |
-
|
923 |
-
# HTML component to display the map in a larger column
|
924 |
-
with gr.Column(scale=3):
|
925 |
-
map_html = gr.HTML(label="Air Quality Monitoring Stations Map")
|
926 |
-
|
927 |
-
# Set up event handlers
|
928 |
-
map_button.click(
|
929 |
-
fn=show_map,
|
930 |
-
inputs=[parameter_dropdown],
|
931 |
-
outputs=map_html
|
932 |
-
)
|
933 |
-
|
934 |
-
# Initialize the map on load
|
935 |
-
interface.load(
|
936 |
-
fn=show_map,
|
937 |
-
inputs=[parameter_dropdown],
|
938 |
-
outputs=map_html
|
939 |
-
)
|
940 |
-
|
941 |
-
return interface
|
942 |
-
|
943 |
-
# For direct execution and Hugging Face Spaces deployment
|
944 |
-
if __name__ == "__main__":
|
945 |
-
app = create_air_quality_map_ui()
|
946 |
-
app.launch()
|
947 |
-
York",
|
948 |
"cbsa_name": "New York-Newark-Jersey City",
|
949 |
"date_established": "1999-07-15",
|
950 |
"last_sample_date": "2024-04-10"
|
@@ -962,4 +578,9 @@ if __name__ == "__main__":
|
|
962 |
"address": "681 Kelly Street",
|
963 |
"city_name": "Bronx",
|
964 |
"county_name": "Bronx",
|
965 |
-
"state_name": "New
|
|
|
|
|
|
|
|
|
|
|
|
561 |
"city_name": "New York",
|
562 |
"county_name": "New York",
|
563 |
"state_name": "New York",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
564 |
"cbsa_name": "New York-Newark-Jersey City",
|
565 |
"date_established": "1999-07-15",
|
566 |
"last_sample_date": "2024-04-10"
|
|
|
578 |
"address": "681 Kelly Street",
|
579 |
"city_name": "Bronx",
|
580 |
"county_name": "Bronx",
|
581 |
+
"state_name": "New York",
|
582 |
+
"cbsa_name": "New York-Newark-Jersey City",
|
583 |
+
"date_established": "1998-01-01",
|
584 |
+
"last_sample_date": "2024-04-10"
|
585 |
+
}
|
586 |
+
]
|