Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import google.generativeai as genai | |
| from geopy.geocoders import Nominatim | |
| # from geopy.exc import GeocoderTimedOut, GeocoderUnavailable # Not explicitly caught, requests.timeout handles | |
| import folium | |
| from streamlit_folium import st_folium | |
| import pandas as pd | |
| import requests | |
| import re | |
| import os | |
| from datetime import datetime, timedelta | |
| # --- Page Configuration --- | |
| st.set_page_config( | |
| layout="wide", | |
| page_title="Landslide Factor Explorer | India", # More professional title | |
| page_icon="๐๏ธ", # Favicon | |
| initial_sidebar_state="collapsed" | |
| ) | |
| # Custom CSS for enhanced UI | |
| st.markdown(""" | |
| <style> | |
| /* Main styling */ | |
| .main .block-container { | |
| padding-top: 1rem; /* Reduced top padding */ | |
| padding-bottom: 2rem; | |
| padding-left: 2rem; /* Added horizontal padding */ | |
| padding-right: 2rem; /* Added horizontal padding */ | |
| } | |
| /* Header styling */ | |
| h1, h2, h3, h4, h5 { | |
| font-family: 'Roboto', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| color: #2c3e50; /* Dark blue-gray for headers */ | |
| } | |
| h1 { | |
| color: #1f618d; /* Slightly different color for main title if needed */ | |
| } | |
| /* Card-like containers */ | |
| .card { | |
| background-color: #FFFFFF; | |
| border-radius: 12px; /* Softer radius */ | |
| padding: 20px; | |
| box-shadow: 0 6px 12px rgba(0, 0, 0, 0.08); /* Softer shadow */ | |
| margin-bottom: 20px; | |
| border: 1px solid #e0e0e0; /* Light border */ | |
| } | |
| .data-card { /* Specific card for data sections */ | |
| background-color: #f9f9f9; /* Slightly off-white */ | |
| border-radius: 10px; | |
| padding: 15px; | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); | |
| margin-bottom: 15px; | |
| border-left: 5px solid #3498db; /* Accent color */ | |
| } | |
| /* For metric containers */ | |
| .metric-container { | |
| background-color: #f8f9fa; /* Lighter background */ | |
| border-radius: 8px; | |
| padding: 15px; | |
| border-left: 4px solid #1abc9c; /* Green accent */ | |
| margin-bottom: 10px; | |
| text-align: center; | |
| } | |
| .stMetric { /* Target Streamlit's metric component */ | |
| background-color: #ffffff; | |
| border-radius: 8px; | |
| padding: 15px 20px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.05); | |
| border: 1px solid #eee; | |
| } | |
| .stMetric > label { /* Metric label */ | |
| font-weight: 500 !important; | |
| color: #555 !important; | |
| } | |
| .stMetric > div:nth-child(2) > div { /* Metric value */ | |
| font-size: 1.6em !important; | |
| font-weight: 600 !important; | |
| color: #2c3e50 !important; | |
| } | |
| /* For maps */ | |
| .map-container { | |
| border-radius: 12px; | |
| overflow: hidden; | |
| border: 1px solid #ddd; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.05); | |
| } | |
| /* KPI badges (using Streamlit's delta color logic more directly) */ | |
| /* .stMetric [data-testid="stMetricDelta"] { ... } if specific styling is needed */ | |
| /* Data visualization enhancements */ | |
| .data-viz { /* For charts */ | |
| border-radius: 8px; | |
| overflow: hidden; | |
| border: 1px solid #eaeaea; | |
| padding: 10px; | |
| background-color: #fff; | |
| } | |
| /* Dividers */ | |
| hr { | |
| margin: 30px 0; | |
| border: 0; | |
| height: 1px; | |
| background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(44, 62, 80, 0.2), rgba(0, 0, 0, 0)); | |
| } | |
| /* Button enhancements */ | |
| .stButton>button { | |
| border-radius: 25px; | |
| font-weight: 600; | |
| padding: 10px 20px; | |
| transition: all 0.2s ease-in-out; | |
| border: 1px solid #3498db; /* Primary color border */ | |
| background-color: #3498db; /* Primary color */ | |
| color: white; | |
| } | |
| .stButton>button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 10px rgba(52, 152, 219, 0.3); | |
| background-color: #2980b9; /* Darker shade on hover */ | |
| border-color: #2980b9; | |
| } | |
| .stButton>button[kind="secondary"] { /* For reset button */ | |
| background-color: #e74c3c; | |
| border-color: #e74c3c; | |
| } | |
| .stButton>button[kind="secondary"]:hover { | |
| background-color: #c0392b; | |
| border-color: #c0392b; | |
| box-shadow: 0 5px 10px rgba(231, 76, 60, 0.3); | |
| } | |
| /* Tab styling */ | |
| .stTabs [data-baseweb="tab-list"] { | |
| gap: 10px; /* Increased gap */ | |
| border-bottom: 2px solid #ddd; /* Underline for tab list */ | |
| } | |
| .stTabs [data-baseweb="tab"] { | |
| border-radius: 6px 6px 0px 0px; | |
| padding: 12px 18px; /* More padding */ | |
| font-weight: 600; /* Bolder */ | |
| background-color: #f0f2f6; /* Light background for inactive tabs */ | |
| color: #555; | |
| transition: background-color 0.2s, color 0.2s; | |
| } | |
| .stTabs [data-baseweb="tab--selected"] { | |
| background-color: #3498db; /* Primary color for selected tab */ | |
| color: white; | |
| border-bottom: 2px solid #3498db; /* Ensure it aligns with tab list border */ | |
| } | |
| /* Primary header styling */ | |
| .main-header { | |
| background: white; | |
| color: #000080; /* Navy Blue - from original, kept for consistency */ | |
| padding: 15px 25px; | |
| border-radius: 12px; | |
| margin-bottom: 25px; | |
| text-align: center; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.1); | |
| } | |
| .main-header h1 { | |
| margin: 0; | |
| font-size: 2.2em; | |
| font-weight: 700; | |
| color: #2c3e50; /* Overriding the general h1 for this specific header */ | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.1); | |
| } | |
| /* Warning box styling */ | |
| .warning-box { | |
| background-color: #fff9e6; /* Lighter yellow */ | |
| border-left: 6px solid #ffc107; | |
| color: #856404; | |
| padding: 20px; | |
| border-radius: 8px; | |
| margin: 20px 0; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.05); | |
| } | |
| .warning-box h3 { | |
| margin-top: 0; | |
| color: #856404; /* Match text color */ | |
| font-weight: 600; | |
| } | |
| .warning-box ul { | |
| padding-left: 20px; | |
| margin-bottom: 0; | |
| } | |
| /* KPI metrics overall container styling */ | |
| .kpi-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-bottom: 20px; | |
| } | |
| /* Footer styling */ | |
| .footer { | |
| margin-top: 40px; | |
| text-align: center; | |
| padding: 25px; | |
| background-color: #34495e; /* Dark footer */ | |
| color: #ecf0f1; /* Light text for dark footer */ | |
| border-radius: 10px 10px 0 0; /* Rounded top corners */ | |
| font-size: 0.9em; | |
| } | |
| .footer a { | |
| color: #3498db; /* Link color */ | |
| text-decoration: none; | |
| } | |
| .footer a:hover { | |
| text-decoration: underline; | |
| } | |
| /* Search box enhancement */ | |
| .search-container .stTextInput input { | |
| border-radius: 25px !important; | |
| padding: 12px 20px !important; | |
| border: 1px solid #bdc3c7 !important; /* Light gray border */ | |
| box-shadow: none !important; /* Remove default Streamlit shadow */ | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| } | |
| .search-container .stTextInput input:focus { | |
| border-color: #3498db !important; /* Primary color on focus */ | |
| box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2) !important; | |
| } | |
| /* Styling for info/success messages */ | |
| .stAlert > div[data-baseweb="alert"] { | |
| border-radius: 8px !important; | |
| } | |
| /* Section headers */ | |
| .section-header { | |
| margin-top: 25px; | |
| margin-bottom: 15px; | |
| padding-bottom: 5px; | |
| border-bottom: 2px solid #3498db; /* Primary color underline */ | |
| display: inline-block; /* To make border only as wide as text */ | |
| } | |
| .section-header h4 { | |
| margin-bottom: 0; | |
| color: #3498db; /* Primary color for section titles */ | |
| } | |
| /* Expander styling */ | |
| .stExpander { | |
| border: 1px solid #e0e0e0 !important; | |
| border-radius: 8px !important; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.03) !important; | |
| } | |
| .stExpander header { | |
| background-color: #f8f9fa !important; | |
| border-radius: 8px 8px 0 0 !important; /* Match expander radius */ | |
| padding: 10px 15px !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- Gemini API Key Handling --- | |
| API_KEY = os.getenv("GOOGLE_API_KEY", "AIzaSyDkiYr-eSkqIXpZ1fHlik_YFsFtfQoFi0w") # Use yours, or allow env var | |
| if not API_KEY or API_KEY == "YOUR_API_KEY_HERE": # Default check | |
| st.sidebar.error("๐ด GOOGLE_API_KEY not set. Please set it as an environment variable or enter below.") | |
| API_KEY = st.sidebar.text_input("Enter your Gemini API Key:", type="password", key="api_key_input_explorer_v4") | |
| if API_KEY and API_KEY != "YOUR_API_KEY_HERE": | |
| try: | |
| genai.configure(api_key=API_KEY) | |
| except Exception as e: | |
| st.error(f"Error configuring Gemini API: {e}") | |
| st.stop() | |
| else: | |
| st.error("๐ด Gemini API Key is required to run this application.") | |
| st.stop() | |
| # --- Services & Constants --- | |
| geolocator = Nominatim(user_agent="india_landslide_explorer_v4") | |
| FORECAST_DAYS = 14 | |
| SEISMIC_RADIUS_KM = 150 | |
| SEISMIC_MIN_MAGNITUDE = 4.0 | |
| SEISMIC_DAYS_AGO = 30 | |
| # --- Session State Initialization --- | |
| if 'map_center_india' not in st.session_state: st.session_state.map_center_india = [20.5937, 78.9629] | |
| if 'map_zoom_india' not in st.session_state: st.session_state.map_zoom_india = 4 | |
| if 'selected_lat_lon' not in st.session_state: st.session_state.selected_lat_lon = None | |
| if 'location_name' not in st.session_state: st.session_state.location_name = "" | |
| if 'exploration_output' not in st.session_state: st.session_state.exploration_output = {"kpi_data": {}, "detailed_text": {}} | |
| if 'api_data_fetched' not in st.session_state: st.session_state.api_data_fetched = {} | |
| if 'is_fetching_data' not in st.session_state: st.session_state.is_fetching_data = False | |
| # --- Helper Functions (get_elevation, fetch_rainfall_data, fetch_seismic_data, reverse_geocode) --- | |
| def get_elevation(lat, lon): | |
| try: | |
| url = f"https://api.open-meteo.com/v1/elevation?latitude={lat}&longitude={lon}" | |
| response = requests.get(url, timeout=10) | |
| response.raise_for_status() | |
| data = response.json() | |
| return data['elevation'][0] | |
| except Exception: return "N/A" | |
| def fetch_rainfall_data(lat, lon, forecast_days_count=FORECAST_DAYS): | |
| url = "https://api.open-meteo.com/v1/forecast" | |
| params = { | |
| "latitude": lat, "longitude": lon, | |
| "daily": "precipitation_sum,precipitation_hours", | |
| "current": "precipitation,rain,showers,snowfall", | |
| "forecast_days": forecast_days_count, "timezone": "auto" | |
| } | |
| try: | |
| response = requests.get(url, params=params, timeout=15) | |
| response.raise_for_status() | |
| data = response.json() | |
| current_data = data.get("current", {}) | |
| daily_data = data.get("daily", {}) | |
| df_daily_forecast = pd.DataFrame() | |
| if daily_data.get("time") and daily_data.get("precipitation_sum"): | |
| df_daily_forecast = pd.DataFrame({ | |
| "Date": pd.to_datetime(daily_data["time"]), | |
| "Rainfall_Sum (mm)": daily_data["precipitation_sum"], | |
| "Precipitation_Hours (hrs)": daily_data.get("precipitation_hours", [0]*len(daily_data["time"])) | |
| }).set_index("Date") | |
| return { | |
| "current_precipitation_mm": current_data.get("precipitation", "N/A"), | |
| "current_rain_mm": current_data.get("rain", "N/A"), | |
| "current_showers_mm": current_data.get("showers", "N/A"), | |
| "current_snowfall_cm": current_data.get("snowfall", "N/A"), | |
| "daily_forecast_df": df_daily_forecast | |
| } | |
| except Exception as e: | |
| st.toast(f"Weather fetch error: {e}", icon="๐ฆ๏ธ") | |
| return {"current_precipitation_mm": "Error", "daily_forecast_df": pd.DataFrame()} | |
| def fetch_seismic_data(lat, lon, radius_km=SEISMIC_RADIUS_KM, min_mag=SEISMIC_MIN_MAGNITUDE, days_ago=SEISMIC_DAYS_AGO): | |
| try: | |
| end_time = datetime.utcnow() | |
| start_time = end_time - timedelta(days=days_ago) | |
| url = "https://earthquake.usgs.gov/fdsnws/event/1/query" | |
| params = { | |
| "format": "geojson", "latitude": lat, "longitude": lon, | |
| "maxradiuskm": radius_km, "minmagnitude": min_mag, | |
| "starttime": start_time.strftime("%Y-%m-%dT%H:%M:%S"), | |
| "endtime": end_time.strftime("%Y-%m-%dT%H:%M:%S"), "orderby": "time" | |
| } | |
| response = requests.get(url, params=params, timeout=15) | |
| response.raise_for_status() | |
| data = response.json() | |
| earthquakes = [] | |
| for feature in data.get("features", []): | |
| props = feature.get("properties", {}); geom = feature.get("geometry", {}) | |
| if props and geom and props.get("mag") is not None and geom.get("coordinates"): | |
| earthquakes.append({ | |
| "place": props.get("place", "Unknown"), "magnitude": props.get("mag"), | |
| "time": datetime.utcfromtimestamp(props.get("time") / 1000).strftime('%Y-%m-%d %H:%M UTC'), | |
| "depth_km": geom.get("coordinates")[2] if len(geom.get("coordinates", [])) > 2 else "N/A", | |
| "url": props.get("url")}) | |
| return earthquakes | |
| except Exception as e: | |
| st.toast(f"Seismic fetch error: {e}", icon="๐"); return [] | |
| def reverse_geocode(lat, lon): | |
| try: | |
| location = geolocator.reverse((lat, lon), exactly_one=True, timeout=10) | |
| return location.address if location else "Unknown location" | |
| except Exception: return "Could not determine address" | |
| # --- Gemini Prompt and Parsing V4 --- | |
| def get_gemini_exploration_v4(location_name, lat_lon, api_data): | |
| model = genai.GenerativeModel('gemini-1.5-flash-latest') | |
| elevation_str = f"{api_data.get('elevation_m', 'N/A')}" | |
| weather_data = api_data.get('weather', {}) | |
| current_precip_str = f"{weather_data.get('current_precipitation_mm', 'N/A')}" | |
| forecast_df = weather_data.get('daily_forecast_df') | |
| forecast_summary_str = "N/A" | |
| if forecast_df is not None and not forecast_df.empty: | |
| summary_days = min(7, len(forecast_df)) | |
| forecast_days_summary = [f"Day {i+1} ({forecast_df.index[i].strftime('%Y-%m-%d')}): {forecast_df['Rainfall_Sum (mm)'].iloc[i] if pd.notna(forecast_df['Rainfall_Sum (mm)'].iloc[i]) else 'N/A'} mm" for i in range(summary_days)] | |
| forecast_summary_str = "; ".join(forecast_days_summary) if forecast_days_summary else "No forecast data." | |
| elif isinstance(forecast_df, pd.DataFrame) and forecast_df.empty: | |
| forecast_summary_str = "Forecast data empty/unavailable." | |
| seismic_events = api_data.get('seismic', []) | |
| seismic_summary_str = "No significant recent seismic activity reported by USGS in the vicinity." | |
| if seismic_events: | |
| event_strs = [f"Mag {event['magnitude']} event near {event['place'].split('of')[-1].strip() if 'of' in event['place'] else event['place']}, on {event['time'].split(' ')[0]}" for event in seismic_events[:2]] | |
| seismic_summary_str = "Recent Seismic Activity: " + "; ".join(event_strs) | |
| if len(seismic_events) > 2: seismic_summary_str += f"; and {len(seismic_events)-2} more similar events." | |
| prompt = f""" | |
| You are an AI assistant for an advanced educational landslide factor exploration tool focused on INDIA (Version 4 - Visual Focus). | |
| This tool DOES NOT use specific user observations of local conditions. | |
| Your discussion will be based on the provided general location, fetched API data (elevation, weather, recent seismic activity), and your broad knowledge of Indian geography, geology, land cover, and climate. | |
| This is strictly for educational purposes to explore POTENTIAL factors for a TYPE of area, NOT a real-time prediction or specific site assessment. | |
| Location & Fetched Data: | |
| - Approximate Location Name: "{location_name}" (Lat/Lon: {lat_lon[0]:.4f}, {lat_lon[1]:.4f}) | |
| - Elevation: {elevation_str} meters | |
| - Current Precipitation Summary: {current_precip_str} mm | |
| - Rainfall Forecast Summary (e.g., next 7 days): {forecast_summary_str} | |
| - Recent Seismic Activity Summary (within ~{SEISMIC_RADIUS_KM}km, M{SEISMIC_MIN_MAGNITUDE}+, last {SEISMIC_DAYS_AGO} days): {seismic_summary_str} | |
| Task: | |
| Based on the above information and your general knowledge, please provide the following structured exploration. | |
| First, provide specific KPI data, then provide the detailed textual explanations. | |
| KPI_DATA_START | |
| GENERAL_SUSCEPTIBILITY_LEVEL: [Provide one single category: Low / Moderate / High / Very High - based on typical regional characteristics for this type of area] | |
| RAINFALL_IMPACT_ASSESSMENT: [Provide one single category: Low Concern / Moderate Concern / Significant Concern / High Concern - regarding its potential to trigger landslides in this type of area given the forecast and typical seasonal patterns] | |
| SEISMIC_IMPACT_ASSESSMENT: [Provide one single category: Negligible / Low Potential / Moderate Potential / Significant Potential - as a landslide trigger in this type of area, considering reported activity and general regional seismicity] | |
| TOP_HYPOTHETICAL_LANDSLIDE_TYPES: [List up to 3 most common/likely landslide types for similar regions in India, separated by commas, e.g., Debris Flow, Rockfall, Rotational Slump] | |
| KEY_CONTRIBUTING_FACTORS_POINTS: | |
| - [Brief point (max 10 words) on a key natural factor, e.g., Steep topography typical of the region] | |
| - [Brief point (max 10 words) on a key human-induced factor, e.g., Unplanned construction if prevalent in similar areas] | |
| - [Brief point (max 10 words) on another critical factor, e.g., Intense monsoon rainfall patterns] | |
| TYPICAL_LAND_COVER_INFERRED: [Describe in 1-3 words the most typical general land cover you infer for this type of region, e.g., Forested Slopes, Agricultural Terraces, Urbanizing Hillsides, Barren Rocky Terrain] | |
| KPI_DATA_END | |
| Now, provide the detailed textual explanations, structured with the following headers: | |
| HEADER_KEY_INSIGHTS_SUMMARY | |
| Provide 2-3 bullet points elaborating on the most critical potential landslide-related insights or considerations for this TYPE of area in India, building upon the KPI data. | |
| HEADER_SUSCEPTIBILITY_DISCUSSION | |
| A. General Discussion of Landslide Susceptibility for this TYPE of Area: | |
| (Elaborate on the GENERAL_SUSCEPTIBILITY_LEVEL. Discuss typical geological features, soil types, or topographical characteristics for this type of area. Ensure the output for GENERAL_SUSCEPTIBILITY_LEVEL provided in KPI_DATA_START is consistent with this discussion and includes "General Susceptibility for this type of area: " before the level, e.g., "General Susceptibility for this type of area: Moderate"). | |
| HEADER_DATA_ANALYSIS | |
| B. Analysis of Fetched Data in Context of Potential Landslides: | |
| (Elaborate on RAINFALL_IMPACT_ASSESSMENT and SEISMIC_IMPACT_ASSESSMENT. Discuss how fetched rainfall, elevation, and seismic data influence landslide potential, considering seasonal patterns and regional context). | |
| HEADER_HYPOTHETICAL_FACTORS | |
| C. Hypothetical Contributing Factors (Beyond Fetched Data): | |
| (Elaborate on KEY_CONTRIBUTING_FACTORS_POINTS and TYPICAL_LAND_COVER_INFERRED. Discuss typical land cover and other natural/human-induced factors common to such regions in India). | |
| HEADER_COMMON_LANDSLIDE_TYPES | |
| D. Common Landslide Types in Similar Indian Regions: | |
| (Elaborate on TOP_HYPOTHETICAL_LANDSLIDE_TYPES. Describe their characteristics and triggers relevant to the scenario). | |
| HEADER_CRITICAL_LOCAL_DATA_NEED | |
| E. Critical Importance of Local Site-Specific Data (Emphasize very strongly!): | |
| (Explain why absence of local observations makes specific risk assessment impossible. Detail necessary local data). | |
| HEADER_AWARENESS_PREPAREDNESS | |
| F. General Awareness & Preparedness Ideas (India Context): | |
| (Suggest general, non-site-specific educational points on landslide awareness/preparedness). | |
| HEADER_OFFICIAL_RESOURCES | |
| G. Official Indian Resources & Further Learning: | |
| (List key Indian government agencies and information sources). | |
| Structure your response exactly with the specified KPI_DATA_START/END and HEADER_ SECTION NAMES. | |
| Maintain an educational tone. Explicitly and repeatedly state the limitations. | |
| """ | |
| try: | |
| response = model.generate_content(prompt) | |
| return response.text | |
| except Exception as e: | |
| st.error(f"Error communicating with Gemini API: {e}") | |
| return None | |
| def parse_gemini_output_v4(text): | |
| if not text: return {"kpi_data": {}, "detailed_text": {}} | |
| kpi_data = {} | |
| default_kpi_values = { | |
| "GENERAL_SUSCEPTIBILITY_LEVEL": "N/A", | |
| "RAINFALL_IMPACT_ASSESSMENT": "N/A", | |
| "SEISMIC_IMPACT_ASSESSMENT": "N/A", | |
| "TOP_HYPOTHETICAL_LANDSLIDE_TYPES": "Not specified", | |
| "KEY_CONTRIBUTING_FACTORS_POINTS": [], | |
| "TYPICAL_LAND_COVER_INFERRED": "N/A" | |
| } | |
| kpi_data.update(default_kpi_values) | |
| detailed_text_sections_map = { | |
| "HEADER_KEY_INSIGHTS_SUMMARY": "๐ Key Insights Summary", | |
| "HEADER_SUSCEPTIBILITY_DISCUSSION": "๐ง General Susceptibility Discussion", | |
| "HEADER_DATA_ANALYSIS": "๐ Analysis of Fetched Data", | |
| "HEADER_HYPOTHETICAL_FACTORS": "๐ค Contributing Factors", | |
| "HEADER_COMMON_LANDSLIDE_TYPES": "๐๏ธ Common Landslide Types", | |
| "HEADER_CRITICAL_LOCAL_DATA_NEED": "โCRUCIAL: Need for Local Site-Specific Dataโ", | |
| "HEADER_AWARENESS_PREPAREDNESS": "๐ก General Awareness & Preparedness", | |
| "HEADER_OFFICIAL_RESOURCES": "๐ฎ๐ณ Official Resources & Further Learning" | |
| } | |
| parsed_detailed_text = {display_name: [] for _, display_name in detailed_text_sections_map.items()} | |
| in_kpi_section = False | |
| current_detailed_section_key = None | |
| key_factors_collecting = False | |
| kpi_regex_map = { | |
| "GENERAL_SUSCEPTIBILITY_LEVEL": re.compile(r"GENERAL_SUSCEPTIBILITY_LEVEL:\s*(.+)", re.IGNORECASE), | |
| "RAINFALL_IMPACT_ASSESSMENT": re.compile(r"RAINFALL_IMPACT_ASSESSMENT:\s*(.+)", re.IGNORECASE), | |
| "SEISMIC_IMPACT_ASSESSMENT": re.compile(r"SEISMIC_IMPACT_ASSESSMENT:\s*(.+)", re.IGNORECASE), | |
| "TOP_HYPOTHETICAL_LANDSLIDE_TYPES": re.compile(r"TOP_HYPOTHETICAL_LANDSLIDE_TYPES:\s*(.+)", re.IGNORECASE), | |
| "TYPICAL_LAND_COVER_INFERRED": re.compile(r"TYPICAL_LAND_COVER_INFERRED:\s*(.+)", re.IGNORECASE), | |
| } | |
| for line in text.splitlines(): | |
| line_strip = line.strip() | |
| if not line_strip: continue | |
| if line_strip == "KPI_DATA_START": | |
| in_kpi_section = True; continue | |
| if line_strip == "KPI_DATA_END": | |
| in_kpi_section = False; key_factors_collecting = False; continue | |
| if in_kpi_section: | |
| matched_specific_kpi = False | |
| for key, pattern in kpi_regex_map.items(): | |
| match = pattern.match(line_strip) | |
| if match: | |
| kpi_data[key] = match.group(1).strip() | |
| matched_specific_kpi = True; break | |
| if matched_specific_kpi: continue | |
| if line_strip.startswith("KEY_CONTRIBUTING_FACTORS_POINTS:"): | |
| key_factors_collecting = True; kpi_data["KEY_CONTRIBUTING_FACTORS_POINTS"] = [] # Reset for new parse | |
| continue | |
| if key_factors_collecting and line_strip.startswith("-"): | |
| kpi_data["KEY_CONTRIBUTING_FACTORS_POINTS"].append(line_strip.lstrip("- ").strip()) | |
| continue | |
| found_new_header = False | |
| for header_key_from_prompt, display_name in detailed_text_sections_map.items(): | |
| if line_strip == header_key_from_prompt: | |
| current_detailed_section_key = display_name | |
| found_new_header = True; break | |
| if not found_new_header and current_detailed_section_key: | |
| parsed_detailed_text[current_detailed_section_key].append(line) | |
| final_detailed_text = {k: "\n".join(v).strip() for k, v in parsed_detailed_text.items()} | |
| return {"kpi_data": kpi_data, "detailed_text": final_detailed_text} | |
| # --- UI Rendering with Enhanced Styling --- | |
| st.markdown('<div class="main-header"><h1>๐ฎ๐ณ India Landslide Factor Explorer V4</h1></div>', unsafe_allow_html=True) | |
| st.caption("Educational Tool by Google Gemini & Streamlit - Exploring Potential Landslide Factors") | |
| st.markdown(""" | |
| <div class="card"> | |
| <h3>๐บ๏ธ How to Use This Tool</h3> | |
| <p>Welcome! Begin by <strong>selecting a location on the map</strong> or using the <strong>search bar</strong> to find a specific place in India. | |
| The tool will then fetch publicly available data (elevation, weather forecast, recent seismic activity) for the chosen area. | |
| After data retrieval, you can initiate an AI-powered exploration. The AI will provide a <em>generalized discussion</em> on potential landslide susceptibility and contributing factors relevant to that <strong>type of area in India</strong>, based on the fetched data and its broad geographical knowledge.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="warning-box"> | |
| <h3>โ ๏ธ CRITICAL DISCLAIMER & LIMITATIONS</h3> | |
| <ul> | |
| <li>This tool <strong>DOES NOT use any specific local observations or detailed site-specific geotechnical data</strong>.</li> | |
| <li>The AI-generated discussion is <strong>HIGHLY GENERALIZED, HYPOTHETICAL, and intended for BROAD EDUCATIONAL PURPOSES ONLY</strong>.</li> | |
| <li><strong>IT IS NOT A PREDICTION, nor a real-time warning system, nor a site-specific risk assessment.</strong> It cannot replace professional engineering or geological surveys.</li> | |
| <li>For actual safety information, risk assessment, or emergency guidance, <strong>ALWAYS consult official Indian government authorities</strong> (like NDMA, GSI) and qualified local geotechnical experts.</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| col_map_input, col_ai_output = st.columns([0.45, 0.55]) # Adjusted column ratio | |
| with col_map_input: | |
| st.markdown('<div class="card">', unsafe_allow_html=True) # Wrap entire input column in a card | |
| st.markdown('<div class="section-header"><h4>๐ Select Location & View Data</h4></div>', unsafe_allow_html=True) | |
| st.markdown('<div class="search-container">', unsafe_allow_html=True) | |
| search_location_input = st.text_input( | |
| "Search for a location in India:", | |
| key="search_loc_v4", | |
| placeholder="e.g., Shimla, Munnar, Darjeeling..." | |
| ) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| search_btn_col, reset_btn_col = st.columns([3,1]) | |
| with search_btn_col: | |
| if st.button("๐ Search Location", key="search_btn_v4", use_container_width=True): | |
| if search_location_input: | |
| with st.spinner(f"Searching for '{search_location_input}'..."): | |
| try: | |
| loc = geolocator.geocode(search_location_input + ", India", timeout=10) | |
| if loc: | |
| st.session_state.selected_lat_lon = [loc.latitude, loc.longitude] | |
| st.session_state.map_center_india = [loc.latitude, loc.longitude] | |
| st.session_state.map_zoom_india = 11 | |
| st.session_state.location_name = loc.address | |
| st.session_state.api_data_fetched = {} | |
| st.session_state.exploration_output = {"kpi_data": {}, "detailed_text": {}} | |
| st.session_state.is_fetching_data = True # Trigger data fetching | |
| st.toast(f"๐บ๏ธ Location found: {loc.address.split(',')[0]}. Fetching data...", icon="โ ") | |
| st.rerun() | |
| else: | |
| st.warning(f"โ Could not find '{search_location_input}'. Please try a different or more specific name.") | |
| except Exception as e: | |
| st.error(f"Geocoding error: {e}") | |
| else: | |
| st.info("Please enter a location name to search.") | |
| with reset_btn_col: | |
| if st.button("๐ Reset", key="reset_btn_v4", use_container_width=True, type="secondary"): | |
| st.session_state.selected_lat_lon = None | |
| st.session_state.map_center_india = [20.5937, 78.9629] | |
| st.session_state.map_zoom_india = 4 | |
| st.session_state.location_name = "" | |
| st.session_state.api_data_fetched = {} | |
| st.session_state.exploration_output = {"kpi_data": {}, "detailed_text": {}} | |
| st.session_state.is_fetching_data = False | |
| st.toast("๐ Map & selection reset.", icon="๐บ๏ธ") | |
| st.rerun() | |
| st.markdown('<div class="map-container">', unsafe_allow_html=True) | |
| st.markdown("<small><i>Click on the map to select a point, or use search above.</i></small>", unsafe_allow_html=True) | |
| folium_map_display = folium.Map( | |
| location=st.session_state.map_center_india, | |
| zoom_start=st.session_state.map_zoom_india, | |
| tiles="CartoDB positron", | |
| key="folium_map_v4_instance" # Ensure unique key if map is complex | |
| ) | |
| if st.session_state.selected_lat_lon: | |
| folium.Marker( | |
| st.session_state.selected_lat_lon, | |
| popup=f"Selected: {st.session_state.location_name.split(',')[0]}" if st.session_state.location_name else "Selected Point", | |
| tooltip="Current Selection", | |
| icon=folium.Icon(color="red", icon="info-sign") | |
| ).add_to(folium_map_display) | |
| map_interaction_data = st_folium( | |
| folium_map_display, | |
| width="100%", | |
| height=330, | |
| key="map_v4_interaction", | |
| returned_objects=["last_clicked"] | |
| ) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| if map_interaction_data and map_interaction_data.get("last_clicked"): | |
| clicked_lat = map_interaction_data["last_clicked"]["lat"] | |
| clicked_lon = map_interaction_data["last_clicked"]["lng"] | |
| if st.session_state.selected_lat_lon is None or \ | |
| abs(st.session_state.selected_lat_lon[0] - clicked_lat) > 0.00001 or \ | |
| abs(st.session_state.selected_lat_lon[1] - clicked_lon) > 0.00001: | |
| st.session_state.selected_lat_lon = [clicked_lat, clicked_lon] | |
| st.session_state.location_name = reverse_geocode(clicked_lat, clicked_lon) | |
| st.session_state.map_center_india = [clicked_lat, clicked_lon] # Recenter map | |
| st.session_state.map_zoom_india = max(st.session_state.map_zoom_india, 11) # Zoom in | |
| st.session_state.api_data_fetched = {} | |
| st.session_state.exploration_output = {"kpi_data": {}, "detailed_text": {}} | |
| st.session_state.is_fetching_data = True # Trigger data fetching | |
| st.toast(f"๐ Pinned: {st.session_state.location_name.split(',')[0]}. Fetching data...", icon="๐บ๏ธ") | |
| st.rerun() | |
| explore_button_active = False | |
| if st.session_state.selected_lat_lon: | |
| st.success(f"**Selected Location:** {st.session_state.location_name}\n(Lat: {st.session_state.selected_lat_lon[0]:.4f}, Lon: {st.session_state.selected_lat_lon[1]:.4f})") | |
| if st.session_state.is_fetching_data and not st.session_state.api_data_fetched: # Fetch data only if flag is true and not fetched | |
| with st.spinner(f"โณ Fetching environmental data for {st.session_state.location_name.split(',')[0]}... This might take a few seconds."): | |
| lat, lon = st.session_state.selected_lat_lon | |
| api_data_temp = {} | |
| api_data_temp['elevation_m'] = get_elevation(lat, lon) | |
| api_data_temp['weather'] = fetch_rainfall_data(lat, lon) | |
| api_data_temp['seismic'] = fetch_seismic_data(lat, lon) | |
| st.session_state.api_data_fetched = api_data_temp | |
| st.session_state.is_fetching_data = False # Reset flag | |
| st.rerun() # Rerun to display fetched data | |
| if st.session_state.api_data_fetched: # Display fetched data | |
| st.markdown('<div class="data-card">', unsafe_allow_html=True) | |
| st.markdown("##### ๐ฐ๏ธ Fetched Environmental Data:") | |
| api_data = st.session_state.api_data_fetched | |
| elev = api_data.get('elevation_m', 'N/A') | |
| weather = api_data.get('weather', {}) | |
| curr_precip = weather.get('current_precipitation_mm', 'N/A') | |
| seismic_events = api_data.get('seismic', []) | |
| data_cols = st.columns(2) | |
| with data_cols[0]: | |
| st.metric(label="๐๏ธ Elevation", value=f"{elev} m" if elev != "N/A" else "N/A") | |
| with data_cols[1]: | |
| st.metric(label="๐ง Current Precip.", value=f"{curr_precip} mm" if curr_precip not in ["N/A", "Error"] else curr_precip) | |
| with st.expander(f"๐ Seismic Activity (Last {SEISMIC_DAYS_AGO} days, M{SEISMIC_MIN_MAGNITUDE}+, ~{SEISMIC_RADIUS_KM}km radius)", expanded=len(seismic_events) > 0): | |
| if seismic_events: | |
| st.caption(f"Found {len(seismic_events)} significant earthquake(s) reported by USGS:") | |
| for event in seismic_events[:5]: | |
| st.markdown(f"- **M {event['magnitude']}** - {event['place']} ({event['time']}). Depth: {event['depth_km']} km. [More Info]({event.get('url', '#')})", unsafe_allow_html=True) | |
| if len(seismic_events) > 5: st.caption(f"...and {len(seismic_events)-5} more.") | |
| else: | |
| st.caption("No significant recent seismic activity reported by USGS matching criteria.") | |
| st.markdown("##### ๐ฆ๏ธ Rainfall Forecast (mm/day):") | |
| forecast_df = weather.get('daily_forecast_df') | |
| if forecast_df is not None and not forecast_df.empty: | |
| st.markdown('<div class="data-viz">', unsafe_allow_html=True) | |
| st.line_chart(forecast_df['Rainfall_Sum (mm)'], height=180) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| cum_rain = forecast_df['Rainfall_Sum (mm)'].cumsum() | |
| periods = [3, 7, min(FORECAST_DAYS, len(cum_rain))] | |
| st.markdown("**Cumulative Rainfall Forecast:**") | |
| cum_cols_display = st.columns(len(periods)) | |
| for i, p_days in enumerate(periods): | |
| if 0 < p_days <= len(cum_rain): | |
| val = cum_rain.iloc[p_days-1] | |
| with cum_cols_display[i]: | |
| st.metric(label=f"{p_days}-Day Total", value=f"{val:.1f}mm" if pd.notna(val) else "N/A") | |
| else: | |
| st.caption("Rainfall forecast data unavailable or encountered an error.") | |
| st.markdown('</div>', unsafe_allow_html=True) # End data-card | |
| if elev != "N/A" and curr_precip not in ["N/A", "Error"]: # Enable button if core data is present | |
| explore_button_active = True | |
| else: # No location selected | |
| st.info("๐ Please select a location on the map or use the search bar to begin.") | |
| if explore_button_active: | |
| if st.button("๐ค Explore Potential Factors with AI", type="primary", use_container_width=True, key="explore_btn_v4"): | |
| if st.session_state.selected_lat_lon and st.session_state.api_data_fetched: | |
| with st.spinner("๐ก Gemini AI is analyzing... This may take a moment for a comprehensive exploration."): | |
| raw_gemini_output = get_gemini_exploration_v4( | |
| st.session_state.location_name, | |
| st.session_state.selected_lat_lon, | |
| st.session_state.api_data_fetched | |
| ) | |
| if raw_gemini_output: | |
| st.session_state.exploration_output = parse_gemini_output_v4(raw_gemini_output) | |
| st.toast("โ AI Exploration complete!", icon="๐ก") | |
| else: | |
| st.error("AI exploration failed. Please check API key or try again later.") | |
| else: | |
| st.warning("Please select a location and ensure data is fetched before exploring.") | |
| elif st.session_state.selected_lat_lon and not st.session_state.api_data_fetched and not st.session_state.is_fetching_data: | |
| st.warning("Data for the selected location is still fetching or incomplete. AI exploration is disabled until data is ready.") | |
| st.markdown('</div>', unsafe_allow_html=True) # End of card for col_map_input | |
| with col_ai_output: | |
| st.markdown('<div class="card">', unsafe_allow_html=True) # Wrap entire AI output column in a card | |
| st.markdown('<div class="section-header"><h4>๐ AI-Powered Exploration (Generalized)</h4></div>', unsafe_allow_html=True) | |
| output_data = st.session_state.exploration_output | |
| kpi_results = output_data.get("kpi_data", {}) | |
| detailed_results = output_data.get("detailed_text", {}) | |
| if kpi_results and any(val != "N/A" and val != "Not specified" and val for val in kpi_results.values()): | |
| st.markdown("##### ๐ Key Indicators (AI Inferred for this Type of Area):") | |
| st.markdown('<div class="kpi-grid">', unsafe_allow_html=True) | |
| sus_level = kpi_results.get("GENERAL_SUSCEPTIBILITY_LEVEL", "N/A") | |
| sus_delta_color = "normal" | |
| if "low" in sus_level.lower(): sus_delta_color = "normal" | |
| elif "moderate" in sus_level.lower(): sus_delta_color = "off" | |
| elif "high" in sus_level.lower() or "very high" in sus_level.lower(): sus_delta_color = "inverse" | |
| st.metric(label="๐๏ธ General Susceptibility", value=sus_level, delta_color=sus_delta_color, help="AI's assessment of general landslide susceptibility for this type of area in India, based on broad knowledge.") | |
| rain_impact = kpi_results.get("RAINFALL_IMPACT_ASSESSMENT", "N/A") | |
| rain_delta_color = "normal" | |
| if "low" in rain_impact.lower(): rain_delta_color = "normal" | |
| elif "moderate" in rain_impact.lower(): rain_delta_color = "off" | |
| elif "significant" in rain_impact.lower() or "high" in rain_impact.lower(): rain_delta_color = "inverse" | |
| st.metric(label="๐ง Rainfall Impact", value=rain_impact, delta_color=rain_delta_color, help="AI's assessment of rainfall's potential role, considering forecast and typical seasonal patterns for the area type.") | |
| seismic_impact = kpi_results.get("SEISMIC_IMPACT_ASSESSMENT", "N/A") | |
| seis_delta_color = "normal" | |
| if "negligible" in seismic_impact.lower() or "low" in seismic_impact.lower(): seis_delta_color = "normal" | |
| elif "moderate" in seismic_impact.lower(): seis_delta_color = "off" | |
| elif "significant" in seismic_impact.lower(): seis_delta_color = "inverse" | |
| st.metric(label="๐ Seismic Impact", value=seismic_impact, delta_color=seis_delta_color, help="AI's assessment of seismic activity's potential role as a trigger for this type of area.") | |
| st.markdown('</div>', unsafe_allow_html=True) # End kpi-grid | |
| st.markdown("---") | |
| col_kpi_list1, col_kpi_list2 = st.columns(2) | |
| with col_kpi_list1: | |
| st.markdown("##### ๐๏ธ Top Landslide Types:") | |
| top_types_str = kpi_results.get("TOP_HYPOTHETICAL_LANDSLIDE_TYPES", "Not specified by AI.") | |
| top_types_list = [s.strip() for s in top_types_str.split(',') if s.strip()] | |
| if top_types_list and top_types_list[0].lower() != "not specified": | |
| for l_type in top_types_list: st.markdown(f"- {l_type}") | |
| else: st.caption(top_types_str) | |
| with col_kpi_list2: | |
| st.markdown("##### ๐ณ Typical Land Cover (Inferred):") | |
| land_cover = kpi_results.get("TYPICAL_LAND_COVER_INFERRED", "Not specified by AI.") | |
| st.info(f"{land_cover}") | |
| st.markdown("##### ๐ Key Contributing Factors:") | |
| key_factors = kpi_results.get("KEY_CONTRIBUTING_FACTORS_POINTS", []) | |
| if key_factors and isinstance(key_factors, list) and any(key_factors): | |
| for factor in key_factors: st.markdown(f"- _{factor}_") | |
| else: st.caption("Not specified or N/A by AI.") | |
| st.markdown("---") | |
| st.markdown("##### ๐ฌ Detailed AI Exploration Text:") | |
| tab_titles = [key for key in detailed_results.keys() if detailed_results[key]] | |
| if tab_titles: | |
| tabs = st.tabs(tab_titles) | |
| for i, title in enumerate(tab_titles): | |
| with tabs[i]: | |
| st.markdown(detailed_results[title], unsafe_allow_html=True) # Allow HTML for Gemini's formatting | |
| else: | |
| st.warning("AI exploration did not yield detailed textual content. The API might have had an issue or the prompt needs adjustment.") | |
| if 'gemini_raw_output_debug' in st.session_state and st.session_state['gemini_raw_output_debug']: | |
| with st.expander("Show Raw Gemini Output (for debugging)"): | |
| st.text_area("Raw Output:", st.session_state['gemini_raw_output_debug'], height=200) | |
| elif st.session_state.selected_lat_lon and explore_button_active: | |
| st.info("๐ค Click the 'Explore Potential Factors with AI' button on the left panel after data for the selected location has been fetched. The AI's insights will appear here.") | |
| elif not st.session_state.selected_lat_lon: | |
| st.info("๐ Please select a location in the left panel first. AI exploration results will then be generated and displayed here.") | |
| else: | |
| st.info("AI exploration results will appear here once a location is selected, data is fetched, and the AI analysis is run.") | |
| st.markdown("---") | |
| st.markdown('<div class="section-header"><h4>๐ฎ๐ณ Official Indian Resources</h4></div>', unsafe_allow_html=True) | |
| st.markdown(""" | |
| For accurate, official, and site-specific landslide information and warnings in India, please consult these primary resources: | |
| - **National Disaster Management Authority (NDMA):** [ndma.gov.in](https://ndma.gov.in) - For national guidelines and disaster management. | |
| - **Geological Survey of India (GSI):** [gsi.gov.in](https://www.gsi.gov.in/) - For geological data, landslide hazard zonation maps. | |
| - **National Remote Sensing Centre (NRSC) Bhuvan Portal (ISRO):** [bhuvan.nrsc.gov.in](https://bhuvan.nrsc.gov.in/bhuvan_links.php) - For satellite imagery, thematic maps & disaster related services. | |
| - **India Meteorological Department (IMD):** [mausam.imd.gov.in](https://mausam.imd.gov.in/) - For weather forecasts and warnings. | |
| - **Your local State Disaster Management Authority (SDMA)** website (search for your state's SDMA). | |
| """, unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) # End of card for col_ai_output | |
| st.markdown("---") | |
| st.markdown( | |
| """ | |
| <div class="footer"> | |
| <p><strong>Tool Version:</strong> Explorer 4.0 Enhanced UI</p> | |
| <p>This is an <strong>educational tool</strong> for exploring POTENTIAL landslide factors based on generalized knowledge and limited public data. | |
| It <strong>DOES NOT</strong> provide official warnings, site-specific risk assessments, or professional geotechnical advice. | |
| Real-world landslide analysis requires extensive, detailed local data and expert assessment by qualified professionals. | |
| Always refer to official government sources for safety and risk information.</p> | |
| <p>Powered by <a href="https://streamlit.io" target="_blank">Streamlit</a> and <a href="https://ai.google.dev/" target="_blank">Google Gemini</a>.</p> | |
| </div> | |
| """, unsafe_allow_html=True | |
| ) |