Update app.py
Browse files
app.py
CHANGED
@@ -225,12 +225,15 @@ class AccurateAirQualityMapper:
|
|
225 |
value = float(fields[7]) if fields[7].replace('.','').replace('-','').isdigit() else 0
|
226 |
parameter = fields[5]
|
227 |
|
228 |
-
#
|
229 |
-
|
230 |
-
continue
|
231 |
|
232 |
aqi = self.calculate_aqi(parameter, value)
|
233 |
|
|
|
|
|
|
|
|
|
234 |
record = {
|
235 |
'DateObserved': fields[0],
|
236 |
'HourObserved': fields[1],
|
@@ -243,9 +246,10 @@ class AccurateAirQualityMapper:
|
|
243 |
'Latitude': lat,
|
244 |
'Longitude': lon,
|
245 |
'AQI': aqi,
|
246 |
-
'Category': {'Name': self.get_aqi_category(aqi)},
|
247 |
'ReportingArea': fields[3],
|
248 |
-
'StateCode': aqs_id[:2] if len(aqs_id) >= 2 else 'US'
|
|
|
249 |
}
|
250 |
|
251 |
data.append(record)
|
@@ -288,39 +292,62 @@ class AccurateAirQualityMapper:
|
|
288 |
units = item['ReportingUnits']
|
289 |
category = item['Category']['Name']
|
290 |
|
291 |
-
# Create popup
|
292 |
-
|
293 |
-
|
294 |
-
<
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
303 |
|
304 |
-
#
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
315 |
else:
|
316 |
-
|
|
|
|
|
317 |
|
318 |
# Add marker
|
319 |
folium.Marker(
|
320 |
[lat, lon],
|
321 |
popup=folium.Popup(popup_content, max_width=300),
|
322 |
-
tooltip=
|
323 |
-
icon=folium.Icon(color=marker_color, icon=
|
324 |
).add_to(m)
|
325 |
|
326 |
except Exception as e:
|
@@ -329,16 +356,19 @@ class AccurateAirQualityMapper:
|
|
329 |
# Add legend
|
330 |
legend_html = """
|
331 |
<div style="position: fixed;
|
332 |
-
bottom: 50px; left: 50px; width:
|
333 |
background-color: white; border:2px solid grey; z-index:9999;
|
334 |
-
font-size:
|
335 |
-
<h4>
|
|
|
336 |
<p><i class="fa fa-circle" style="color:green"></i> Good (0-50)</p>
|
337 |
<p><i class="fa fa-circle" style="color:orange"></i> Moderate (51-100)</p>
|
338 |
<p><i class="fa fa-circle" style="color:orange"></i> Unhealthy for Sensitive (101-150)</p>
|
339 |
<p><i class="fa fa-circle" style="color:red"></i> Unhealthy (151-200)</p>
|
340 |
<p><i class="fa fa-circle" style="color:purple"></i> Very Unhealthy (201-300)</p>
|
341 |
<p><i class="fa fa-circle" style="color:darkred"></i> Hazardous (301+)</p>
|
|
|
|
|
342 |
</div>
|
343 |
"""
|
344 |
m.get_root().html.add_child(folium.Element(legend_html))
|
@@ -352,13 +382,15 @@ class AccurateAirQualityMapper:
|
|
352 |
|
353 |
table_data = []
|
354 |
for item in data:
|
|
|
355 |
table_data.append({
|
356 |
'Site Name': item['SiteName'],
|
357 |
'State': item['StateCode'],
|
358 |
'Parameter': item['ParameterName'],
|
|
|
359 |
'Value': item['Value'],
|
360 |
'Units': item['ReportingUnits'],
|
361 |
-
'AQI': item['AQI'],
|
362 |
'Category': item['Category']['Name'],
|
363 |
'Latitude': round(item['Latitude'], 4),
|
364 |
'Longitude': round(item['Longitude'], 4),
|
@@ -375,11 +407,30 @@ mapper = AccurateAirQualityMapper()
|
|
375 |
|
376 |
def update_map():
|
377 |
"""Update map with accurate coordinates"""
|
378 |
-
print("π Starting
|
379 |
|
380 |
# Fetch data
|
381 |
data, status = mapper.fetch_airnow_bulk_data()
|
382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
383 |
# Create map
|
384 |
map_html = mapper.create_map(data)
|
385 |
|
@@ -393,39 +444,40 @@ with gr.Blocks(title="Accurate AirNow Sensor Map", theme=gr.themes.Soft()) as de
|
|
393 |
|
394 |
gr.Markdown(
|
395 |
"""
|
396 |
-
# π―
|
397 |
|
398 |
-
**β
PRECISE COORDINATES** -
|
399 |
|
400 |
-
This map displays
|
401 |
-
1. **
|
402 |
-
2. **
|
403 |
-
3. **
|
|
|
404 |
|
405 |
## Key Features:
|
406 |
-
- π― **
|
407 |
-
-
|
408 |
-
-
|
409 |
-
-
|
410 |
-
-
|
411 |
|
412 |
-
**β οΈ Data Note**:
|
413 |
For regulatory purposes, use EPA's official AQS data.
|
414 |
"""
|
415 |
)
|
416 |
|
417 |
with gr.Row():
|
418 |
-
load_button = gr.Button("π― Load
|
419 |
|
420 |
-
status_text = gr.Markdown("Click the button above to load
|
421 |
|
422 |
with gr.Tabs():
|
423 |
-
with gr.TabItem("πΊοΈ
|
424 |
-
map_output = gr.HTML(label="
|
425 |
|
426 |
-
with gr.TabItem("π Station Data"):
|
427 |
data_table = gr.Dataframe(
|
428 |
-
label="Air Quality
|
429 |
interactive=False
|
430 |
)
|
431 |
|
@@ -433,13 +485,17 @@ with gr.Blocks(title="Accurate AirNow Sensor Map", theme=gr.themes.Soft()) as de
|
|
433 |
"""
|
434 |
## Data Sources:
|
435 |
|
436 |
-
**Coordinates**: EPA Air Quality System (AQS) - Official monitor locations
|
437 |
-
**
|
438 |
-
**Coverage**:
|
|
|
|
|
|
|
|
|
439 |
|
440 |
## Files Used:
|
441 |
-
- `aqs_monitors.zip` - EPA monitor coordinates
|
442 |
-
- `HourlyData_YYYYMMDDHH.dat` - AirNow real-time observations
|
443 |
|
444 |
## Links:
|
445 |
- [EPA AQS Data](https://aqs.epa.gov/aqsweb/airdata/download_files.html)
|
|
|
225 |
value = float(fields[7]) if fields[7].replace('.','').replace('-','').isdigit() else 0
|
226 |
parameter = fields[5]
|
227 |
|
228 |
+
# Include ALL parameters (air quality + meteorological)
|
229 |
+
# Don't filter - the original successful run included everything
|
|
|
230 |
|
231 |
aqi = self.calculate_aqi(parameter, value)
|
232 |
|
233 |
+
# Determine if it's an air quality or meteorological parameter
|
234 |
+
air_quality_params = ['OZONE', 'PM2.5', 'PM10', 'NO2', 'SO2', 'CO']
|
235 |
+
is_air_quality = parameter in air_quality_params
|
236 |
+
|
237 |
record = {
|
238 |
'DateObserved': fields[0],
|
239 |
'HourObserved': fields[1],
|
|
|
246 |
'Latitude': lat,
|
247 |
'Longitude': lon,
|
248 |
'AQI': aqi,
|
249 |
+
'Category': {'Name': self.get_aqi_category(aqi) if is_air_quality else 'Meteorological'},
|
250 |
'ReportingArea': fields[3],
|
251 |
+
'StateCode': aqs_id[:2] if len(aqs_id) >= 2 else 'US',
|
252 |
+
'IsAirQuality': is_air_quality
|
253 |
}
|
254 |
|
255 |
data.append(record)
|
|
|
292 |
units = item['ReportingUnits']
|
293 |
category = item['Category']['Name']
|
294 |
|
295 |
+
# Create popup content
|
296 |
+
if is_air_quality:
|
297 |
+
popup_content = f"""
|
298 |
+
<div style="width: 250px;">
|
299 |
+
<h4>{site_name} <span style="color: red;">π¬οΈ Air Quality</span></h4>
|
300 |
+
<p><b>Parameter:</b> {parameter}</p>
|
301 |
+
<p><b>Value:</b> {value} {units}</p>
|
302 |
+
<p><b>AQI:</b> {aqi} ({category})</p>
|
303 |
+
<p><b>Coordinates:</b> {lat:.4f}, {lon:.4f}</p>
|
304 |
+
<p><b>Time:</b> {item['DateObserved']} {item['HourObserved']}:00 GMT</p>
|
305 |
+
<p><b>Station ID:</b> {item['AQSID']}</p>
|
306 |
+
</div>
|
307 |
+
"""
|
308 |
+
tooltip_text = f"{site_name}: {parameter} = {value} {units} (AQI: {aqi})"
|
309 |
+
else:
|
310 |
+
popup_content = f"""
|
311 |
+
<div style="width: 250px;">
|
312 |
+
<h4>{site_name} <span style="color: blue;">π‘οΈ Meteorological</span></h4>
|
313 |
+
<p><b>Parameter:</b> {parameter}</p>
|
314 |
+
<p><b>Value:</b> {value} {units}</p>
|
315 |
+
<p><b>Coordinates:</b> {lat:.4f}, {lon:.4f}</p>
|
316 |
+
<p><b>Time:</b> {item['DateObserved']} {item['HourObserved']}:00 GMT</p>
|
317 |
+
<p><b>Station ID:</b> {item['AQSID']}</p>
|
318 |
+
</div>
|
319 |
+
"""
|
320 |
+
tooltip_text = f"{site_name}: {parameter} = {value} {units}"
|
321 |
|
322 |
+
# Determine marker appearance based on parameter type
|
323 |
+
is_air_quality = item.get('IsAirQuality', False)
|
324 |
+
|
325 |
+
if is_air_quality:
|
326 |
+
# Color based on AQI for air quality parameters
|
327 |
+
if aqi <= 50:
|
328 |
+
marker_color = 'green'
|
329 |
+
elif aqi <= 100:
|
330 |
+
marker_color = 'orange'
|
331 |
+
elif aqi <= 150:
|
332 |
+
marker_color = 'orange'
|
333 |
+
elif aqi <= 200:
|
334 |
+
marker_color = 'red'
|
335 |
+
elif aqi <= 300:
|
336 |
+
marker_color = 'purple'
|
337 |
+
else:
|
338 |
+
marker_color = 'darkred'
|
339 |
+
icon_type = 'cloud'
|
340 |
else:
|
341 |
+
# Meteorological parameters use blue/gray
|
342 |
+
marker_color = 'blue'
|
343 |
+
icon_type = 'info-sign'
|
344 |
|
345 |
# Add marker
|
346 |
folium.Marker(
|
347 |
[lat, lon],
|
348 |
popup=folium.Popup(popup_content, max_width=300),
|
349 |
+
tooltip=tooltip_text,
|
350 |
+
icon=folium.Icon(color=marker_color, icon=icon_type)
|
351 |
).add_to(m)
|
352 |
|
353 |
except Exception as e:
|
|
|
356 |
# Add legend
|
357 |
legend_html = """
|
358 |
<div style="position: fixed;
|
359 |
+
bottom: 50px; left: 50px; width: 200px; height: 260px;
|
360 |
background-color: white; border:2px solid grey; z-index:9999;
|
361 |
+
font-size:12px; padding: 10px">
|
362 |
+
<h4>Station Legend</h4>
|
363 |
+
<p><b>π¬οΈ Air Quality (AQI):</b></p>
|
364 |
<p><i class="fa fa-circle" style="color:green"></i> Good (0-50)</p>
|
365 |
<p><i class="fa fa-circle" style="color:orange"></i> Moderate (51-100)</p>
|
366 |
<p><i class="fa fa-circle" style="color:orange"></i> Unhealthy for Sensitive (101-150)</p>
|
367 |
<p><i class="fa fa-circle" style="color:red"></i> Unhealthy (151-200)</p>
|
368 |
<p><i class="fa fa-circle" style="color:purple"></i> Very Unhealthy (201-300)</p>
|
369 |
<p><i class="fa fa-circle" style="color:darkred"></i> Hazardous (301+)</p>
|
370 |
+
<p><b>π‘οΈ Meteorological:</b></p>
|
371 |
+
<p><i class="fa fa-circle" style="color:blue"></i> Weather Data</p>
|
372 |
</div>
|
373 |
"""
|
374 |
m.get_root().html.add_child(folium.Element(legend_html))
|
|
|
382 |
|
383 |
table_data = []
|
384 |
for item in data:
|
385 |
+
is_air_quality = item.get('IsAirQuality', False)
|
386 |
table_data.append({
|
387 |
'Site Name': item['SiteName'],
|
388 |
'State': item['StateCode'],
|
389 |
'Parameter': item['ParameterName'],
|
390 |
+
'Type': 'π¬οΈ Air Quality' if is_air_quality else 'π‘οΈ Meteorological',
|
391 |
'Value': item['Value'],
|
392 |
'Units': item['ReportingUnits'],
|
393 |
+
'AQI': item['AQI'] if is_air_quality else 'N/A',
|
394 |
'Category': item['Category']['Name'],
|
395 |
'Latitude': round(item['Latitude'], 4),
|
396 |
'Longitude': round(item['Longitude'], 4),
|
|
|
407 |
|
408 |
def update_map():
|
409 |
"""Update map with accurate coordinates"""
|
410 |
+
print("π Starting comprehensive air quality and meteorological mapping...")
|
411 |
|
412 |
# Fetch data
|
413 |
data, status = mapper.fetch_airnow_bulk_data()
|
414 |
|
415 |
+
if data:
|
416 |
+
# Show parameter breakdown like the original
|
417 |
+
df_temp = pd.DataFrame(data)
|
418 |
+
param_counts = df_temp['ParameterName'].value_counts()
|
419 |
+
|
420 |
+
print(f"\nπ Data Summary:")
|
421 |
+
print(f"Total stations: {len(df_temp)}")
|
422 |
+
print(f"Parameters monitored: {df_temp['ParameterName'].nunique()}")
|
423 |
+
print(f"Unique sites: {df_temp['SiteName'].nunique()}")
|
424 |
+
|
425 |
+
print(f"\nParameter breakdown:")
|
426 |
+
for param, count in param_counts.head(10).items():
|
427 |
+
print(f"{param}: {count}")
|
428 |
+
|
429 |
+
# Update status to include breakdown
|
430 |
+
air_quality_count = len([d for d in data if d.get('IsAirQuality', False)])
|
431 |
+
met_count = len(data) - air_quality_count
|
432 |
+
status = f"β
SUCCESS: {len(data)} total stations ({air_quality_count} air quality + {met_count} meteorological) from {len(set(d['SiteName'] for d in data))} unique sites"
|
433 |
+
|
434 |
# Create map
|
435 |
map_html = mapper.create_map(data)
|
436 |
|
|
|
444 |
|
445 |
gr.Markdown(
|
446 |
"""
|
447 |
+
# π― Complete AirNow Monitoring Network Map
|
448 |
|
449 |
+
**β
PRECISE COORDINATES + ALL STATIONS** - Every sensor with exact locations!
|
450 |
|
451 |
+
This map displays the **complete AirNow monitoring network** with accurate coordinates:
|
452 |
+
1. **All Parameters**: Air quality (OZONE, PM2.5, PM10, NO2, SO2, CO) + Meteorological (TEMP, WIND, HUMIDITY, etc.)
|
453 |
+
2. **EPA Coordinates**: Precise lat/lon for every monitoring station
|
454 |
+
3. **Real-time Data**: Current hourly readings from 2,000+ stations
|
455 |
+
4. **Visual Distinction**: π¬οΈ Air quality (colored by AQI) vs π‘οΈ Meteorological (blue)
|
456 |
|
457 |
## Key Features:
|
458 |
+
- π― **All 7,000+ Sensors**: Complete monitoring network coverage
|
459 |
+
- π **Exact Locations**: EPA's official coordinate database
|
460 |
+
- π¬οΈ **Air Quality**: Color-coded by AQI health categories
|
461 |
+
- π‘οΈ **Weather Data**: Temperature, wind, humidity, pressure
|
462 |
+
- β‘ **Real-time**: Latest hourly observations
|
463 |
|
464 |
+
**β οΈ Data Note**: Real-time preliminary data for public information.
|
465 |
For regulatory purposes, use EPA's official AQS data.
|
466 |
"""
|
467 |
)
|
468 |
|
469 |
with gr.Row():
|
470 |
+
load_button = gr.Button("π― Load Complete Monitoring Network", variant="primary", size="lg")
|
471 |
|
472 |
+
status_text = gr.Markdown("Click the button above to load ALL monitoring stations with precise coordinates.")
|
473 |
|
474 |
with gr.Tabs():
|
475 |
+
with gr.TabItem("πΊοΈ Complete Network Map"):
|
476 |
+
map_output = gr.HTML(label="Complete AirNow Monitoring Network with Precise Coordinates")
|
477 |
|
478 |
+
with gr.TabItem("π All Station Data"):
|
479 |
data_table = gr.Dataframe(
|
480 |
+
label="All Monitoring Stations (Air Quality + Meteorological)",
|
481 |
interactive=False
|
482 |
)
|
483 |
|
|
|
485 |
"""
|
486 |
## Data Sources:
|
487 |
|
488 |
+
**Coordinates**: EPA Air Quality System (AQS) - Official monitor locations (364,377+ records)
|
489 |
+
**Monitoring Data**: AirNow hourly bulk files - Real-time observations from all sensors
|
490 |
+
**Coverage**: 7,000+ monitoring sensors across US, Canada, and parts of Mexico
|
491 |
+
|
492 |
+
## Parameters Included:
|
493 |
+
**π¬οΈ Air Quality**: OZONE, PM2.5, PM10, NO2, SO2, CO (color-coded by AQI)
|
494 |
+
**π‘οΈ Meteorological**: TEMP, WIND, HUMIDITY, PRESSURE, SOLAR, PRECIP (blue markers)
|
495 |
|
496 |
## Files Used:
|
497 |
+
- `aqs_monitors.zip` - EPA monitor coordinates
|
498 |
+
- `HourlyData_YYYYMMDDHH.dat` - AirNow real-time observations (ALL parameters)
|
499 |
|
500 |
## Links:
|
501 |
- [EPA AQS Data](https://aqs.epa.gov/aqsweb/airdata/download_files.html)
|