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("""

🌸 Pollen Forecast Map

Get detailed, scientifically-accurate pollen forecasts with actual particle concentrations!

🎯 How to Use:
β€’ Search by Address: Type any city, address, or landmark in the search box
β€’ Use Quick Cities: Click preset buttons for major cities worldwide
β€’ Enter Coordinates: Input precise latitude/longitude values
β€’ Map Note: The map displays your location but doesn't support clicking to select new points
""") with gr.Row(): with gr.Column(scale=1): gr.HTML("

πŸ“ Location Selection

") # Address search gr.HTML("

πŸ” Search by Address

") 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("

πŸ“ Or Use Coordinates

") # 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("

πŸ™οΈ Popular Cities

") 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("

πŸ—ΊοΈ Location Map

") gr.HTML("""
πŸ“ Note: 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.
""") 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()