from flask import Flask, render_template, request import pandas as pd import folium from folium.plugins import HeatMap, MarkerCluster, Fullscreen, MiniMap from folium.raster_layers import TileLayer from datetime import datetime, timedelta, timezone import re import google.generativeai as genai import os import requests # <--- CORRECTED: Added requests import import toml # For potentially reading secrets.toml from datetime import datetime, timezone # Make sure timezone is imported # --- Configuration --- DEFAULT_MIN_LAT_INDIA = 6.0 DEFAULT_MAX_LAT_INDIA = 38.0 DEFAULT_MIN_LON_INDIA = 68.0 DEFAULT_MAX_LON_INDIA = 98.0 DEFAULT_REGION_NAME_INDIA = "India & Surrounding" TSUNAMI_MAG_THRESHOLD = 6.8 TSUNAMI_DEPTH_THRESHOLD = 70 OFFSHORE_KEYWORDS = ["sea", "ocean", "off the coast", "ridge", "trench", "gulf", "bay", "islands region", "strait"] TECTONIC_PLATES_URL = "https://raw.githubusercontent.com/fraxen/tectonicplates/master/GeoJSON/PB2002_boundaries.json" USGS_API_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query" # --- Load API Key --- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") if not GEMINI_API_KEY: try: secrets_path = os.path.join(os.path.dirname(__file__), '.streamlit', 'secrets.toml') if os.path.exists(secrets_path): secrets = toml.load(secrets_path) GEMINI_API_KEY = secrets.get("GEMINI_API_KEY") except Exception as e: print(f"Could not load secrets.toml: {e}") GEMINI_API_KEY = None if not GEMINI_API_KEY: print("WARNING: GEMINI_API_KEY not found. AI features will be disabled.") # --- API Functions --- def fetch_earthquake_data(starttime, endtime, min_mag, min_lat, max_lat, min_lon, max_lon, source="USGS"): print(f"Fetching data from {source} from {starttime.strftime('%Y-%m-%d %H:%M')} to {endtime.strftime('%Y-%m-%d %H:%M')} for Mag >= {min_mag}...") params = { 'format': 'geojson', 'starttime': starttime.strftime('%Y-%m-%dT%H:%M:%S'), 'endtime': endtime.strftime('%Y-%m-%dT%H:%M:%S'), 'minlatitude': min_lat, 'maxlatitude': max_lat, 'minlongitude': min_lon, 'maxlongitude': max_lon, 'minmagnitude': min_mag, 'orderby': 'time' } try: response = requests.get(USGS_API_URL, params=params, timeout=30) response.raise_for_status() data = response.json() features = data.get('features', []) earthquakes = [] for feature in features: properties = feature.get('properties', {}) geometry = feature.get('geometry', {}) coordinates = geometry.get('coordinates', [None, None, None]) earthquakes.append({ 'id': feature.get('id'), 'magnitude': properties.get('mag'), 'place': properties.get('place'), 'time': pd.to_datetime(properties.get('time'), unit='ms', utc=True), 'url': properties.get('url'), 'longitude': coordinates[0], 'latitude': coordinates[1], 'depth': coordinates[2] }) df = pd.DataFrame(earthquakes) if not df.empty: df = df.sort_values(by='time', ascending=False) print(f"Fetched {len(df)} earthquakes from {source}.") return df except requests.exceptions.RequestException as e: print(f"Error fetching data from {source}: {e}") return pd.DataFrame() except Exception as e: print(f"An unexpected error occurred during data fetching: {e}") return pd.DataFrame() def fetch_tectonic_plates_data(url): print("Fetching tectonic plate boundaries data...") try: response = requests.get(url, timeout=30) response.raise_for_status() print("Tectonic plate data fetched.") return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching tectonic plate data: {e}") return None # --- Helper Functions --- def is_offshore(place_description, depth_km): if place_description is None or depth_km is None: return False place_lower = str(place_description).lower() for keyword in OFFSHORE_KEYWORDS: if keyword in place_lower: return True return False def get_marker_color_by_magnitude(magnitude): if magnitude is None: return 'gray' if magnitude < 4.0: return 'green' elif magnitude < 5.0: return 'blue' elif magnitude < 6.0: return 'orange' elif magnitude < 7.0: return 'red' else: return 'darkred' # --- Gemini LLM Function --- def get_gemini_interpretation(api_key, data_summary_prompt): if not api_key: return "API Key not configured for Gemini." try: genai.configure(api_key=api_key) model = genai.GenerativeModel('gemini-1.5-flash-latest') response = model.generate_content(data_summary_prompt) return response.text except Exception as e: print(f"Error communicating with Gemini API: {e}") return "Could not retrieve interpretation from AI model." app = Flask(__name__) predefined_regions_dict = { "India & Surrounding": (DEFAULT_MIN_LAT_INDIA, DEFAULT_MAX_LAT_INDIA, DEFAULT_MIN_LON_INDIA, DEFAULT_MAX_LON_INDIA), "Indian Ocean Region (Tsunami Focus)": (-20, 35, 40, 120), "Northern India (Himalayan Belt)": (25, 38, 70, 98), "Andaman & Nicobar Region": (5, 15, 90, 95), "Global (Significant Quakes)": (-60, 60, -180, 180) } def get_default_configs_for_region(region_name): if region_name == "Global (Significant Quakes)": return {"days_historical": 730, "min_magnitude_historical": 4.5, "days_recent": 7, "min_magnitude_recent": 4.0, "alert_threshold_magnitude": 5.0, "show_tectonic_plates": True, "enable_ai_interpretation": bool(GEMINI_API_KEY)} elif region_name == "Indian Ocean Region (Tsunami Focus)": return {"days_historical": 730, "min_magnitude_historical": 4.0, "days_recent": 7, "min_magnitude_recent": 3.5, "alert_threshold_magnitude": 4.5, "show_tectonic_plates": True, "enable_ai_interpretation": bool(GEMINI_API_KEY)} else: return {"days_historical": 730, "min_magnitude_historical": 3.0, "days_recent": 7, "min_magnitude_recent": 2.5, "alert_threshold_magnitude": 3.5, "show_tectonic_plates": True, "enable_ai_interpretation": bool(GEMINI_API_KEY)} @app.route('/', methods=['GET', 'POST']) def index(): map_html = None historical_df = pd.DataFrame() recent_df = pd.DataFrame() significant_quakes = [] tsunami_potential_events = [] ai_interpretation = None initial_load = True current_config = {} if request.method == 'POST': initial_load = False current_config['selected_region_name'] = request.form.get('selected_region_name', DEFAULT_REGION_NAME_INDIA) # Load defaults for selected region first, then override with form values defaults = get_default_configs_for_region(current_config['selected_region_name']) current_config.update(defaults) current_config['days_historical'] = int(request.form.get('days_historical', defaults['days_historical'])) current_config['min_magnitude_historical'] = float(request.form.get('min_magnitude_historical', defaults['min_magnitude_historical'])) current_config['days_recent'] = int(request.form.get('days_recent', defaults['days_recent'])) current_config['min_magnitude_recent'] = float(request.form.get('min_magnitude_recent', defaults['min_magnitude_recent'])) current_config['alert_threshold_magnitude'] = float(request.form.get('alert_threshold_magnitude', defaults['alert_threshold_magnitude'])) current_config['show_tectonic_plates'] = request.form.get('show_tectonic_plates') == 'True' current_config['enable_ai_interpretation'] = request.form.get('enable_ai_interpretation') == 'True' else: # GET initial_load = True current_config['selected_region_name'] = DEFAULT_REGION_NAME_INDIA defaults = get_default_configs_for_region(DEFAULT_REGION_NAME_INDIA) current_config.update(defaults) if request.method == 'POST': # Process data only on POST min_lat, max_lat, min_lon, max_lon = predefined_regions_dict[current_config['selected_region_name']] end_time_global = datetime.now(timezone.utc) start_time_historical = end_time_global - timedelta(days=current_config['days_historical']) start_time_recent = end_time_global - timedelta(days=current_config['days_recent']) historical_df = fetch_earthquake_data(start_time_historical, end_time_global, current_config['min_magnitude_historical'], min_lat, max_lat, min_lon, max_lon) recent_df = fetch_earthquake_data(start_time_recent, end_time_global, current_config['min_magnitude_recent'], min_lat, max_lat, min_lon, max_lon) map_center_lat = (min_lat + max_lat) / 2; map_center_lon = (min_lon + max_lon) / 2 if current_config['selected_region_name'] == "Global (Significant Quakes)": initial_zoom = 2 elif abs(max_lat - min_lat) > 30 or abs(max_lon - min_lon) > 30: initial_zoom = 3 else: initial_zoom = 4 if abs(max_lat - min_lat) > 15 or abs(max_lon - min_lon) > 15 else 5 m = folium.Map(location=[map_center_lat, map_center_lon], zoom_start=initial_zoom, tiles=None) TileLayer(tiles='https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', attr='OpenTopoMap', name='OpenTopoMap (Terrain)', show=True).add_to(m) TileLayer("OpenStreetMap", name="OpenStreetMap", show=False).add_to(m) if current_config['show_tectonic_plates']: plate_data = fetch_tectonic_plates_data(TECTONIC_PLATES_URL) if plate_data: folium.GeoJson(plate_data, name="Tectonic Plates", style_function=lambda x: {'color': '#E04A00', 'weight': 3, 'opacity': 0.9}, tooltip=folium.GeoJsonTooltip(fields=['Name'], aliases=['Plate Name:'], localize=True)).add_to(m) if not historical_df.empty: heat_data_list = [[row_h['latitude'], row_h['longitude'], row_h['magnitude']] if pd.notnull(row_h['magnitude']) else [row_h['latitude'], row_h['longitude']] for _, row_h in historical_df.iterrows() if pd.notnull(row_h['latitude']) and pd.notnull(row_h['longitude'])] if heat_data_list: HeatMap(heat_data_list, name="Historical Heatmap").add_to(m) if not recent_df.empty: mc = MarkerCluster(name="Recent Earthquakes").add_to(m) for _, row_r in recent_df.iterrows(): if pd.notnull(row_r['latitude']) and pd.notnull(row_r['longitude']) and pd.notnull(row_r['magnitude']): mag = f"{row_r['magnitude']:.1f}"; depth = f"{row_r['depth']:.1f} km" if pd.notnull(row_r['depth']) else "N/A"; color = get_marker_color_by_magnitude(row_r['magnitude']) popup = f"RECENT
M{mag} at {row_r['place']}
{row_r['time'].strftime('%Y-%m-%d %H:%M')}
Depth: {depth}
USGS" folium.CircleMarker(location=[row_r['latitude'], row_r['longitude']], radius=max(3, (row_r['magnitude'] * 1.8)), popup=folium.Popup(popup, max_width=300), color=color, fill=True, fill_color=color, fill_opacity=0.7, tooltip=f"M{mag}").add_to(mc) folium.LayerControl().add_to(m) Fullscreen().add_to(m) map_html = m._repr_html_() if not recent_df.empty: recent_alerts = recent_df.copy() recent_alerts['magnitude'] = pd.to_numeric(recent_alerts['magnitude'], errors='coerce').dropna() temp_sig_q = recent_alerts[recent_alerts['magnitude'] >= current_config['alert_threshold_magnitude']] for _, sq_row in temp_sig_q.iterrows(): q_info = sq_row.to_dict(); q_info['tsunami_risk_info'] = "" if pd.notnull(sq_row.get('magnitude')) and pd.notnull(sq_row.get('depth')) and pd.notnull(sq_row.get('place')): if sq_row['magnitude'] >= TSUNAMI_MAG_THRESHOLD and sq_row['depth'] <= TSUNAMI_DEPTH_THRESHOLD and is_offshore(sq_row['place'], sq_row['depth']): q_info['tsunami_risk_info'] = "🌊 POTENTIAL TSUNAMI RISK"; tsunami_potential_events.append(q_info) significant_quakes.append(q_info) if current_config['enable_ai_interpretation'] and GEMINI_API_KEY: summary_ai = f"Region: {current_config['selected_region_name']}\nHist: {current_config['days_historical']}d, M>={current_config['min_magnitude_historical']}, Tot:{len(historical_df)}\n" if not historical_df.empty and 'magnitude' in historical_df and not historical_df['magnitude'].dropna().empty: summary_ai += f"Lgst hist.M: {historical_df['magnitude'].dropna().max():.1f}\n" summary_ai += f"Recent: {current_config['days_recent']}d, M>={current_config['min_magnitude_recent']}, Tot:{len(recent_df)}\n" if not recent_df.empty and 'magnitude' in recent_df and not recent_df['magnitude'].dropna().empty: summary_ai += f"Lgst rec.M: {recent_df['magnitude'].dropna().max():.1f}, Avg rec.M: {recent_df['magnitude'].dropna().mean():.1f}\n" summary_ai += f"Alerts (M>={current_config['alert_threshold_magnitude']}): {len(significant_quakes)}\n" if significant_quakes: summary_ai += "Top sig. quakes:\n" for r_s in significant_quakes[:2]: summary_ai += f" - M {r_s.get('magnitude',0.0):.1f} at {r_s.get('place','N/A')} on {r_s.get('time').strftime('%Y-%m-%d')}\n" summary_ai += f"Tsunami risk events: {len(tsunami_potential_events)}\n" full_prompt_ai = f"""You are a seismic data analyst. Based on this summary for '{current_config['selected_region_name']}', give a concise interpretation (max 3-4 paragraphs): 1. Overall seismic activity (recent vs. hist.). 2. Notable patterns/clusters in recent data. 3. Areas more active recently. 4. General outlook (NO PREDICTIONS). IMPORTANT: Start with a disclaimer. Data: {summary_ai}Interpretation:""" ai_interpretation = get_gemini_interpretation(GEMINI_API_KEY, full_prompt_ai) current_time_for_render = datetime.now(timezone.utc) return render_template('index.html', map_html=map_html, historical_df=historical_df, # For potential future use in template recent_df=recent_df, # For Top 5 table significant_quakes=significant_quakes, tsunami_potential_events=tsunami_potential_events, ai_interpretation=ai_interpretation, predefined_regions_list=predefined_regions_dict.keys(), now= current_time_for_render, current_config=current_config, initial_load=initial_load) if __name__ == '__main__': app.run(debug=True)