Spaces:
Sleeping
Sleeping
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() |