nakas commited on
Commit
027a5d8
·
verified ·
1 Parent(s): 53f65ae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +302 -299
app.py CHANGED
@@ -1,338 +1,341 @@
1
  import gradio as gr
2
  import requests
3
- import pandas as pd
4
  import folium
5
- from folium import plugins
6
  import json
7
- from datetime import datetime, timedelta
8
- import plotly.express as px
9
- import plotly.graph_objects as go
10
- from plotly.subplots import make_subplots
11
 
12
- class AirQualityMonitor:
 
 
13
  def __init__(self):
14
- self.base_url = "https://www.airnowapi.org/aq"
15
- # You'll need to get an API key from https://docs.airnowapi.org/
16
- self.api_key = "YOUR_API_KEY_HERE" # Replace with actual API key
17
-
18
- def get_current_observations(self, state_code=None, bbox=None):
19
- """Get current air quality observations"""
20
- if not self.api_key or self.api_key == "YOUR_API_KEY_HERE":
21
- # Return sample data for demo purposes
22
- return self._get_sample_data()
23
-
24
- url = f"{self.base_url}/observation/zipCode/current/"
25
- params = {
26
- 'format': 'application/json',
27
- 'API_KEY': self.api_key,
28
- 'distance': 100
29
  }
30
-
31
- if state_code:
32
- # Get observations for a specific state
33
- params['zipCode'] = self._get_state_zip(state_code)
34
-
35
- try:
36
- response = requests.get(url, params=params)
37
- response.raise_for_status()
38
- return response.json()
39
- except Exception as e:
40
- print(f"API Error: {e}")
41
- return self._get_sample_data()
42
-
43
- def _get_state_zip(self, state_code):
44
- """Get a representative zip code for a state"""
45
- state_zips = {
46
- 'CA': '90210', 'NY': '10001', 'TX': '73301', 'FL': '33101',
47
- 'IL': '60601', 'PA': '19101', 'OH': '43215', 'GA': '30301',
48
- 'NC': '27601', 'MI': '48201', 'NJ': '07001', 'VA': '23219',
49
- 'WA': '98101', 'AZ': '85001', 'MA': '02101', 'TN': '37201',
50
- 'IN': '46201', 'MO': '63101', 'MD': '21201', 'WI': '53201'
51
  }
52
- return state_zips.get(state_code, '90210')
53
 
54
- def _get_sample_data(self):
55
- """Return sample data for demonstration"""
56
- return [
57
- {
58
- "DateObserved": "2025-06-29",
59
- "HourObserved": 14,
60
- "LocalTimeZone": "PST",
61
- "ReportingArea": "Los Angeles-South Coast Air Basin",
62
- "StateCode": "CA",
63
- "Latitude": 34.0522,
64
- "Longitude": -118.2437,
65
- "ParameterName": "PM2.5",
66
- "AQI": 65,
67
- "Category": {"Number": 2, "Name": "Moderate"}
68
- },
69
- {
70
- "DateObserved": "2025-06-29",
71
- "HourObserved": 14,
72
- "LocalTimeZone": "EST",
73
- "ReportingArea": "New York",
74
- "StateCode": "NY",
75
- "Latitude": 40.7128,
76
- "Longitude": -74.0060,
77
- "ParameterName": "Ozone",
78
- "AQI": 45,
79
- "Category": {"Number": 1, "Name": "Good"}
80
- },
81
- {
82
- "DateObserved": "2025-06-29",
83
- "HourObserved": 14,
84
- "LocalTimeZone": "CST",
85
- "ReportingArea": "Chicago",
86
- "StateCode": "IL",
87
- "Latitude": 41.8781,
88
- "Longitude": -87.6298,
89
- "ParameterName": "PM2.5",
90
- "AQI": 85,
91
- "Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"}
92
- },
93
- {
94
- "DateObserved": "2025-06-29",
95
- "HourObserved": 14,
96
- "LocalTimeZone": "MST",
97
- "ReportingArea": "Phoenix",
98
- "StateCode": "AZ",
99
- "Latitude": 33.4484,
100
- "Longitude": -112.0740,
101
- "ParameterName": "Ozone",
102
- "AQI": 95,
103
- "Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"}
104
- },
105
- {
106
- "DateObserved": "2025-06-29",
107
- "HourObserved": 14,
108
- "LocalTimeZone": "PST",
109
- "ReportingArea": "Seattle",
110
- "StateCode": "WA",
111
- "Latitude": 47.6062,
112
- "Longitude": -122.3321,
113
- "ParameterName": "PM2.5",
114
- "AQI": 25,
115
- "Category": {"Number": 1, "Name": "Good"}
116
- },
117
- {
118
- "DateObserved": "2025-06-29",
119
- "HourObserved": 14,
120
- "LocalTimeZone": "EST",
121
- "ReportingArea": "Miami",
122
- "StateCode": "FL",
123
- "Latitude": 25.7617,
124
- "Longitude": -80.1918,
125
- "ParameterName": "Ozone",
126
- "AQI": 55,
127
- "Category": {"Number": 2, "Name": "Moderate"}
128
- }
129
- ]
130
-
131
- def get_aqi_color(aqi):
132
- """Return color based on AQI value"""
133
- if aqi <= 50:
134
- return "#00E400" # Green
135
- elif aqi <= 100:
136
- return "#FFFF00" # Yellow
137
- elif aqi <= 150:
138
- return "#FF7E00" # Orange
139
- elif aqi <= 200:
140
- return "#FF0000" # Red
141
- elif aqi <= 300:
142
- return "#8F3F97" # Purple
143
- else:
144
- return "#7E0023" # Maroon
145
-
146
- def create_air_quality_map(data):
147
- """Create an interactive map with air quality data"""
148
- if not data:
149
- # Create empty map centered on US
150
- m = folium.Map(location=[39.8283, -98.5795], zoom_start=4)
151
- folium.Marker(
152
- [39.8283, -98.5795],
153
- popup="No data available. Please add your AirNow API key.",
154
- icon=folium.Icon(color='red')
155
- ).add_to(m)
156
- return m._repr_html_()
157
 
158
- # Calculate map center
159
- lats = [float(station['Latitude']) for station in data]
160
- lons = [float(station['Longitude']) for station in data]
161
- center_lat = sum(lats) / len(lats)
162
- center_lon = sum(lons) / len(lons)
163
 
164
- # Create map
165
- m = folium.Map(location=[center_lat, center_lon], zoom_start=4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- # Add markers for each monitoring station
168
- for station in data:
169
- lat = float(station['Latitude'])
170
- lon = float(station['Longitude'])
171
- aqi = station.get('AQI', 0)
172
- parameter = station.get('ParameterName', 'Unknown')
173
- area = station.get('ReportingArea', 'Unknown Area')
174
- category = station.get('Category', {}).get('Name', 'Unknown')
 
 
 
 
 
 
 
175
 
176
- color = get_aqi_color(aqi)
 
 
 
 
177
 
178
- popup_html = f"""
179
- <div style="width: 200px;">
180
- <h4>{area}</h4>
181
- <p><strong>AQI:</strong> {aqi}</p>
182
- <p><strong>Parameter:</strong> {parameter}</p>
183
- <p><strong>Category:</strong> {category}</p>
184
- <p><strong>Date:</strong> {station.get('DateObserved', 'N/A')}</p>
185
- <p><strong>Time:</strong> {station.get('HourObserved', 'N/A')}:00 {station.get('LocalTimeZone', '')}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  </div>
187
  """
 
188
 
189
- folium.CircleMarker(
190
- location=[lat, lon],
191
- radius=8,
192
- popup=folium.Popup(popup_html, max_width=250),
193
- color='black',
194
- weight=1,
195
- fill=True,
196
- fillColor=color,
197
- fillOpacity=0.8,
198
- tooltip=f"{area}: AQI {aqi}"
199
- ).add_to(m)
200
-
201
- # Add legend
202
- legend_html = '''
203
- <div style="position: fixed;
204
- top: 10px; right: 10px; width: 160px; height: 140px;
205
- background-color: white; border:2px solid grey; z-index:9999;
206
- font-size:14px; padding: 10px">
207
- <h4>AQI Legend</h4>
208
- <p><i class="fa fa-circle" style="color:#00E400"></i> Good (0-50)</p>
209
- <p><i class="fa fa-circle" style="color:#FFFF00"></i> Moderate (51-100)</p>
210
- <p><i class="fa fa-circle" style="color:#FF7E00"></i> Unhealthy for Sensitive (101-150)</p>
211
- <p><i class="fa fa-circle" style="color:#FF0000"></i> Unhealthy (151-200)</p>
212
- <p><i class="fa fa-circle" style="color:#8F3F97"></i> Very Unhealthy (201-300)</p>
213
- </div>
214
- '''
215
- m.get_root().html.add_child(folium.Element(legend_html))
216
 
217
- return m._repr_html_()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- def create_aqi_chart(data):
220
- """Create a chart showing AQI levels by location"""
221
- if not data:
222
- return None
223
-
224
- df = pd.DataFrame(data)
225
- df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce')
226
-
227
- # Create bar chart
228
- fig = px.bar(df,
229
- x='ReportingArea',
230
- y='AQI',
231
- color='AQI',
232
- color_continuous_scale='RdYlGn_r',
233
- title='Air Quality Index by Location',
234
- labels={'AQI': 'Air Quality Index', 'ReportingArea': 'Location'})
235
-
236
- fig.update_layout(xaxis_tickangle=-45, height=500)
237
- fig.update_traces(hovertemplate='<b>%{x}</b><br>AQI: %{y}<extra></extra>')
238
-
239
- return fig
240
 
241
- def get_air_quality_summary(data):
242
- """Generate a summary of air quality data"""
243
- if not data:
244
- return "No air quality data available."
245
-
246
- df = pd.DataFrame(data)
247
- df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce')
248
 
249
- total_stations = len(df)
250
- avg_aqi = df['AQI'].mean()
251
- max_aqi = df['AQI'].max()
252
- min_aqi = df['AQI'].min()
253
 
254
- # Count by category
255
- categories = df['Category'].apply(lambda x: x.get('Name', 'Unknown') if isinstance(x, dict) else 'Unknown')
256
- category_counts = categories.value_counts()
257
-
258
- summary = f"""
259
- ## Air Quality Summary
260
-
261
- **Total Monitoring Stations:** {total_stations}
262
- **Average AQI:** {avg_aqi:.1f}
263
- **Highest AQI:** {max_aqi}
264
- **Lowest AQI:** {min_aqi}
265
-
266
- ### Air Quality Categories:
267
- """
268
-
269
- for category, count in category_counts.items():
270
- percentage = (count / total_stations) * 100
271
- summary += f"- **{category}:** {count} stations ({percentage:.1f}%)\n"
272
-
273
- return summary
274
-
275
- def update_data():
276
- """Main function to fetch and display air quality data"""
277
- monitor = AirQualityMonitor()
278
- data = monitor.get_current_observations()
279
 
280
- map_html = create_air_quality_map(data)
281
- chart = create_aqi_chart(data)
282
- summary = get_air_quality_summary(data)
283
 
284
- return map_html, chart, summary
285
 
286
  # Create Gradio interface
287
- with gr.Blocks(title="Air Quality Monitor", theme=gr.themes.Soft()) as demo:
288
- gr.Markdown("# 🌬️ Real-Time Air Quality Monitor")
289
- gr.Markdown("Monitor air quality data from NOAA/EPA AirNow stations across the United States")
290
-
291
- with gr.Row():
292
- gr.Markdown("""
293
- This application displays real-time air quality data from over 2,000 monitoring stations
294
- across the United States. The data includes Air Quality Index (AQI) values for various
295
- pollutants like PM2.5, PM10, Ozone, and more.
296
 
297
- **Note:** To access live data, you need to:
298
- 1. Get a free API key from [AirNow API](https://docs.airnowapi.org/)
299
- 2. Replace `YOUR_API_KEY_HERE` in the code with your actual API key
300
 
301
- Currently showing sample data for demonstration.
302
- """)
303
-
304
- with gr.Row():
305
- update_btn = gr.Button("🔄 Update Air Quality Data", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
306
 
307
  with gr.Row():
308
- with gr.Column(scale=2):
309
- map_output = gr.HTML(label="Air Quality Map")
 
 
 
 
 
310
  with gr.Column(scale=1):
311
- summary_output = gr.Markdown(label="Summary")
312
 
313
- with gr.Row():
314
- chart_output = gr.Plot(label="AQI Chart")
315
 
316
- # Add information section
317
- with gr.Row():
318
- gr.Markdown("""
319
- ## About Air Quality Index (AQI)
320
 
321
- The AQI is an index for reporting daily air quality. It tells you how clean or polluted your air is:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
- - **Good (0-50)**: 🟢 Air quality is satisfactory
324
- - **Moderate (51-100)**: 🟡 Acceptable for most people
325
- - **Unhealthy for Sensitive Groups (101-150)**: 🟠 May cause problems for sensitive individuals
326
- - **Unhealthy (151-200)**: 🔴 May cause health problems for everyone
327
- - **Very Unhealthy (201-300)**: 🟣 Health alert for everyone
328
- - **Hazardous (301+)**: 🟤 Health emergency
329
 
330
- **Data Source:** EPA AirNow - Real-time air quality observations from government monitoring stations
331
- """)
 
 
 
 
332
 
333
- # Load initial data
334
- demo.load(update_data, outputs=[map_output, chart_output, summary_output])
335
- update_btn.click(update_data, outputs=[map_output, chart_output, summary_output])
 
 
 
 
 
 
336
 
 
337
  if __name__ == "__main__":
338
- demo.launch()
 
1
  import gradio as gr
2
  import requests
 
3
  import folium
 
4
  import json
5
+ import time
6
+ import os
7
+ from typing import Dict, List, Optional, Tuple
8
+ import pandas as pd
9
 
10
+ class AirQualityMapper:
11
+ """Class to handle AirNow API interactions and map generation"""
12
+
13
  def __init__(self):
14
+ self.base_url = "https://www.airnowapi.org"
15
+ self.aqi_colors = {
16
+ "Good": "#00E400",
17
+ "Moderate": "#FFFF00",
18
+ "Unhealthy for Sensitive Groups": "#FF7E00",
19
+ "Unhealthy": "#FF0000",
20
+ "Very Unhealthy": "#8F3F97",
21
+ "Hazardous": "#7E0023"
 
 
 
 
 
 
 
22
  }
23
+ self.aqi_ranges = {
24
+ (0, 50): "Good",
25
+ (51, 100): "Moderate",
26
+ (101, 150): "Unhealthy for Sensitive Groups",
27
+ (151, 200): "Unhealthy",
28
+ (201, 300): "Very Unhealthy",
29
+ (301, 500): "Hazardous"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
 
31
 
32
+ def get_aqi_category(self, aqi_value: int) -> str:
33
+ """Get AQI category based on value"""
34
+ for (min_val, max_val), category in self.aqi_ranges.items():
35
+ if min_val <= aqi_value <= max_val:
36
+ return category
37
+ return "Unknown"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ def get_aqi_color(self, category: str) -> str:
40
+ """Get color for AQI category"""
41
+ return self.aqi_colors.get(category, "#808080")
 
 
42
 
43
+ def fetch_airnow_data(self, api_key: str) -> Tuple[List[Dict], str]:
44
+ """
45
+ Fetch air quality data from AirNow API
46
+ Returns: (data_list, status_message)
47
+ """
48
+ if not api_key or api_key.strip() == "":
49
+ return [], "❌ Please enter a valid AirNow API key"
50
+
51
+ try:
52
+ # Get data for major US cities and regions
53
+ # We'll use a comprehensive list of state capitals and major cities
54
+ locations = [
55
+ ("90210", "California"), ("10001", "New York"), ("60601", "Illinois"),
56
+ ("75201", "Texas"), ("33101", "Florida"), ("30301", "Georgia"),
57
+ ("98101", "Washington"), ("97201", "Oregon"), ("80201", "Colorado"),
58
+ ("85001", "Arizona"), ("89101", "Nevada"), ("84101", "Utah"),
59
+ ("59601", "Montana"), ("58501", "North Dakota"), ("57501", "South Dakota"),
60
+ ("68501", "Nebraska"), ("66601", "Kansas"), ("73101", "Oklahoma"),
61
+ ("55101", "Minnesota"), ("50301", "Iowa"), ("65101", "Missouri"),
62
+ ("72201", "Arkansas"), ("70801", "Louisiana"), ("39201", "Mississippi"),
63
+ ("35201", "Alabama"), ("37201", "Tennessee"), ("40601", "Kentucky"),
64
+ ("25301", "West Virginia"), ("23219", "Virginia"), ("27601", "North Carolina"),
65
+ ("29201", "South Carolina"), ("32301", "Florida"), ("01501", "Massachusetts"),
66
+ ("06101", "Connecticut"), ("02901", "Rhode Island"), ("03301", "New Hampshire"),
67
+ ("05601", "Vermont"), ("04330", "Maine"), ("19901", "Delaware"),
68
+ ("21201", "Maryland"), ("17101", "Pennsylvania"), ("07001", "New Jersey"),
69
+ ("12201", "New York"), ("43215", "Ohio"), ("46201", "Indiana"),
70
+ ("48601", "Michigan"), ("53201", "Wisconsin"), ("99501", "Alaska"),
71
+ ("96801", "Hawaii")
72
+ ]
73
+
74
+ all_data = []
75
+
76
+ for zipcode, state in locations:
77
+ try:
78
+ # Current observations endpoint
79
+ url = f"{self.base_url}/aq/observation/zipCode/current/"
80
+ params = {
81
+ "format": "application/json",
82
+ "zipCode": zipcode,
83
+ "distance": 50, # 50 mile radius
84
+ "API_KEY": api_key
85
+ }
86
+
87
+ response = requests.get(url, params=params, timeout=10)
88
+
89
+ if response.status_code == 200:
90
+ data = response.json()
91
+ if data: # If data is not empty
92
+ for observation in data:
93
+ observation['source_state'] = state
94
+ observation['source_zipcode'] = zipcode
95
+ all_data.extend(data)
96
+
97
+ # Add delay to respect rate limits
98
+ time.sleep(0.5)
99
+
100
+ except requests.exceptions.RequestException as e:
101
+ continue # Skip this location and continue with others
102
+
103
+ if not all_data:
104
+ return [], "⚠️ No air quality data found. Please check your API key or try again later."
105
+
106
+ # Remove duplicates based on reporting area
107
+ seen_areas = set()
108
+ unique_data = []
109
+ for item in all_data:
110
+ area_key = (item.get('ReportingArea', ''), item.get('StateCode', ''))
111
+ if area_key not in seen_areas:
112
+ seen_areas.add(area_key)
113
+ unique_data.append(item)
114
+
115
+ return unique_data, f"✅ Successfully loaded {len(unique_data)} monitoring locations"
116
+
117
+ except Exception as e:
118
+ return [], f"❌ Error fetching data: {str(e)}"
119
 
120
+ def create_map(self, data: List[Dict]) -> str:
121
+ """Create an interactive map with air quality data"""
122
+ if not data:
123
+ # Create a basic US map if no data
124
+ m = folium.Map(location=[39.8283, -98.5795], zoom_start=4)
125
+ folium.Marker(
126
+ [39.8283, -98.5795],
127
+ popup="No data available. Please check your API key.",
128
+ icon=folium.Icon(color='red', icon='info-sign')
129
+ ).add_to(m)
130
+ return m._repr_html_()
131
+
132
+ # Calculate center point of all data
133
+ lats = [item['Latitude'] for item in data if 'Latitude' in item]
134
+ lons = [item['Longitude'] for item in data if 'Longitude' in item]
135
 
136
+ if lats and lons:
137
+ center_lat = sum(lats) / len(lats)
138
+ center_lon = sum(lons) / len(lons)
139
+ else:
140
+ center_lat, center_lon = 39.8283, -98.5795 # Center of US
141
 
142
+ # Create map
143
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=4)
144
+
145
+ # Add markers for each monitoring location
146
+ for item in data:
147
+ try:
148
+ lat = item.get('Latitude')
149
+ lon = item.get('Longitude')
150
+ aqi = item.get('AQI', 0)
151
+ parameter = item.get('ParameterName', 'Unknown')
152
+ area = item.get('ReportingArea', 'Unknown Area')
153
+ state = item.get('StateCode', 'Unknown')
154
+ category = item.get('Category', {}).get('Name', self.get_aqi_category(aqi))
155
+
156
+ if lat is None or lon is None:
157
+ continue
158
+
159
+ # Get color based on AQI category
160
+ color = self.get_aqi_color(category)
161
+
162
+ # Create popup content
163
+ popup_content = f"""
164
+ <div style="width: 200px;">
165
+ <h4>{area}, {state}</h4>
166
+ <p><b>AQI:</b> {aqi} ({category})</p>
167
+ <p><b>Parameter:</b> {parameter}</p>
168
+ <p><b>Location:</b> {lat:.3f}, {lon:.3f}</p>
169
+ <p><b>Last Updated:</b> {item.get('DateObserved', 'Unknown')} {item.get('HourObserved', '')}:00</p>
170
+ </div>
171
+ """
172
+
173
+ # Determine marker color based on AQI
174
+ if aqi <= 50:
175
+ marker_color = 'green'
176
+ elif aqi <= 100:
177
+ marker_color = 'yellow'
178
+ elif aqi <= 150:
179
+ marker_color = 'orange'
180
+ elif aqi <= 200:
181
+ marker_color = 'red'
182
+ elif aqi <= 300:
183
+ marker_color = 'purple'
184
+ else:
185
+ marker_color = 'darkred'
186
+
187
+ # Add marker
188
+ folium.Marker(
189
+ [lat, lon],
190
+ popup=folium.Popup(popup_content, max_width=250),
191
+ tooltip=f"{area}: AQI {aqi}",
192
+ icon=folium.Icon(color=marker_color, icon='cloud')
193
+ ).add_to(m)
194
+
195
+ except Exception as e:
196
+ continue # Skip problematic markers
197
+
198
+ # Add legend
199
+ legend_html = """
200
+ <div style="position: fixed;
201
+ bottom: 50px; left: 50px; width: 150px; height: 180px;
202
+ background-color: white; border:2px solid grey; z-index:9999;
203
+ font-size:14px; padding: 10px">
204
+ <h4>AQI Legend</h4>
205
+ <p><i class="fa fa-circle" style="color:green"></i> Good (0-50)</p>
206
+ <p><i class="fa fa-circle" style="color:yellow"></i> Moderate (51-100)</p>
207
+ <p><i class="fa fa-circle" style="color:orange"></i> Unhealthy for Sensitive (101-150)</p>
208
+ <p><i class="fa fa-circle" style="color:red"></i> Unhealthy (151-200)</p>
209
+ <p><i class="fa fa-circle" style="color:purple"></i> Very Unhealthy (201-300)</p>
210
+ <p><i class="fa fa-circle" style="color:darkred"></i> Hazardous (301+)</p>
211
  </div>
212
  """
213
+ m.get_root().html.add_child(folium.Element(legend_html))
214
 
215
+ return m._repr_html_()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
+ def create_data_table(self, data: List[Dict]) -> pd.DataFrame:
218
+ """Create a data table from the air quality data"""
219
+ if not data:
220
+ return pd.DataFrame()
221
+
222
+ # Extract relevant columns
223
+ table_data = []
224
+ for item in data:
225
+ table_data.append({
226
+ 'Reporting Area': item.get('ReportingArea', 'Unknown'),
227
+ 'State': item.get('StateCode', 'Unknown'),
228
+ 'AQI': item.get('AQI', 0),
229
+ 'Category': item.get('Category', {}).get('Name', self.get_aqi_category(item.get('AQI', 0))),
230
+ 'Parameter': item.get('ParameterName', 'Unknown'),
231
+ 'Date': item.get('DateObserved', 'Unknown'),
232
+ 'Hour': item.get('HourObserved', 'Unknown'),
233
+ 'Latitude': item.get('Latitude', 'Unknown'),
234
+ 'Longitude': item.get('Longitude', 'Unknown')
235
+ })
236
+
237
+ df = pd.DataFrame(table_data)
238
+ return df.sort_values('AQI', ascending=False)
239
 
240
+ # Initialize the mapper
241
+ mapper = AirQualityMapper()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ def update_map(api_key: str):
244
+ """Update the map with fresh air quality data"""
245
+ if not api_key.strip():
246
+ return "Please enter your AirNow API key above.", pd.DataFrame()
 
 
 
247
 
248
+ # Fetch data
249
+ data, status = mapper.fetch_airnow_data(api_key)
 
 
250
 
251
+ # Create map
252
+ map_html = mapper.create_map(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ # Create data table
255
+ df = mapper.create_data_table(data)
 
256
 
257
+ return map_html, df
258
 
259
  # Create Gradio interface
260
+ with gr.Blocks(title="AirNow Air Quality Sensor Map", theme=gr.themes.Soft()) as demo:
261
+ gr.Markdown(
262
+ """
263
+ # 🌬️ AirNow Air Quality Sensor Map
 
 
 
 
 
264
 
265
+ This interactive map displays real-time air quality data from EPA's AirNow network of over 2,000 monitoring stations across the United States.
 
 
266
 
267
+ ## How to use:
268
+ 1. **Get an API Key**: Register for a free API key at [docs.airnowapi.org](https://docs.airnowapi.org/)
269
+ 2. **Enter your API key** in the field below
270
+ 3. **Click "Load Air Quality Data"** to fetch current readings
271
+ 4. **Explore the map**: Click on markers to see detailed information about each monitoring station
272
+
273
+ ## About the Data:
274
+ - Data is updated hourly from state, local, tribal, and federal air quality agencies
275
+ - Colors indicate Air Quality Index (AQI) levels from Good (green) to Hazardous (dark red)
276
+ - AQI values tell you how clean or polluted the air is and associated health effects
277
+
278
+ **⚠️ Note**: This data is preliminary and should not be used for regulatory decisions. For official data, visit [EPA's AirData](https://www.epa.gov/outdoor-air-quality-data).
279
+ """
280
+ )
281
 
282
  with gr.Row():
283
+ with gr.Column(scale=3):
284
+ api_key_input = gr.Textbox(
285
+ label="AirNow API Key",
286
+ placeholder="Enter your AirNow API key here...",
287
+ type="password",
288
+ info="Get your free API key at docs.airnowapi.org"
289
+ )
290
  with gr.Column(scale=1):
291
+ load_button = gr.Button("Load Air Quality Data", variant="primary", size="lg")
292
 
293
+ status_text = gr.Markdown("Enter your API key and click 'Load Air Quality Data' to begin.")
 
294
 
295
+ with gr.Tabs():
296
+ with gr.TabItem("Interactive Map"):
297
+ map_output = gr.HTML(label="Air Quality Map", height=600)
 
298
 
299
+ with gr.TabItem("Data Table"):
300
+ data_table = gr.Dataframe(
301
+ label="Air Quality Monitoring Stations",
302
+ height=500,
303
+ interactive=False
304
+ )
305
+
306
+ gr.Markdown(
307
+ """
308
+ ## AQI Health Guidelines:
309
+
310
+ - **Good (0-50)**: Air quality is satisfactory for everyone
311
+ - **Moderate (51-100)**: Air quality is acceptable for most people
312
+ - **Unhealthy for Sensitive Groups (101-150)**: Members of sensitive groups may experience health effects
313
+ - **Unhealthy (151-200)**: Everyone may begin to experience health effects
314
+ - **Very Unhealthy (201-300)**: Health warnings of emergency conditions
315
+ - **Hazardous (301+)**: Health alert - everyone may experience serious health effects
316
 
317
+ ## Data Sources:
318
+ - **AirNow API**: Real-time air quality data from EPA's monitoring network
319
+ - **Monitoring Agencies**: 120+ local, state, tribal, and federal government agencies
320
+ - **Update Frequency**: Hourly observations, daily forecasts
 
 
321
 
322
+ ## Links:
323
+ - [AirNow.gov](https://www.airnow.gov) - Official air quality information
324
+ - [AirNow API Documentation](https://docs.airnowapi.org/) - API documentation and registration
325
+ - [EPA AirData](https://www.epa.gov/outdoor-air-quality-data) - Official regulatory air quality data
326
+ """
327
+ )
328
 
329
+ # Set up event handler
330
+ load_button.click(
331
+ fn=update_map,
332
+ inputs=[api_key_input],
333
+ outputs=[map_output, data_table]
334
+ ).then(
335
+ fn=lambda: "Map updated with latest air quality data! 🌍",
336
+ outputs=[status_text]
337
+ )
338
 
339
+ # Launch the app
340
  if __name__ == "__main__":
341
+ demo.launch(share=True)