nakas's picture
Update app.py
96ee524 verified
import gradio as gr
import requests
import folium
import os
import json
from datetime import datetime, timedelta
import pandas as pd
# Get API key from environment
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
def create_map(lat=40.7128, lon=-74.0060):
"""Create a Folium map centered at given coordinates"""
m = folium.Map(
location=[lat, lon],
zoom_start=10,
tiles="OpenStreetMap"
)
# Add a marker for the selected location
folium.Marker(
[lat, lon],
popup=f"Selected Location: {lat:.4f}, {lon:.4f}",
tooltip="Selected location for pollen forecast",
icon=folium.Icon(color='red', icon='info-sign')
).add_to(m)
return m._repr_html_()
def geocode_address(address):
"""Convert address to coordinates using a free geocoding service"""
try:
# Using Nominatim (OpenStreetMap) for free geocoding
url = "https://nominatim.openstreetmap.org/search"
params = {
'q': address,
'format': 'json',
'limit': 1
}
headers = {'User-Agent': 'PollenForecastApp/1.0'}
response = requests.get(url, params=params, headers=headers)
response.raise_for_status()
data = response.json()
if data:
lat = float(data[0]['lat'])
lon = float(data[0]['lon'])
display_name = data[0]['display_name']
return lat, lon, display_name
else:
return None, None, "Address not found"
except Exception as e:
return None, None, f"Geocoding error: {str(e)}"
def get_pollen_category_from_index(index_value):
"""Convert index value to category when API doesn't provide it"""
if not isinstance(index_value, (int, float)):
return "UNKNOWN"
if index_value <= 2.4:
return "VERY_LOW"
elif index_value <= 4.8:
return "LOW"
elif index_value <= 7.2:
return "MEDIUM"
elif index_value <= 9.6:
return "HIGH"
else:
return "VERY_HIGH"
def get_pollen_data(lat, lon, days=5):
"""Fetch pollen data from Google Pollen API"""
if not GOOGLE_API_KEY:
return "Error: Google API key not found. Please set GOOGLE_API_KEY as a secret."
# Google Pollen API endpoint
url = "https://pollen.googleapis.com/v1/forecast:lookup"
# Calculate date range
start_date = datetime.now()
end_date = start_date + timedelta(days=days-1)
params = {
"key": GOOGLE_API_KEY,
"location.longitude": lon,
"location.latitude": lat,
"days": days,
"plantsDescription": True
}
try:
response = requests.get(url, params=params)
response.raise_for_status()
data = response.json()
# Debug: Print the structure to understand the API response
print(f"API Response keys: {data.keys()}")
if "dailyInfo" in data and len(data["dailyInfo"]) > 0:
print(f"First day info keys: {data['dailyInfo'][0].keys()}")
print(f"Date structure: {data['dailyInfo'][0].get('date', 'No date field')}")
# Debug pollen info structure
if "pollenTypeInfo" in data["dailyInfo"][0]:
for pollen in data["dailyInfo"][0]["pollenTypeInfo"]:
print(f"Pollen type: {pollen.get('code', 'Unknown')}")
if "indexInfo" in pollen:
index_info = pollen["indexInfo"]
print(f" Index info: {index_info}")
print(f" Category: '{index_info.get('category', 'Missing')}'")
print(f" Value: {index_info.get('value', 'Missing')}")
return data
except requests.exceptions.RequestException as e:
error_msg = f"Error fetching pollen data: {str(e)}"
if hasattr(e, 'response') and e.response is not None:
error_msg += f" (Status: {e.response.status_code})"
return error_msg
except json.JSONDecodeError as e:
return f"Error parsing response: {str(e)}"
except Exception as e:
return f"Unexpected error: {str(e)}"
def format_pollen_data(data):
"""Format pollen data for display in user-friendly terms"""
if isinstance(data, str): # Error message
return data, None
if "dailyInfo" not in data:
return "No pollen data available for this location.", None
# Create formatted output
output = []
output.append("# 🌸 Pollen Forecast Report\n")
# Location info if available
if "regionInfo" in data:
region = data["regionInfo"]
output.append(f"**📍 Location:** {region.get('displayName', 'Unknown')}\n")
# Add explanation of what pollen index means with precise measurements
output.append("## 📊 Understanding Your Pollen Forecast\n")
output.append("**The Pollen Index** measures the actual concentration of pollen grains in the air:")
output.append(f"- **Index 0-2 (0-50 grains/m³):** Very Low - Minimal pollen particles in the air")
output.append(f"- **Index 3-5 (51-150 grains/m³):** Low - Light pollen concentration")
output.append(f"- **Index 6-7 (151-500 grains/m³):** Medium - Moderate pollen density")
output.append(f"- **Index 8-9 (501-1000 grains/m³):** High - Heavy pollen concentration")
output.append(f"- **Index 10+ (1000+ grains/m³):** Very High - Extreme pollen density")
output.append("")
output.append("**🔬 What This Means Scientifically:**")
output.append("Each number represents grains of pollen per cubic meter of air over 24 hours.")
output.append("To put this in perspective:")
output.append("- **50 grains/m³** = About 50 microscopic pollen particles in a space the size of a large refrigerator")
output.append("- **500 grains/m³** = 500 pollen particles in that same space - enough to trigger most allergies")
output.append("- **1000+ grains/m³** = Over 1,000 particles - like breathing through a cloud of pollen")
output.append("")
output.append("**⚗️ Technical Details:**")
output.append("Measured using specialized air samplers that collect particles on microscope slides.")
output.append("Each pollen grain is 15-200 micrometers (invisible to naked eye, 400x magnification needed).\n")
# Daily pollen data
daily_data = []
for day_info in data["dailyInfo"]:
# Handle different date formats from the API
date_info = day_info.get("date", {})
if isinstance(date_info, dict):
# If date is a dict, try to extract the date string
if "year" in date_info and "month" in date_info and "day" in date_info:
year = date_info["year"]
month = date_info["month"]
day = date_info["day"]
date_obj = datetime(year, month, day)
else:
# Fallback to today's date if we can't parse
date_obj = datetime.now()
elif isinstance(date_info, str):
# If it's already a string, parse it
try:
date_obj = datetime.strptime(date_info, "%Y-%m-%d")
except ValueError:
date_obj = datetime.now()
else:
# Fallback
date_obj = datetime.now()
formatted_date = date_obj.strftime("%B %d, %Y")
day_of_week = date_obj.strftime("%A")
output.append(f"## 📅 {day_of_week}, {formatted_date}\n")
if "pollenTypeInfo" in day_info:
pollen_types = day_info["pollenTypeInfo"]
# Create a row for the dataframe
row_data = {"Date": f"{day_of_week}, {formatted_date}"}
# Calculate overall day severity
max_index = 0
total_types = 0
for pollen in pollen_types:
pollen_type = pollen["code"].replace("_", " ").title()
index_info = pollen.get("indexInfo", {})
index_value = index_info.get("value", 0)
api_category = index_info.get("category", "")
# Debug: Print what we're getting from the API
print(f"Processing {pollen_type}: index={index_value}, api_category='{api_category}'")
# Use fallback if API category is missing or unknown
if api_category and api_category in ["VERY_LOW", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"]:
category = api_category
else:
# Use our fallback function
category = get_pollen_category_from_index(index_value)
print(f" Using fallback category: {category}")
if isinstance(index_value, (int, float)) and index_value > max_index:
max_index = index_value
total_types += 1
# Convert technical terms to user-friendly descriptions
severity_map = {
"VERY_LOW": {"emoji": "🟢", "description": "Excellent", "advice": "Great day to be outdoors!"},
"LOW": {"emoji": "🟡", "description": "Good", "advice": "Most people will be fine outdoors"},
"MEDIUM": {"emoji": "🟠", "description": "Moderate", "advice": "Consider taking allergy medication"},
"HIGH": {"emoji": "🔴", "description": "Poor", "advice": "Limit outdoor activities if allergic"},
"VERY_HIGH": {"emoji": "🟣", "description": "Very Poor", "advice": "Stay indoors if possible"},
"UNKNOWN": {"emoji": "⚪", "description": "Data Unavailable", "advice": "Monitor symptoms and check again later"}
}
severity = severity_map.get(category, severity_map["UNKNOWN"])
# More specific pollen type descriptions
pollen_descriptions = {
"Tree": "🌳 Tree pollen (oak, birch, cedar, etc.)",
"Grass": "🌱 Grass pollen (timothy, bermuda, etc.)",
"Weed": "🌿 Weed pollen (ragweed, sagebrush, etc.)"
}
pollen_desc = pollen_descriptions.get(pollen_type, f"🌸 {pollen_type}")
# Convert index to approximate grains per cubic meter
grains_per_m3 = "Unknown"
if isinstance(index_value, (int, float)):
if index_value <= 2:
grains_per_m3 = f"~{int(index_value * 25)} grains/m³"
elif index_value <= 5:
grains_per_m3 = f"~{int(50 + (index_value-2) * 33)} grains/m³"
elif index_value <= 7:
grains_per_m3 = f"~{int(150 + (index_value-5) * 175)} grains/m³"
elif index_value <= 9:
grains_per_m3 = f"~{int(500 + (index_value-7) * 250)} grains/m³"
else:
grains_per_m3 = f"~{int(1000 + (index_value-10) * 500)}+ grains/m³"
output.append(f"### {pollen_desc}")
output.append(f"**Level:** {severity['emoji']} {severity['description']} (Index: {index_value}/10)")
output.append(f"**Scientific Measurement:** {grains_per_m3}")
output.append(f"**What this means:** {severity['advice']}")
row_data[pollen_type] = f"{severity['description']} ({index_value}) - {grains_per_m3}"
# Add plant descriptions if available
if "plantDescription" in pollen:
plants = pollen["plantDescription"]
if "plants" in plants:
plant_list = [plant["displayName"] for plant in plants["plants"][:3]] # Show top 3
output.append(f"**Main sources:** {', '.join(plant_list)}")
output.append("")
# Add overall day assessment
if max_index > 0:
if max_index <= 2:
day_rating = "🟢 **Excellent day** for outdoor activities!"
elif max_index <= 5:
day_rating = "🟡 **Good day** - most people will be comfortable outside"
elif max_index <= 7:
day_rating = "🟠 **Moderate day** - allergy sufferers should prepare"
elif max_index <= 9:
day_rating = "🔴 **Challenging day** - limit outdoor exposure if allergic"
else:
day_rating = "🟣 **Very difficult day** - stay indoors if you have allergies"
output.append(f"**Overall Assessment:** {day_rating}\n")
daily_data.append(row_data)
output.append("---\n")
# Create DataFrame for tabular view
df = None
if daily_data:
df = pd.DataFrame(daily_data)
# Add comprehensive advice section
output.append("\n## 💡 Practical Tips for Pollen Season")
output.append("### 🔬 **Understanding the Numbers:**")
output.append("- **Under 100 grains/m³:** Like having a few specks of dust in a large room")
output.append("- **100-500 grains/m³:** Similar to light sawdust in the air - noticeable to sensitive people")
output.append("- **500-1000 grains/m³:** Like fine powder in the air - most people will feel it")
output.append("- **Over 1000 grains/m³:** Heavy particulate load - breathing through a pollen cloud")
output.append("\n### 🏠 **Indoor Protection:**")
output.append("- Keep windows and doors closed during high pollen days (500+ grains/m³)")
output.append("- Use air conditioning with clean HEPA filters")
output.append("- Consider an air purifier for your bedroom")
output.append("\n### 🚶 **When Going Outside:**")
output.append("- Check this forecast before planning outdoor activities")
output.append("- Wear wraparound sunglasses to protect your eyes")
output.append("- Consider wearing a mask during very high pollen days (1000+ grains/m³)")
output.append("- Plan outdoor activities for late evening when pollen counts are lower")
output.append("\n### 🚿 **After Being Outdoors:**")
output.append("- Shower and change clothes to remove pollen")
output.append("- Wash your hair before bed to avoid pollen on your pillow")
output.append("- Keep pets indoors or wipe them down after walks")
output.append("\n### 💊 **Medication Timing:**")
output.append("- Start allergy medications BEFORE symptoms begin")
output.append("- Take antihistamines in the evening for next-day protection")
output.append("- Consult your doctor about prescription options for severe allergies")
return "\n".join(output), df
def update_location(lat, lon):
"""Update the map and fetch pollen data for new location"""
if lat is None or lon is None:
return create_map(), "Please select a location on the map.", None
# Create new map
new_map = create_map(lat, lon)
# Get pollen data
pollen_data = get_pollen_data(lat, lon)
formatted_data, df = format_pollen_data(pollen_data)
return new_map, formatted_data, df
# Create the Gradio interface
with gr.Blocks(title="🌸 Pollen Forecast Map", theme=gr.themes.Soft()) as app:
gr.HTML("""
<div style="text-align: center; padding: 20px;">
<h1>🌸 Pollen Forecast Map</h1>
<p>Get detailed, scientifically-accurate pollen forecasts with actual particle concentrations!</p>
<div style="background-color: #e8f5e8; padding: 15px; border-radius: 10px; margin: 15px 0;">
<strong>🎯 How to Use:</strong>
<br>• <strong>Search by Address:</strong> Type any city, address, or landmark in the search box
<br>• <strong>Use Quick Cities:</strong> Click preset buttons for major cities worldwide
<br>• <strong>Enter Coordinates:</strong> Input precise latitude/longitude values
<br>• <strong>Map Note:</strong> The map displays your location but doesn't support clicking to select new points
</div>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
gr.HTML("<h3>📍 Location Selection</h3>")
# Address search
gr.HTML("<h4>🔍 Search by Address</h4>")
address_input = gr.Textbox(
label="Enter Address or City",
placeholder="e.g., Central Park, New York or Paris, France",
info="Enter any address, city, or landmark"
)
search_btn = gr.Button("🔍 Search Address", variant="primary")
gr.HTML("<h4>📐 Or Use Coordinates</h4>")
# Coordinate inputs
lat_input = gr.Number(
label="Latitude",
value=40.7128,
precision=6,
info="Enter latitude (-90 to 90)"
)
lon_input = gr.Number(
label="Longitude",
value=-74.0060,
precision=6,
info="Enter longitude (-180 to 180)"
)
update_btn = gr.Button("🔄 Update Location", variant="secondary")
# Preset locations
gr.HTML("<h4>🏙️ Popular Cities</h4>")
with gr.Row():
nyc_btn = gr.Button("🗽 New York", size="sm")
la_btn = gr.Button("🌴 Los Angeles", size="sm")
with gr.Row():
chicago_btn = gr.Button("🌬️ Chicago", size="sm")
miami_btn = gr.Button("🏖️ Miami", size="sm")
with gr.Row():
london_btn = gr.Button("🇬🇧 London", size="sm")
tokyo_btn = gr.Button("🇯🇵 Tokyo", size="sm")
with gr.Column(scale=2):
# Map display
gr.HTML("<h3>🗺️ Location Map</h3>")
gr.HTML("""
<div style="background-color: #f0f8ff; padding: 10px; border-radius: 5px; margin-bottom: 10px;">
<strong>📍 Note:</strong> The map shows your selected location but doesn't support clicking to select new points.
Use the address search or coordinate inputs on the left to change locations.
</div>
""")
map_html = gr.HTML(
value=create_map(),
label="Current Location"
)
# Location info display
location_info = gr.Textbox(
label="📍 Selected Location",
value="New York City, NY, USA",
interactive=False
)
with gr.Row():
with gr.Column():
# Pollen data output
pollen_output = gr.Markdown(
value="Select a location to view pollen forecast.",
label="Pollen Forecast"
)
# Data table
pollen_table = gr.Dataframe(
label="Pollen Data Summary",
visible=False
)
# Event handlers
def search_address(address):
"""Search for address and return coordinates"""
if not address.strip():
return 40.7128, -74.0060, "Please enter an address to search"
lat, lon, display_name = geocode_address(address)
if lat is not None and lon is not None:
return lat, lon, display_name
else:
return 40.7128, -74.0060, display_name # display_name contains error message
def set_nyc():
return 40.7128, -74.0060, "New York City, NY, USA"
def set_la():
return 34.0522, -118.2437, "Los Angeles, CA, USA"
def set_chicago():
return 41.8781, -87.6298, "Chicago, IL, USA"
def set_miami():
return 25.7617, -80.1918, "Miami, FL, USA"
def set_london():
return 51.5074, -0.1278, "London, England, UK"
def set_tokyo():
return 35.6762, 139.6503, "Tokyo, Japan"
# Button events
search_btn.click(
fn=search_address,
inputs=[address_input],
outputs=[lat_input, lon_input, location_info]
).then(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
update_btn.click(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
nyc_btn.click(
fn=set_nyc,
outputs=[lat_input, lon_input, location_info]
).then(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
la_btn.click(
fn=set_la,
outputs=[lat_input, lon_input, location_info]
).then(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
chicago_btn.click(
fn=set_chicago,
outputs=[lat_input, lon_input, location_info]
).then(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
miami_btn.click(
fn=set_miami,
outputs=[lat_input, lon_input, location_info]
).then(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
london_btn.click(
fn=set_london,
outputs=[lat_input, lon_input, location_info]
).then(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
tokyo_btn.click(
fn=set_tokyo,
outputs=[lat_input, lon_input, location_info]
).then(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
# Auto-update when coordinates change
lat_input.change(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
lon_input.change(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
# Load initial data
app.load(
fn=lambda: (40.7128, -74.0060, "New York City, NY, USA"),
outputs=[lat_input, lon_input, location_info]
).then(
fn=update_location,
inputs=[lat_input, lon_input],
outputs=[map_html, pollen_output, pollen_table]
)
if __name__ == "__main__":
app.launch()