Spaces:
Sleeping
Sleeping
Upload 9 files
Browse files- app.py +678 -0
- cloudburst_classifier.pkl +3 -0
- cloudburst_regressor.pkl +3 -0
- finalclouddata.csv +0 -0
- floodfloodlfodd.csv +0 -0
- scaler.pkl +3 -0
- synthetic_cloudburst_data.csv +0 -0
- templates/index.html +397 -0
- templates/index2.html +219 -0
app.py
ADDED
@@ -0,0 +1,678 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, request, render_template
|
2 |
+
import requests
|
3 |
+
from datetime import datetime, date, timedelta
|
4 |
+
import joblib
|
5 |
+
import numpy as np
|
6 |
+
import shap
|
7 |
+
import google.generativeai as genai
|
8 |
+
import json
|
9 |
+
import logging
|
10 |
+
import os
|
11 |
+
import pandas as pd
|
12 |
+
|
13 |
+
# Attempt to import dice_ml and set a flag
|
14 |
+
try:
|
15 |
+
import dice_ml
|
16 |
+
|
17 |
+
dice_ml_available = True
|
18 |
+
logging.info("dice_ml library found and imported successfully.")
|
19 |
+
except ImportError as e_import: # Catch the specific ImportError
|
20 |
+
dice_ml_available = False
|
21 |
+
# Log the actual import error, which can be very helpful for debugging
|
22 |
+
logging.warning(f"IMPORTANT: dice_ml library FAILED TO IMPORT: {e_import}. "
|
23 |
+
f"DICE explanations will be unavailable. Ensure 'dice-ml' is installed in your Python environment (e.g., 'pip install dice-ml').")
|
24 |
+
|
25 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
26 |
+
|
27 |
+
app = Flask(__name__)
|
28 |
+
|
29 |
+
# IMPORTANT: Set this environment variable or replace the placeholder
|
30 |
+
GEMINI_API_KEY = "AIzaSyDkiYr-eSkqIXpZ1fHlik_YFsFtfQoFi0w"
|
31 |
+
if GEMINI_API_KEY == "YOUR_GEMINI_API_KEY_HERE":
|
32 |
+
logging.warning(
|
33 |
+
"Using a placeholder Gemini API Key. AI Analysis will likely fail. Please set the GEMINI_API_KEY environment variable or update app.py.")
|
34 |
+
|
35 |
+
NOMINATIM_USER_AGENT = 'CloudburstPredictorApp/1.0 ([email protected])' # Update with your contact
|
36 |
+
|
37 |
+
# Model and explainer globals
|
38 |
+
reg = None # Regressor (for probability score)
|
39 |
+
scaler = None
|
40 |
+
explainer = None # SHAP explainer
|
41 |
+
scaled_background_data = None
|
42 |
+
dice_explainer = None # DICE explainer
|
43 |
+
dice_data_object = None # DICE Data object
|
44 |
+
|
45 |
+
feature_names = [
|
46 |
+
'Min Temp (°C)', 'Max Temp (°C)', 'Humidity (2m %)', 'Pressure (hPa)',
|
47 |
+
'Precipitation (mm)', 'Rain (mm)', 'Precipitation Probability (%)',
|
48 |
+
'Cloud Cover (%)', 'Wind Speed (km/h)', 'Wind Gust (km/h)',
|
49 |
+
'Wind Direction (Encoded)', 'Is Day (Encoded)', 'Temp (2m °C)',
|
50 |
+
'Weather Description (Encoded)'
|
51 |
+
]
|
52 |
+
|
53 |
+
weather_code_mapping = {
|
54 |
+
0: {"desc": "Clear sky", "icon": "fa-sun"}, 1: {"desc": "Mainly clear", "icon": "fa-cloud-sun"},
|
55 |
+
2: {"desc": "Partly cloudy", "icon": "fa-cloud"}, 3: {"desc": "Overcast", "icon": "fa-smog"},
|
56 |
+
45: {"desc": "Fog", "icon": "fa-smog"}, 48: {"desc": "Depositing rime fog", "icon": "fa-smog"},
|
57 |
+
51: {"desc": "Light drizzle", "icon": "fa-cloud-rain"}, 53: {"desc": "Moderate drizzle", "icon": "fa-cloud-rain"},
|
58 |
+
55: {"desc": "Dense drizzle", "icon": "fa-cloud-showers-heavy"},
|
59 |
+
56: {"desc": "Light freezing drizzle", "icon": "fa-snowflake"},
|
60 |
+
57: {"desc": "Dense freezing drizzle", "icon": "fa-snowflake"},
|
61 |
+
61: {"desc": "Slight rain", "icon": "fa-cloud-rain"},
|
62 |
+
63: {"desc": "Moderate rain", "icon": "fa-cloud-showers-heavy"},
|
63 |
+
65: {"desc": "Heavy rain", "icon": "fa-cloud-pour"},
|
64 |
+
66: {"desc": "Light freezing rain", "icon": "fa-cloud-meatball"},
|
65 |
+
67: {"desc": "Heavy freezing rain", "icon": "fa-cloud-meatball"},
|
66 |
+
71: {"desc": "Slight snow fall", "icon": "fa-snowflake"},
|
67 |
+
73: {"desc": "Moderate snow fall", "icon": "fa-snowflake"},
|
68 |
+
75: {"desc": "Heavy snow fall", "icon": "fa-snowflake"}, 77: {"desc": "Snow grains", "icon": "fa-snowflake"},
|
69 |
+
80: {"desc": "Slight rain showers", "icon": "fa-cloud-sun-rain"},
|
70 |
+
81: {"desc": "Moderate rain showers", "icon": "fa-cloud-showers-heavy"},
|
71 |
+
82: {"desc": "Violent rain showers", "icon": "fa-cloud-pour"},
|
72 |
+
85: {"desc": "Slight snow showers", "icon": "fa-cloud-meatball"},
|
73 |
+
86: {"desc": "Heavy snow showers", "icon": "fa-cloud-meatball"},
|
74 |
+
95: {"desc": "Thunderstorm", "icon": "fa-bolt-lightning"},
|
75 |
+
96: {"desc": "Thunderstorm with slight hail", "icon": "fa-cloud-bolt"},
|
76 |
+
99: {"desc": "Thunderstorm with heavy hail", "icon": "fa-cloud-bolt"}
|
77 |
+
}
|
78 |
+
|
79 |
+
|
80 |
+
def get_weather_detail(code, detail_type="desc", default_desc="Unknown", default_icon="fa-question-circle"):
|
81 |
+
mapping = weather_code_mapping.get(code)
|
82 |
+
if mapping: return mapping.get(detail_type, default_desc if detail_type == "desc" else default_icon)
|
83 |
+
return default_desc if detail_type == "desc" else default_icon
|
84 |
+
|
85 |
+
|
86 |
+
def load_models():
|
87 |
+
global reg, scaler, explainer, scaled_background_data, dice_explainer, dice_data_object, dice_ml_available, feature_names
|
88 |
+
try:
|
89 |
+
logging.info("Attempting to load models and scaler...")
|
90 |
+
reg_path, scaler_path, background_data_path = 'cloudburst_regressor.pkl', 'scaler.pkl', 'scaled_background_data_sample.npy'
|
91 |
+
|
92 |
+
if os.path.exists(reg_path):
|
93 |
+
reg = joblib.load(reg_path); logging.info("Regressor loaded.")
|
94 |
+
else:
|
95 |
+
logging.warning(f"Regressor model not found at {reg_path}. Regression disabled."); reg = None
|
96 |
+
if os.path.exists(scaler_path):
|
97 |
+
scaler = joblib.load(scaler_path); logging.info("Scaler loaded.")
|
98 |
+
else:
|
99 |
+
logging.warning(f"Scaler not found at {scaler_path}. Predictions disabled."); scaler = None
|
100 |
+
if os.path.exists(background_data_path):
|
101 |
+
scaled_background_data = np.load(background_data_path, allow_pickle=True)
|
102 |
+
logging.info(f"Background data for explainers loaded. Shape: {scaled_background_data.shape}")
|
103 |
+
if scaled_background_data.ndim == 1 and scaler and hasattr(scaler, 'n_features_in_') and \
|
104 |
+
scaled_background_data.shape[0] == scaler.n_features_in_:
|
105 |
+
scaled_background_data = scaled_background_data.reshape(1, -1);
|
106 |
+
logging.info(f"Reshaped 1D background data to: {scaled_background_data.shape}")
|
107 |
+
else:
|
108 |
+
logging.warning(f"Background data not found at {background_data_path}. Explainers may be affected.");
|
109 |
+
scaled_background_data = None
|
110 |
+
|
111 |
+
model_ready = reg and scaler and hasattr(reg, 'n_features_in_') and hasattr(scaler, 'n_features_in_') and \
|
112 |
+
reg.n_features_in_ == scaler.n_features_in_ == len(feature_names)
|
113 |
+
|
114 |
+
if model_ready:
|
115 |
+
# SHAP Explainer
|
116 |
+
is_tree_model = any(hasattr(reg, attr) for attr in ['tree_', 'booster_', 'estimators_']) or \
|
117 |
+
reg.__class__.__name__ in ['RandomForestRegressor', 'GradientBoostingRegressor',
|
118 |
+
'XGBRegressor', 'LGBMRegressor']
|
119 |
+
if is_tree_model:
|
120 |
+
explainer = shap.TreeExplainer(reg); logging.info("SHAP TreeExplainer initialized.")
|
121 |
+
elif scaled_background_data is not None and scaled_background_data.shape[1] == scaler.n_features_in_:
|
122 |
+
summary_data = shap.kmeans(scaled_background_data, min(10, scaled_background_data.shape[0])) if \
|
123 |
+
scaled_background_data.shape[0] > 10 else scaled_background_data
|
124 |
+
explainer = shap.KernelExplainer(reg.predict, summary_data);
|
125 |
+
logging.info(f"SHAP KernelExplainer initialized with background summary shape {summary_data.shape}.")
|
126 |
+
else:
|
127 |
+
logging.warning(
|
128 |
+
"SHAP explainer could not be initialized (non-tree model and no suitable background data)."); explainer = None
|
129 |
+
|
130 |
+
# DICE Explainer
|
131 |
+
if dice_ml_available and scaled_background_data is not None and scaled_background_data.shape[1] == len(
|
132 |
+
feature_names):
|
133 |
+
try:
|
134 |
+
logging.info("Attempting to initialize DICE explainer...")
|
135 |
+
df_background_for_dice = pd.DataFrame(scaled_background_data, columns=feature_names)
|
136 |
+
df_dice_data_constructor = df_background_for_dice.copy()
|
137 |
+
df_dice_data_constructor['Cloudburst_Probability'] = reg.predict(scaled_background_data)
|
138 |
+
|
139 |
+
dice_data_object = dice_ml.Data(dataframe=df_dice_data_constructor,
|
140 |
+
continuous_features=feature_names,
|
141 |
+
outcome_name='Cloudburst_Probability')
|
142 |
+
dice_model_wrapper = dice_ml.Model(model=reg, backend='sklearn', model_type='regressor')
|
143 |
+
dice_explainer = dice_ml.Dice(dice_data_object, dice_model_wrapper, method="random")
|
144 |
+
logging.info("DICE explainer initialized successfully.")
|
145 |
+
except Exception as e_dice_init:
|
146 |
+
logging.error(f"Error initializing DICE explainer: {e_dice_init}", exc_info=True)
|
147 |
+
dice_explainer = None # Keep dice_ml_available as True, but explainer object is None
|
148 |
+
elif not dice_ml_available:
|
149 |
+
logging.warning(
|
150 |
+
"DICE explainer not initialized: dice_ml library not available.") # Already logged at import
|
151 |
+
else:
|
152 |
+
logging.warning(
|
153 |
+
"DICE explainer not initialized: background data for DICE missing or mismatched."); dice_explainer = None
|
154 |
+
else:
|
155 |
+
logging.warning(
|
156 |
+
"SHAP and DICE explainers disabled due to missing models, scaler, or feature count mismatch.")
|
157 |
+
explainer = None;
|
158 |
+
dice_explainer = None
|
159 |
+
|
160 |
+
except Exception as e_load:
|
161 |
+
logging.error(f"Error during model loading: {e_load}", exc_info=True)
|
162 |
+
finally:
|
163 |
+
if not model_ready: logging.critical(
|
164 |
+
"CRITICAL: Model/Scaler/Feature_names integrity check failed. Predictions unreliable.")
|
165 |
+
|
166 |
+
|
167 |
+
load_models()
|
168 |
+
|
169 |
+
wind_direction_mapping = {"E": 0, "N": 1, "NE": 2, "NW": 3, "S": 4, "SE": 5, "SW": 6, "W": 7}
|
170 |
+
is_day_mapping = {1: 0, 0: 1} # API: 1=Day (model:0), 0=Night (model:1)
|
171 |
+
wind_direction_full_names = {"N": "North", "NE": "Northeast", "E": "East", "SE": "Southeast", "S": "South",
|
172 |
+
"SW": "Southwest", "W": "West", "NW": "Northwest"}
|
173 |
+
|
174 |
+
|
175 |
+
def map_weather_description_to_encoding(code):
|
176 |
+
if code is None: return 0
|
177 |
+
try:
|
178 |
+
code = int(code)
|
179 |
+
except (ValueError, TypeError):
|
180 |
+
return 0
|
181 |
+
weather_encoding_map = {0: 0, 1: 0, 2: 5, 3: 4, 45: 2, 48: 2, 51: 1, 53: 1, 55: 1, 56: 1, 57: 1, 61: 6, 63: 6,
|
182 |
+
65: 3, 66: 6, 67: 3, 71: 6, 73: 6, 75: 6, 77: 6, 80: 6, 81: 6, 82: 3, 85: 6, 86: 6, 95: 7,
|
183 |
+
96: 7, 99: 7}
|
184 |
+
return weather_encoding_map.get(code, 0)
|
185 |
+
|
186 |
+
|
187 |
+
def get_previous_week_data(lat, lon):
|
188 |
+
today = date.today();
|
189 |
+
start_date, end_date = today - timedelta(days=8), today - timedelta(days=1)
|
190 |
+
url = "https://api.open-meteo.com/v1/archive"
|
191 |
+
params = {"latitude": lat, "longitude": lon, "daily": 'precipitation_sum,rain_sum',
|
192 |
+
"hourly": 'temperature_2m,relativehumidity_2m,pressure_msl,cloudcover,windspeed_10m,windgusts_10m,precipitation_probability',
|
193 |
+
"timezone": "auto", "start_date": start_date.strftime("%Y-%m-%d"),
|
194 |
+
"end_date": end_date.strftime("%Y-%m-%d")}
|
195 |
+
hist_avgs = {"avg_precipitation_sum": 0.1, "avg_rain_sum": 0.1, "avg_relativehumidity_2m": 65.0,
|
196 |
+
"avg_pressure_msl": 1012.0,
|
197 |
+
"avg_cloudcover": 40.0, "avg_temp": 22.0, "avg_wind_speed": 8.0, "avg_wind_gust": 12.0,
|
198 |
+
"avg_precip_prob": 15.0}
|
199 |
+
try:
|
200 |
+
response = requests.get(url, params=params, timeout=15);
|
201 |
+
response.raise_for_status();
|
202 |
+
data_hist = response.json()
|
203 |
+
key_map = [(("daily", "precipitation_sum"), "avg_precipitation_sum"), (("daily", "rain_sum"), "avg_rain_sum"),
|
204 |
+
(("hourly", "temperature_2m"), "avg_temp"),
|
205 |
+
(("hourly", "relativehumidity_2m"), "avg_relativehumidity_2m"),
|
206 |
+
(("hourly", "pressure_msl"), "avg_pressure_msl"), (("hourly", "cloudcover"), "avg_cloudcover"),
|
207 |
+
(("hourly", "windspeed_10m"), "avg_wind_speed"), (("hourly", "windgusts_10m"), "avg_wind_gust"),
|
208 |
+
(("hourly", "precipitation_probability"), "avg_precip_prob")]
|
209 |
+
for (data_type, param_name), avg_key in key_map:
|
210 |
+
values = [x for x in data_hist.get(data_type, {}).get(param_name, []) if
|
211 |
+
x is not None and not (isinstance(x, float) and np.isnan(x))]
|
212 |
+
if values: hist_avgs[avg_key] = float(np.mean(values))
|
213 |
+
except Exception as e:
|
214 |
+
logging.error(f"Error fetching/processing historical data: {e}. Using defaults.")
|
215 |
+
return hist_avgs
|
216 |
+
|
217 |
+
|
218 |
+
def degrees_to_cardinal(deg):
|
219 |
+
if deg is None: return "N/A"
|
220 |
+
try:
|
221 |
+
deg = float(deg)
|
222 |
+
except:
|
223 |
+
return "N/A"
|
224 |
+
return ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][int((deg + 22.5) / 45) % 8]
|
225 |
+
|
226 |
+
|
227 |
+
def reverse_geocode(lat, lon):
|
228 |
+
url, headers = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}", {
|
229 |
+
'User-Agent': NOMINATIM_USER_AGENT}
|
230 |
+
try:
|
231 |
+
r = requests.get(url, headers=headers, timeout=10);
|
232 |
+
r.raise_for_status();
|
233 |
+
data = r.json();
|
234 |
+
addr = data.get('address', {})
|
235 |
+
name = addr.get('city') or addr.get('town') or addr.get('village') or data.get('display_name')
|
236 |
+
country = addr.get('country')
|
237 |
+
return f"{name}, {country}" if name and country else data.get('display_name', f'Lat: {lat:.3f}, Lon: {lon:.3f}')
|
238 |
+
except Exception as e:
|
239 |
+
logging.error(f"Geocoding failed: {e}"); return f'Lat: {lat:.3f}, Lon: {lon:.3f} (No Address)'
|
240 |
+
|
241 |
+
|
242 |
+
def map_daily_to_model_features(daily_data_point, historical_avgs, expected_n_features):
|
243 |
+
try:
|
244 |
+
min_temp, max_temp = daily_data_point.get('temperature_2m_min'), daily_data_point.get('temperature_2m_max')
|
245 |
+
precip_sum, rain_sum_val = daily_data_point.get('precipitation_sum', 0.0), daily_data_point.get('rain_sum', 0.0)
|
246 |
+
precip_prob_max = daily_data_point.get('precipitation_probability_max', 0.0)
|
247 |
+
wind_speed_max, wind_gust_max = daily_data_point.get('windspeed_10m_max', 0.0), daily_data_point.get(
|
248 |
+
'windgusts_10m_max', 0.0)
|
249 |
+
wind_dir_deg, weathercode = daily_data_point.get('winddirection_10m_dominant'), daily_data_point.get(
|
250 |
+
'weathercode')
|
251 |
+
humidity, pressure, cloudcover = historical_avgs['avg_relativehumidity_2m'], historical_avgs[
|
252 |
+
'avg_pressure_msl'], historical_avgs['avg_cloudcover']
|
253 |
+
temp_avg_day = (min_temp + max_temp) / 2 if min_temp is not None and max_temp is not None else historical_avgs[
|
254 |
+
'avg_temp']
|
255 |
+
is_day_enc = is_day_mapping.get(1, 0)
|
256 |
+
|
257 |
+
features_raw = [min_temp, max_temp, humidity, pressure, precip_sum, rain_sum_val, precip_prob_max, cloudcover,
|
258 |
+
wind_speed_max, wind_gust_max, wind_direction_mapping.get(degrees_to_cardinal(wind_dir_deg), 1),
|
259 |
+
is_day_enc, temp_avg_day, map_weather_description_to_encoding(weathercode)]
|
260 |
+
|
261 |
+
default_map = {'Min Temp (°C)': historical_avgs['avg_temp'] - 5,
|
262 |
+
'Max Temp (°C)': historical_avgs['avg_temp'] + 5,
|
263 |
+
'Humidity (2m %)': historical_avgs['avg_relativehumidity_2m'],
|
264 |
+
'Pressure (hPa)': historical_avgs['avg_pressure_msl'],
|
265 |
+
'Precipitation (mm)': historical_avgs['avg_precipitation_sum'],
|
266 |
+
'Rain (mm)': historical_avgs['avg_rain_sum'],
|
267 |
+
'Precipitation Probability (%)': historical_avgs['avg_precip_prob'],
|
268 |
+
'Cloud Cover (%)': historical_avgs['avg_cloudcover'],
|
269 |
+
'Wind Speed (km/h)': historical_avgs['avg_wind_speed'],
|
270 |
+
'Wind Gust (km/h)': historical_avgs['avg_wind_gust'],
|
271 |
+
'Wind Direction (Encoded)': 1, 'Is Day (Encoded)': 0,
|
272 |
+
'Temp (2m °C)': historical_avgs['avg_temp'],
|
273 |
+
'Weather Description (Encoded)': 0}
|
274 |
+
|
275 |
+
proc_features = [float(val) if val is not None and not (isinstance(val, float) and np.isnan(val)) else float(
|
276 |
+
default_map[feature_names[i]]) for i, val in enumerate(features_raw)]
|
277 |
+
|
278 |
+
features_arr = np.array([proc_features])
|
279 |
+
if features_arr.shape[1] != expected_n_features: logging.error(
|
280 |
+
f"Daily mapped features count ({features_arr.shape[1]}) != expected ({expected_n_features})."); return None
|
281 |
+
return features_arr
|
282 |
+
except Exception as e:
|
283 |
+
logging.error(f"Error mapping daily to features: {e}", exc_info=True); return None
|
284 |
+
|
285 |
+
|
286 |
+
def get_gemini_analysis(current_weather, current_prediction, future_daily_predictions, shap_explanation, location,
|
287 |
+
dice_explanation=None):
|
288 |
+
if not GEMINI_API_KEY or "YOUR_GEMINI_API_KEY_HERE" in GEMINI_API_KEY: return {
|
289 |
+
"error": "AI analysis key not configured or is placeholder."}
|
290 |
+
try:
|
291 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
292 |
+
model = genai.GenerativeModel('gemini-1.5-flash',
|
293 |
+
generation_config={"temperature": 0.5, "top_p": 0.9, "max_output_tokens": 3500})
|
294 |
+
cw = {k: (v if v is not None else 'N/A') for k, v in (current_weather or {}).items()}
|
295 |
+
cp = {k: (v if v is not None else 'N/A') for k, v in (current_prediction or {}).items()}
|
296 |
+
future_preds_text = [
|
297 |
+
f"- **{p.get('date', 'N/A')}**: Risk {p.get('probability', 'N/A')}% ({p.get('status', 'N/A')}). Weather: {p.get('weather_description', 'N/A')}, Temp: {p.get('min_temp', 'N/A')} to {p.get('max_temp', 'N/A')}. Max Precip Prob: {p.get('precip_prob_max', 'N/A')}."
|
298 |
+
for p in future_daily_predictions or []]
|
299 |
+
future_summary = "\n".join(future_preds_text) if future_preds_text else "No detailed future forecast available."
|
300 |
+
|
301 |
+
base_val_text = "Base model prediction (average probability): Not available."
|
302 |
+
shap_text_parts = []
|
303 |
+
if shap_explanation and not any(
|
304 |
+
item.get('feature', '').lower().startswith(('shap error', 'prediction disabled', 'shap n/a')) for item
|
305 |
+
in shap_explanation):
|
306 |
+
for item in shap_explanation:
|
307 |
+
if item['feature'] == 'Base Value (Average Prediction)':
|
308 |
+
base_val_text = f"Base model prediction (average probability): {item['impact']:.1f}%"
|
309 |
+
elif isinstance(item['impact'], float) and abs(item['impact']) > 0.01:
|
310 |
+
shap_text_parts.append(f" - {item['feature']}: influence of {item['impact']:.1f}% on probability")
|
311 |
+
shap_summary = "Key factors influencing *current* prediction (SHAP values show % change from base):\n" + "\n".join(
|
312 |
+
shap_text_parts[:5]) if shap_text_parts else "SHAP analysis not available or not significant."
|
313 |
+
|
314 |
+
dice_summary_text = "Counterfactual analysis (what-if scenarios to lower risk): Not available or not run."
|
315 |
+
if dice_explanation and dice_explanation.get("counterfactuals"):
|
316 |
+
dice_parts = ["**Insights from Counterfactual Analysis (What could lower the risk?):**"]
|
317 |
+
for i, cf in enumerate(dice_explanation["counterfactuals"][:2]):
|
318 |
+
dice_parts.append(
|
319 |
+
f" *Scenario {i + 1} (to achieve ~{cf.get('achieved_probability', 'target')}% risk):*") # Added % to achieved_probability
|
320 |
+
for change in cf["changes"][:3]:
|
321 |
+
orig_val, cf_val = change.get('original_value_unscaled',
|
322 |
+
change.get('original_value_scaled')), change.get('cf_value_unscaled',
|
323 |
+
change.get(
|
324 |
+
'cf_value_scaled'))
|
325 |
+
dice_parts.append(f" - **{change['feature']}**: change from `{orig_val}` to `{cf_val}`")
|
326 |
+
dice_summary_text = "\n".join(dice_parts)
|
327 |
+
elif dice_explanation and (dice_explanation.get("message") or dice_explanation.get("error")):
|
328 |
+
dice_summary_text = f"Counterfactual analysis: {dice_explanation.get('message') or dice_explanation.get('error')}"
|
329 |
+
|
330 |
+
prompt = f"""
|
331 |
+
You are an expert meteorologist. Analyze the following cloudburst risk data for **{location}** and provide a comprehensive summary.
|
332 |
+
Use Markdown for all formatting (e.g., `## Heading 2`, `### Heading 3`, `* list item`, `**bold text**`, `_italic text_`).
|
333 |
+
|
334 |
+
**Current Weather Snapshot (as of {cw.get('Current Time', 'N/A')}):**
|
335 |
+
- Conditions: {cw.get('Weather Description (Current)', 'N/A')}
|
336 |
+
- Temperature: {cw.get('Temp (2m °C)', 'N/A')}°C (Today's Range: {cw.get('Min Temp (°C)', 'N/A')}°C - {cw.get('Max Temp (°C)', 'N/A')}°C)
|
337 |
+
- Humidity (Recent Avg): {cw.get('Humidity (Past Week Avg %)', 'N/A')}%
|
338 |
+
- Wind: {cw.get('Wind Speed (Current km/h)', 'N/A')} km/h from {cw.get('Wind Direction (Current)', 'N/A')}
|
339 |
+
- Today's Precipitation: {cw.get('Precipitation Today (Accumulated mm)', 'N/A')} mm
|
340 |
+
- Current Hour Precip. Chance: {cw.get('Precipitation Probability (Current Hour %)', 'N/A')}%
|
341 |
+
|
342 |
+
**Immediate Cloudburst Risk Assessment (Now):**
|
343 |
+
- Predicted Likelihood: **{cp.get('Predicted Cloudburst', 'N/A')}**
|
344 |
+
- Probability Score: **{cp.get('Predicted Cloudburst (%)', 'N/A')}%**
|
345 |
+
- {base_val_text}
|
346 |
+
{shap_summary}
|
347 |
+
|
348 |
+
{dice_summary_text}
|
349 |
+
|
350 |
+
**Cloudburst Risk Outlook (Next ~{len(future_daily_predictions) if future_daily_predictions else 0} Days):**
|
351 |
+
{future_summary}
|
352 |
+
|
353 |
+
---
|
354 |
+
**YOUR DETAILED ANALYSIS & ADVICE (Use Markdown formatting as specified above):**
|
355 |
+
|
356 |
+
## Executive Summary
|
357 |
+
_(A concise overview: current cloudburst risk level at {location}, the trend for upcoming days, and critical factors. Subtly weave in SHAP/DICE insights if available for the *immediate* forecast.)_
|
358 |
+
|
359 |
+
## Detailed Risk Breakdown
|
360 |
+
_(Elaborate on the current situation. For the future outlook, if any days show moderate or high risk (e.g., > 40-50% probability), create sub-sections like `### Tuesday: Elevated Risk` and explain the contributing factors for that day.)_
|
361 |
+
|
362 |
+
## Actionable Recommendations & Safety Tips
|
363 |
+
_(Provide 3-5 clear, practical bullet points based on the overall risk. E.g., preparations, travel advice, monitoring official alerts.)_
|
364 |
+
|
365 |
+
## Understanding the Forecast
|
366 |
+
_(Briefly explain that these are model-based predictions with inherent uncertainties and encourage users to stay updated with official meteorological sources.)_
|
367 |
+
|
368 |
+
**Important:** Maintain a factual, clear, and safety-conscious tone. Avoid sensationalism. Ensure all structured text (headings, lists) uses Markdown.
|
369 |
+
gie response in html for proper rendering on webpage.
|
370 |
+
"""
|
371 |
+
logging.info("Sending refined prompt to Gemini API...")
|
372 |
+
response = model.generate_content(prompt)
|
373 |
+
if response.prompt_feedback and response.prompt_feedback.block_reason:
|
374 |
+
reason = response.prompt_feedback.block_reason_message or response.prompt_feedback.block_reason.name
|
375 |
+
logging.warning(f"Gemini API call blocked. Reason: {reason}")
|
376 |
+
return {"error": f"AI analysis blocked by content policy ({reason})."}
|
377 |
+
analysis_text = "".join(part.text for part in response.candidates[0].content.parts) if response.candidates and \
|
378 |
+
response.candidates[
|
379 |
+
0].content else None
|
380 |
+
if analysis_text: return {"analysis": analysis_text}
|
381 |
+
logging.warning(f"Gemini API returned empty or unexpected response. Full response: {response}")
|
382 |
+
return {"error": "AI analysis response was empty or malformed."}
|
383 |
+
except Exception as e:
|
384 |
+
logging.error(f"Error calling Gemini API: {e}", exc_info=True);
|
385 |
+
err_msg = str(e).lower()
|
386 |
+
if any(s in err_msg for s in ["api_key_invalid", "permission_denied", "authentication"]): return {
|
387 |
+
"error": "AI analysis failed: Invalid API Key or auth issue."}
|
388 |
+
if "quota" in err_msg: return {"error": "AI analysis failed: API quota exceeded."}
|
389 |
+
if "rate limit" in err_msg: return {"error": "AI analysis failed: Rate limit. Try again later."}
|
390 |
+
if "Deadline" in str(e) or "timeout" in err_msg: return {"error": "AI analysis failed: Request timed out."}
|
391 |
+
return {"error": f"Failed to get AI analysis: Unexpected error ({type(e).__name__})."}
|
392 |
+
|
393 |
+
|
394 |
+
@app.route('/', methods=['GET'])
|
395 |
+
def index():
|
396 |
+
return render_template('index.html', show_results=False, current_year=datetime.now().year,
|
397 |
+
lat_initial=20.5937, lon_initial=78.9629,
|
398 |
+
weather_code_mapping_json=json.dumps(weather_code_mapping))
|
399 |
+
|
400 |
+
|
401 |
+
@app.route('/forecast', methods=['GET'])
|
402 |
+
def forecast():
|
403 |
+
lat_str, lon_str = request.args.get('lat'), request.args.get('lon')
|
404 |
+
render_args = {'show_results': True, 'current_year': datetime.now().year, 'lat_initial': lat_str,
|
405 |
+
'lon_initial': lon_str,
|
406 |
+
'current_weather': None,
|
407 |
+
'current_prediction': {"Predicted Cloudburst": "Error", "Predicted Cloudburst (%)": "Error"},
|
408 |
+
'future_predictions': [],
|
409 |
+
'shap_explanation': [{"feature": "SHAP N/A", "impact": "Not run or error."}],
|
410 |
+
'dice_explanation': {"error": "DICE N/A"}, 'gemini_analysis': {"error": "Analysis pending."},
|
411 |
+
'future_prob_chart_data_json': "{}", 'shap_chart_data_json': "{}", 'prediction_error': None,
|
412 |
+
'error': None,
|
413 |
+
'weather_code_mapping_json': json.dumps(weather_code_mapping)}
|
414 |
+
|
415 |
+
if not lat_str or not lon_str: render_args.update(
|
416 |
+
{'error': "Latitude and longitude are required.", 'show_results': False}); return render_template('index.html',
|
417 |
+
**render_args)
|
418 |
+
try:
|
419 |
+
lat, lon = float(lat_str), float(
|
420 |
+
lon_str); assert -90 <= lat <= 90 and -180 <= lon <= 180, "Coords out of range."
|
421 |
+
except (ValueError, AssertionError) as e:
|
422 |
+
render_args.update({'error': f"Invalid coordinates: {e}", 'show_results': False}); return render_template(
|
423 |
+
'index.html', **render_args)
|
424 |
+
|
425 |
+
if not (scaler and reg and hasattr(scaler, 'n_features_in_') and hasattr(reg,
|
426 |
+
'n_features_in_') and scaler.n_features_in_ == reg.n_features_in_ == len(
|
427 |
+
feature_names)):
|
428 |
+
err_msg = "Core prediction models/config missing/mismatched. Cannot forecast.";
|
429 |
+
logging.critical(err_msg)
|
430 |
+
render_args.update({'prediction_error': err_msg, 'gemini_analysis': {"error": err_msg}});
|
431 |
+
return render_template('index.html', **render_args)
|
432 |
+
|
433 |
+
weather_api_url = "https://api.open-meteo.com/v1/forecast"
|
434 |
+
api_params_curr = {'latitude': lat, 'longitude': lon, 'current_weather': True, 'timezone': 'auto',
|
435 |
+
'forecast_days': 1,
|
436 |
+
'hourly': 'temperature_2m,relativehumidity_2m,pressure_msl,precipitation,rain,cloudcover,windspeed_10m,windgusts_10m,winddirection_10m,is_day,weathercode,precipitation_probability',
|
437 |
+
'daily': 'temperature_2m_max,temperature_2m_min,precipitation_sum,rain_sum,precipitation_probability_max,weathercode,windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant'}
|
438 |
+
try:
|
439 |
+
r_curr = requests.get(weather_api_url, params=api_params_curr,
|
440 |
+
timeout=15); r_curr.raise_for_status(); data_curr_api = r_curr.json()
|
441 |
+
except requests.exceptions.RequestException as e:
|
442 |
+
render_args['prediction_error'] = f"Weather API error (current): {e}"; return render_template('index.html',
|
443 |
+
**render_args)
|
444 |
+
|
445 |
+
api_cw, api_hrly, api_dly_today = data_curr_api.get("current_weather", {}), data_curr_api.get("hourly",
|
446 |
+
{}), data_curr_api.get(
|
447 |
+
"daily", {})
|
448 |
+
if not (api_cw and api_hrly.get("time") and api_dly_today.get("time")): render_args[
|
449 |
+
'prediction_error'] = "Incomplete current weather data from API."; return render_template('index.html',
|
450 |
+
**render_args)
|
451 |
+
|
452 |
+
curr_time_api = api_cw.get("time");
|
453 |
+
curr_idx = api_hrly["time"].index(curr_time_api) if curr_time_api and curr_time_api in api_hrly["time"] else 0
|
454 |
+
|
455 |
+
def get_val(src, key, idx, default=None):
|
456 |
+
vals = src.get(key); return vals[idx] if vals and idx < len(vals) and vals[idx] is not None else default
|
457 |
+
|
458 |
+
hist_avgs = get_previous_week_data(lat, lon)
|
459 |
+
min_T, max_T = get_val(api_dly_today, "temperature_2m_min", 0, hist_avgs['avg_temp'] - 5), get_val(api_dly_today,
|
460 |
+
"temperature_2m_max",
|
461 |
+
0, hist_avgs[
|
462 |
+
'avg_temp'] + 5)
|
463 |
+
curr_T = get_val(api_hrly, "temperature_2m", curr_idx, hist_avgs['avg_temp'])
|
464 |
+
is_day_api = get_val(api_hrly, "is_day", curr_idx, 1)
|
465 |
+
|
466 |
+
curr_feat_vals = [min_T, max_T,
|
467 |
+
get_val(api_hrly, "relativehumidity_2m", curr_idx, hist_avgs['avg_relativehumidity_2m']),
|
468 |
+
get_val(api_hrly, "pressure_msl", curr_idx, hist_avgs['avg_pressure_msl']),
|
469 |
+
get_val(api_dly_today, "precipitation_sum", 0, hist_avgs['avg_precipitation_sum']),
|
470 |
+
get_val(api_dly_today, "rain_sum", 0, hist_avgs['avg_rain_sum']),
|
471 |
+
get_val(api_dly_today, "precipitation_probability_max", 0,
|
472 |
+
get_val(api_hrly, "precipitation_probability", curr_idx, hist_avgs['avg_precip_prob'])),
|
473 |
+
get_val(api_hrly, "cloudcover", curr_idx, hist_avgs['avg_cloudcover']),
|
474 |
+
get_val(api_dly_today, "windspeed_10m_max", 0,
|
475 |
+
get_val(api_hrly, "windspeed_10m", curr_idx, hist_avgs['avg_wind_speed'])),
|
476 |
+
get_val(api_dly_today, "windgusts_10m_max", 0,
|
477 |
+
get_val(api_hrly, "windgusts_10m", curr_idx, hist_avgs['avg_wind_gust'])),
|
478 |
+
wind_direction_mapping.get(degrees_to_cardinal(
|
479 |
+
get_val(api_dly_today, "winddirection_10m_dominant", 0,
|
480 |
+
get_val(api_hrly, "winddirection_10m", curr_idx, 0))), 1),
|
481 |
+
is_day_mapping.get(is_day_api, 0), curr_T,
|
482 |
+
map_weather_description_to_encoding(
|
483 |
+
get_val(api_dly_today, "weathercode", 0, get_val(api_hrly, "weathercode", curr_idx, 0)))]
|
484 |
+
curr_feat_proc = [float(x) if x is not None and not (isinstance(x, float) and np.isnan(x)) else 0.0 for x in
|
485 |
+
curr_feat_vals]
|
486 |
+
curr_feat_np = np.array([curr_feat_proc])
|
487 |
+
|
488 |
+
# Current Prediction & Explainability
|
489 |
+
if curr_feat_np.shape[1] == scaler.n_features_in_:
|
490 |
+
try:
|
491 |
+
curr_feat_scaled = scaler.transform(curr_feat_np)
|
492 |
+
prob_raw = reg.predict(curr_feat_scaled)[0]
|
493 |
+
prob_clmp = max(0, min(100, int(round(prob_raw))))
|
494 |
+
render_args['current_prediction'] = {"Predicted Cloudburst (%)": prob_clmp,
|
495 |
+
"Predicted Cloudburst": "Yes" if prob_clmp > 50 else "No"}
|
496 |
+
|
497 |
+
# SHAP Explanations
|
498 |
+
if explainer:
|
499 |
+
try:
|
500 |
+
shap_vals_raw = explainer.shap_values(curr_feat_scaled);
|
501 |
+
shap_vals = np.asarray(
|
502 |
+
shap_vals_raw[0] if isinstance(shap_vals_raw, list) else shap_vals_raw).squeeze()
|
503 |
+
if shap_vals.ndim > 1 and shap_vals.shape[0] == 1: shap_vals = shap_vals[0]
|
504 |
+
if len(shap_vals) == len(feature_names):
|
505 |
+
shap_pairs = sorted(zip(feature_names, shap_vals * 100), key=lambda x: abs(x[1]), reverse=True)
|
506 |
+
render_args['shap_explanation'] = [{"feature": name, "impact": float(val)} for name, val in
|
507 |
+
shap_pairs]
|
508 |
+
if hasattr(explainer, 'expected_value'):
|
509 |
+
base_val = explainer.expected_value;
|
510 |
+
base_val = base_val.mean() if isinstance(base_val, np.ndarray) else base_val
|
511 |
+
render_args['shap_explanation'].insert(0, {"feature": "Base Value (Average Prediction)",
|
512 |
+
"impact": float(base_val * 100)})
|
513 |
+
else:
|
514 |
+
render_args['shap_explanation'] = [
|
515 |
+
{"feature": "SHAP Error", "impact": "SHAP values length mismatch."}]
|
516 |
+
except Exception as e_s:
|
517 |
+
logging.error(f"SHAP error: {e_s}", exc_info=True); render_args['shap_explanation'] = [
|
518 |
+
{"feature": "SHAP Error", "impact": str(e_s)}]
|
519 |
+
else:
|
520 |
+
render_args['shap_explanation'] = [{"feature": "SHAP Disabled", "impact": "Explainer not initialized."}]
|
521 |
+
|
522 |
+
# DICE Counterfactuals
|
523 |
+
if dice_explainer and dice_ml_available: # Check both the explainer object and the import flag
|
524 |
+
try:
|
525 |
+
query_instance_df = pd.DataFrame(curr_feat_scaled, columns=feature_names)
|
526 |
+
desired_prob_range = [0, max(0, prob_clmp - 30)]
|
527 |
+
|
528 |
+
if prob_clmp < 30:
|
529 |
+
render_args['dice_explanation'] = {
|
530 |
+
"message": "Current risk is already low. Counterfactuals for further reduction may not be very distinct or meaningful."}
|
531 |
+
else:
|
532 |
+
cfs_object = dice_explainer.generate_counterfactuals(
|
533 |
+
query_instance_df, total_CFs=3, desired_range=desired_prob_range,
|
534 |
+
features_to_vary='all'
|
535 |
+
)
|
536 |
+
if cfs_object and cfs_object.cf_examples_list:
|
537 |
+
processed_cfs = []
|
538 |
+
original_unscaled_features = pd.Series(curr_feat_np[0], index=feature_names)
|
539 |
+
|
540 |
+
for cf_example in cfs_object.cf_examples_list:
|
541 |
+
cf_df_final = cf_example.final_cfs_df
|
542 |
+
if cf_df_final is not None and not cf_df_final.empty:
|
543 |
+
for _, cf_row_scaled_series in cf_df_final.iterrows():
|
544 |
+
achieved_prob = cf_row_scaled_series['Cloudburst_Probability']
|
545 |
+
cf_scaled_values = cf_row_scaled_series.drop(
|
546 |
+
'Cloudburst_Probability').values.reshape(1, -1)
|
547 |
+
cf_unscaled_values = scaler.inverse_transform(cf_scaled_values)[0]
|
548 |
+
cf_unscaled_series = pd.Series(cf_unscaled_values, index=feature_names)
|
549 |
+
|
550 |
+
changes_list = []
|
551 |
+
for feat_name in feature_names:
|
552 |
+
original_val_display = f"{original_unscaled_features[feat_name]:.2f}"
|
553 |
+
cf_val_display = f"{cf_unscaled_series[feat_name]:.2f}"
|
554 |
+
if not np.isclose(original_unscaled_features[feat_name],
|
555 |
+
cf_unscaled_series[feat_name], atol=1e-2):
|
556 |
+
changes_list.append({
|
557 |
+
"feature": feat_name,
|
558 |
+
"original_value_unscaled": original_val_display,
|
559 |
+
"cf_value_unscaled": cf_val_display
|
560 |
+
})
|
561 |
+
if changes_list:
|
562 |
+
processed_cfs.append({
|
563 |
+
"target_probability_range": f"{desired_prob_range[0]}-{desired_prob_range[1]}%",
|
564 |
+
"achieved_probability": f"{achieved_prob:.1f}",
|
565 |
+
"changes": changes_list
|
566 |
+
})
|
567 |
+
render_args['dice_explanation'] = {"counterfactuals": processed_cfs} if processed_cfs else {
|
568 |
+
"message": "No distinct counterfactuals found to significantly lower the risk."}
|
569 |
+
else:
|
570 |
+
render_args['dice_explanation'] = {"message": "No counterfactuals generated by DiCE."}
|
571 |
+
except Exception as e_d:
|
572 |
+
logging.error(f"DICE error during generation: {e_d}", exc_info=True)
|
573 |
+
render_args['dice_explanation'] = {"error": f"DICE generation failed: {str(e_d)}"}
|
574 |
+
elif not dice_ml_available: # Condition for "dice-ml library not loaded"
|
575 |
+
render_args['dice_explanation'] = {
|
576 |
+
"error": "The 'dice-ml' library (for counterfactuals) failed to import. "
|
577 |
+
"Please ensure it is installed correctly in your active Python environment. "
|
578 |
+
"You can typically install it using: pip install dice-ml. "
|
579 |
+
"Check the server console/logs for specific import error messages that occurred at startup."
|
580 |
+
}
|
581 |
+
else: # dice_ml_available is True, but dice_explainer object is None (init failed for other reasons)
|
582 |
+
render_args['dice_explanation'] = {
|
583 |
+
"error": "DICE explainer could not be initialized. This might be due to issues with "
|
584 |
+
"background data ('scaled_background_data_sample.npy'), model compatibility, "
|
585 |
+
"or other setup problems. Check server console/logs for detailed initialization errors that occurred at startup."
|
586 |
+
}
|
587 |
+
|
588 |
+
except Exception as e_p:
|
589 |
+
logging.error(f"Prediction engine error: {e_p}", exc_info=True)
|
590 |
+
render_args.update({'prediction_error': f"Prediction engine error: {e_p}",
|
591 |
+
'shap_explanation': [{"feature": "Prediction Error", "impact": str(e_p)}],
|
592 |
+
'dice_explanation': {"error": "Prediction failed, so DICE analysis was not run."}})
|
593 |
+
else:
|
594 |
+
render_args[
|
595 |
+
'prediction_error'] = "Feature mismatch for current prediction. Expected {} features, got {}.".format(
|
596 |
+
scaler.n_features_in_, curr_feat_np.shape[1])
|
597 |
+
|
598 |
+
render_args['current_weather'] = {
|
599 |
+
"Location Address": reverse_geocode(lat, lon),
|
600 |
+
"Current Time": datetime.fromisoformat(curr_time_api).strftime("%Y-%m-%d %H:%M %Z") if curr_time_api else "N/A",
|
601 |
+
"Min Temp (°C)": f"{min_T:.1f}" if min_T is not None else "N/A",
|
602 |
+
"Max Temp (°C)": f"{max_T:.1f}" if max_T is not None else "N/A",
|
603 |
+
"Temp (2m °C)": f"{curr_T:.1f}" if curr_T is not None else "N/A",
|
604 |
+
"Weather Code": get_val(api_hrly, "weathercode", curr_idx, 0),
|
605 |
+
"Weather Description (Current)": get_weather_detail(get_val(api_hrly, "weathercode", curr_idx, 0), "desc"),
|
606 |
+
"Humidity (Past Week Avg %)": f"{hist_avgs['avg_relativehumidity_2m']:.0f}",
|
607 |
+
"Wind Speed (Current km/h)": f"{get_val(api_hrly, 'windspeed_10m', curr_idx, 0):.1f}",
|
608 |
+
"Wind Direction (Current)": wind_direction_full_names.get(
|
609 |
+
degrees_to_cardinal(get_val(api_hrly, 'winddirection_10m', curr_idx)), "N/A"),
|
610 |
+
"Precipitation Today (Accumulated mm)": f"{get_val(api_dly_today, 'precipitation_sum', 0, 0.0):.1f}",
|
611 |
+
"Precipitation Probability (Current Hour %)": f"{get_val(api_hrly, 'precipitation_probability', curr_idx, 0):.0f}"}
|
612 |
+
|
613 |
+
# Future Predictions
|
614 |
+
api_params_fut = {'latitude': lat, 'longitude': lon, 'timezone': 'auto', 'forecast_days': 8,
|
615 |
+
'daily': 'weathercode,temperature_2m_max,temperature_2m_min,precipitation_sum,rain_sum,precipitation_probability_max,windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant'}
|
616 |
+
try:
|
617 |
+
r_fut = requests.get(weather_api_url, params=api_params_fut, timeout=15);
|
618 |
+
r_fut.raise_for_status();
|
619 |
+
data_fut_api_dly = r_fut.json().get("daily", {})
|
620 |
+
if data_fut_api_dly.get("time") and len(data_fut_api_dly["time"]) > 1:
|
621 |
+
for i in range(1, len(data_fut_api_dly["time"])): # Skip today
|
622 |
+
day_data = {key: get_val(data_fut_api_dly, key, i) for key in data_fut_api_dly.keys()}
|
623 |
+
day_feat_np = map_daily_to_model_features(day_data, hist_avgs, scaler.n_features_in_)
|
624 |
+
day_pred = {"date": day_data.get('time'),
|
625 |
+
"min_temp": f"{day_data.get('temperature_2m_min'):.1f}" if day_data.get(
|
626 |
+
'temperature_2m_min') is not None else "N/A",
|
627 |
+
"max_temp": f"{day_data.get('temperature_2m_max'):.1f}" if day_data.get(
|
628 |
+
'temperature_2m_max') is not None else "N/A",
|
629 |
+
"weather_code": day_data.get('weathercode'),
|
630 |
+
"weather_description": get_weather_detail(day_data.get('weathercode'), "desc"),
|
631 |
+
"precip_prob_max": f"{day_data.get('precipitation_probability_max'):.0f}%" if day_data.get(
|
632 |
+
'precipitation_probability_max') is not None else "N/A",
|
633 |
+
"probability": "N/A", "status": "Error"}
|
634 |
+
if day_feat_np is not None:
|
635 |
+
try:
|
636 |
+
day_prob_raw = reg.predict(scaler.transform(day_feat_np))[0]; day_prob_clmp = max(0, min(100,
|
637 |
+
int(round(
|
638 |
+
day_prob_raw))))
|
639 |
+
except Exception as e_fut_pred:
|
640 |
+
day_prob_clmp = "N/A"; day_pred["status"] = "Pred. Err"; logging.error(
|
641 |
+
f"Future day pred err: {e_fut_pred}")
|
642 |
+
day_pred.update({"probability": day_prob_clmp, "status": "Yes" if isinstance(day_prob_clmp,
|
643 |
+
int) and day_prob_clmp > 50 else "No" if isinstance(
|
644 |
+
day_prob_clmp, int) else day_pred["status"]})
|
645 |
+
else:
|
646 |
+
day_pred["status"] = "Data Err"
|
647 |
+
render_args['future_predictions'].append(day_pred)
|
648 |
+
except Exception as e_fut_api:
|
649 |
+
logging.error(f"Future forecast API/processing error: {e_fut_api}", exc_info=True)
|
650 |
+
|
651 |
+
# Gemini AI Analysis
|
652 |
+
render_args['gemini_analysis'] = get_gemini_analysis(render_args['current_weather'],
|
653 |
+
render_args['current_prediction'],
|
654 |
+
render_args['future_predictions'],
|
655 |
+
render_args['shap_explanation'],
|
656 |
+
render_args['current_weather'].get('Location Address',
|
657 |
+
f'Lat: {lat:.2f}, Lon: {lon:.2f}') if
|
658 |
+
render_args[
|
659 |
+
'current_weather'] else f'Lat: {lat:.2f}, Lon: {lon:.2f}',
|
660 |
+
render_args['dice_explanation'])
|
661 |
+
|
662 |
+
# Chart Data
|
663 |
+
plottable_future = [p for p in render_args['future_predictions'] if isinstance(p.get('probability'), int)]
|
664 |
+
if plottable_future: render_args['future_prob_chart_data_json'] = json.dumps(
|
665 |
+
{"labels": [p['date'] for p in plottable_future], "data": [p['probability'] for p in plottable_future]})
|
666 |
+
|
667 |
+
plottable_shap = [s for s in render_args['shap_explanation'] if
|
668 |
+
s.get('feature') != 'Base Value (Average Prediction)' and isinstance(s.get('impact'),
|
669 |
+
float) and not s.get(
|
670 |
+
'feature', '').lower().startswith(('shap', 'error', 'prediction', 'disabled', 'n/a'))]
|
671 |
+
if plottable_shap: render_args['shap_chart_data_json'] = json.dumps(
|
672 |
+
{"labels": [s['feature'] for s in plottable_shap], "data": [s['impact'] for s in plottable_shap]})
|
673 |
+
|
674 |
+
return render_template('index.html', **render_args)
|
675 |
+
|
676 |
+
|
677 |
+
if __name__ == '__main__':
|
678 |
+
app.run(debug=True, port=os.environ.get("PORT", 5000))
|
cloudburst_classifier.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:954c6209cf075faa720dc875c2d1011c6bd11b22ee53c9194d013bd1136f8857
|
3 |
+
size 1959545
|
cloudburst_regressor.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:e96a532ddedaf840692b96d9b138eb8aa21d48f8fa1dbfd0511fccb2ea0a3616
|
3 |
+
size 16435729
|
finalclouddata.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
floodfloodlfodd.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
scaler.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:1f08430c98dd31536548df99063137e63ded259525a4ecf1fb3e1b0c48d70cbf
|
3 |
+
size 1559
|
synthetic_cloudburst_data.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
templates/index.html
ADDED
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Cloudburst Predictor Pro</title>
|
7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
10 |
+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"/>
|
11 |
+
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
|
12 |
+
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" />
|
13 |
+
<script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
|
14 |
+
|
15 |
+
<style>
|
16 |
+
body {
|
17 |
+
font-family: 'Roboto', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
18 |
+
background: linear-gradient(135deg, #E0F7FA 0%, #B2EBF2 50%, #80DEEA 100%);
|
19 |
+
color: #374151; /* Tailwind gray-700 */
|
20 |
+
overflow-x: hidden;
|
21 |
+
}
|
22 |
+
.content-card {
|
23 |
+
background-color: rgba(255, 255, 255, 0.97);
|
24 |
+
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
|
25 |
+
border: 1px solid rgba(209, 213, 219, 0.4);
|
26 |
+
box-shadow: 0 12px 30px -8px rgba(0, 0, 0, 0.1), 0 8px 15px -8px rgba(0, 0, 0, 0.06);
|
27 |
+
}
|
28 |
+
.header-title { color: #0D47A1; }
|
29 |
+
.section-title { color: #0277BD; }
|
30 |
+
.button-primary {
|
31 |
+
background-color: #0288D1;
|
32 |
+
transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
33 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
34 |
+
}
|
35 |
+
.button-primary:hover {
|
36 |
+
background-color: #0277BD; transform: translateY(-2px);
|
37 |
+
box-shadow: 0 5px 10px rgba(0,0,0,0.15);
|
38 |
+
}
|
39 |
+
#map { height: 320px; border-radius: 0.5rem; border: 1px solid #CFD8DC; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
|
40 |
+
|
41 |
+
/* Gemini AI Analysis Content Styling - CRITICAL FOR MARKDOWN */
|
42 |
+
.gemini-analysis-content { line-height: 1.65; }
|
43 |
+
.gemini-analysis-content h1, .gemini-analysis-content h2, .gemini-analysis-content h3, .gemini-analysis-content h4, .gemini-analysis-content h5, .gemini-analysis-content h6 {
|
44 |
+
color: #0277BD; margin-top: 1.5em; margin-bottom: 0.75em; font-weight: 600;
|
45 |
+
}
|
46 |
+
.gemini-analysis-content h1 { font-size: 2em; border-bottom: 2px solid #80DEEA; padding-bottom: 0.3em;}
|
47 |
+
.gemini-analysis-content h2 { font-size: 1.6em; border-bottom: 1px solid #B2EBF2; padding-bottom: 0.2em;}
|
48 |
+
.gemini-analysis-content h3 { font-size: 1.3em; }
|
49 |
+
.gemini-analysis-content h4 { font-size: 1.15em; color: #0288D1; }
|
50 |
+
.gemini-analysis-content p { margin-bottom: 1em; }
|
51 |
+
.gemini-analysis-content ul, .gemini-analysis-content ol { margin-left: 1.8em; margin-bottom: 1em; list-style-position: outside; }
|
52 |
+
.gemini-analysis-content ul li { list-style-type: disc; }
|
53 |
+
.gemini-analysis-content ol li { list-style-type: decimal; }
|
54 |
+
.gemini-analysis-content li { margin-bottom: 0.5em; }
|
55 |
+
.gemini-analysis-content strong, .gemini-analysis-content b { color: #0D47A1; font-weight: 700; }
|
56 |
+
.gemini-analysis-content em, .gemini-analysis-content i { color: #0277BD; font-style: italic; }
|
57 |
+
.gemini-analysis-content code {
|
58 |
+
background-color: #E0F2F7; color: #01579B;
|
59 |
+
padding: 0.25em 0.5em; border-radius: 4px;
|
60 |
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
61 |
+
font-size: 0.9em; border: 1px solid #B2EBF2;
|
62 |
+
}
|
63 |
+
.gemini-analysis-content pre {
|
64 |
+
background-color: #E8F5E9; border: 1px solid #A5D6A7;
|
65 |
+
padding: 1em; margin-bottom: 1em; border-radius: 0.375rem;
|
66 |
+
overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;
|
67 |
+
}
|
68 |
+
.gemini-analysis-content pre code { background-color: transparent; border: none; padding: 0; }
|
69 |
+
.gemini-analysis-content blockquote {
|
70 |
+
border-left: 4px solid #80DEEA; margin-left: 0; padding-left: 1em;
|
71 |
+
color: #455A64; font-style: italic; margin-bottom: 1em;
|
72 |
+
}
|
73 |
+
.gemini-analysis-content hr {
|
74 |
+
border: none; border-top: 2px dashed #B0BEC5; margin-top: 2em; margin-bottom: 2em;
|
75 |
+
}
|
76 |
+
.gemini-analysis-content table { width: 100%; border-collapse: collapse; margin-bottom: 1em; }
|
77 |
+
.gemini-analysis-content th, .gemini-analysis-content td { border: 1px solid #B0BEC5; padding: 0.6em; text-align: left; }
|
78 |
+
.gemini-analysis-content th { background-color: #E1F5FE; color: #0277BD; }
|
79 |
+
|
80 |
+
|
81 |
+
.risk-yes { color: #D32F2F; } .risk-no { color: #388E3C; }
|
82 |
+
.status-error { color: #F57C00; }
|
83 |
+
|
84 |
+
.table-auto th { background-color: #E1F5FE; color: #0277BD; }
|
85 |
+
.table-auto td, .table-auto th { border: 1px solid #B0BEC5; }
|
86 |
+
|
87 |
+
.future-day-card { border: 1px solid #B0E0E6; transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; }
|
88 |
+
.future-day-card:hover { transform: translateY(-4px); box-shadow: 0 10px 20px rgba(0,0,0,0.12); }
|
89 |
+
.future-day-card .risk-high { background-color: #FFEBEE; border-left: 5px solid #D32F2F; }
|
90 |
+
.future-day-card .risk-moderate { background-color: #FFF9C4; border-left: 5px solid #FBC02D; }
|
91 |
+
.future-day-card .risk-low { background-color: #E8F5E9; border-left: 5px solid #388E3C; }
|
92 |
+
.future-day-card .risk-error { background-color: #ECEFF1; border-left: 5px solid #78909C; }
|
93 |
+
|
94 |
+
input[type="text"]:focus, input[type="number"]:focus { border-color: #0288D1; box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.25); }
|
95 |
+
.scrollable-content { max-height: 450px; overflow-y: auto; padding-right: 10px;}
|
96 |
+
.scrollable-content::-webkit-scrollbar { width: 8px; }
|
97 |
+
.scrollable-content::-webkit-scrollbar-track { background: #E0F7FA; border-radius: 4px;}
|
98 |
+
.scrollable-content::-webkit-scrollbar-thumb { background: #80DEEA; border-radius: 4px;}
|
99 |
+
.scrollable-content::-webkit-scrollbar-thumb:hover { background: #4DD0E1; }
|
100 |
+
</style>
|
101 |
+
</head>
|
102 |
+
<body class="text-gray-700">
|
103 |
+
<div class="container mx-auto p-4 md:p-6 lg:p-8 min-h-screen">
|
104 |
+
<header class="text-center mb-10">
|
105 |
+
<h1 class="text-4xl md:text-5xl font-bold header-title drop-shadow-lg">
|
106 |
+
<i class="fas fa-cloud-bolt mr-3"></i>Cloudburst Prediction System
|
107 |
+
</h1>
|
108 |
+
<p class="text-cyan-700 text-lg mt-2">Advanced Weather Insights & Risk Assessment</p>
|
109 |
+
</header>
|
110 |
+
|
111 |
+
<section id="input-section" class="mb-10 p-6 rounded-xl shadow-xl content-card">
|
112 |
+
<form action="{{ url_for('forecast') }}" method="GET" class="space-y-5">
|
113 |
+
<h2 class="text-3xl font-semibold text-center section-title mb-6">Location Input</h2>
|
114 |
+
<p class="text-center text-gray-600 text-sm mb-5">Pinpoint on map, search, or enter coordinates manually.</p>
|
115 |
+
<div id="map"></div>
|
116 |
+
<div class="grid md:grid-cols-2 gap-5">
|
117 |
+
<div>
|
118 |
+
<label for="lat" class="block text-sm font-medium text-gray-700 mb-1">Latitude:</label>
|
119 |
+
<input type="text" id="lat" name="lat" required class="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 transition" placeholder="e.g., 28.6139" value="{{ lat_initial if lat_initial else '' }}">
|
120 |
+
</div>
|
121 |
+
<div>
|
122 |
+
<label for="lon" class="block text-sm font-medium text-gray-700 mb-1">Longitude:</label>
|
123 |
+
<input type="text" id="lon" name="lon" required class="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 transition" placeholder="e.g., 77.2090" value="{{ lon_initial if lon_initial else '' }}">
|
124 |
+
</div>
|
125 |
+
</div>
|
126 |
+
<button type="submit" class="w-full button-primary text-white font-bold py-3.5 px-6 rounded-lg shadow-md text-lg">
|
127 |
+
<i class="fas fa-magnifying-glass-chart mr-2"></i>Analyze Cloudburst Risk
|
128 |
+
</button>
|
129 |
+
{% if error %}
|
130 |
+
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-md mt-4 text-sm" role="alert">
|
131 |
+
<p><i class="fas fa-exclamation-triangle mr-2"></i>{{ error }}</p>
|
132 |
+
</div>
|
133 |
+
{% endif %}
|
134 |
+
</form>
|
135 |
+
</section>
|
136 |
+
|
137 |
+
{% if show_results %}
|
138 |
+
<div id="results-section" class="space-y-10">
|
139 |
+
{% if prediction_error %}
|
140 |
+
<div class="p-6 rounded-xl shadow-xl content-card bg-red-50 border-l-4 border-red-600">
|
141 |
+
<h3 class="text-xl font-semibold text-red-700 mb-2"><i class="fas fa-shield-virus mr-2"></i>System Alert</h3>
|
142 |
+
<p class="text-red-600">{{ prediction_error }}</p>
|
143 |
+
</div>
|
144 |
+
{% endif %}
|
145 |
+
|
146 |
+
{% if current_weather and current_prediction and not prediction_error %}
|
147 |
+
<section class="p-6 rounded-xl shadow-xl content-card">
|
148 |
+
<h2 class="text-3xl font-semibold section-title mb-5 text-center"><i class="fas fa-clock mr-2"></i>Current Conditions & Immediate Risk</h2>
|
149 |
+
<div class="grid md:grid-cols-2 gap-6 items-start">
|
150 |
+
<div>
|
151 |
+
<h3 class="text-xl font-medium text-gray-800 mb-3"><i class="fas fa-map-marker-alt text-cyan-600 mr-2"></i>{{ current_weather['Location Address'] }}</h3>
|
152 |
+
<p class="text-sm text-gray-500 mb-3">{{ current_weather['Current Time'] }}</p>
|
153 |
+
<div class="bg-sky-50 p-4 rounded-lg space-y-2 text-sm">
|
154 |
+
<p class="flex items-center"><i class="fas {{ weather_code_mapping[current_weather['Weather Code']]['icon'] if current_weather['Weather Code'] in weather_code_mapping else 'fa-question-circle' }} text-xl text-cyan-600 w-6 mr-2"></i> <strong class="w-36">Weather:</strong> {{ current_weather['Weather Description (Current)'] }}</p>
|
155 |
+
<p><i class="fas fa-temperature-half w-6 mr-2 text-cyan-600"></i><strong class="w-36">Temperature:</strong> {{ current_weather['Temp (2m °C)'] }}°C (Min: {{ current_weather['Min Temp (°C)'] }}°C / Max: {{ current_weather['Max Temp (°C)'] }}°C)</p>
|
156 |
+
<p><i class="fas fa-wind w-6 mr-2 text-cyan-600"></i><strong class="w-36">Wind:</strong> {{ current_weather['Wind Speed (Current km/h)'] }} km/h from {{ current_weather['Wind Direction (Current)'] }}</p>
|
157 |
+
<p><i class="fas fa-droplet w-6 mr-2 text-cyan-600"></i><strong class="w-36">Humidity (Avg):</strong> {{ current_weather['Humidity (Past Week Avg %)'] }}%</p>
|
158 |
+
<p><i class="fas fa-cloud-showers-heavy w-6 mr-2 text-cyan-600"></i><strong class="w-36">Precip. Today:</strong> {{ current_weather['Precipitation Today (Accumulated mm)'] }} mm</p>
|
159 |
+
<p><i class="fas fa-umbrella w-6 mr-2 text-cyan-600"></i><strong class="w-36">Precip. Prob (Now):</strong> {{ current_weather['Precipitation Probability (Current Hour %)'] }}%</p>
|
160 |
+
</div>
|
161 |
+
</div>
|
162 |
+
<div class="text-center p-4 rounded-lg {% if current_prediction['Predicted Cloudburst'] == 'Yes' %}bg-red-50 border border-red-200{% elif current_prediction['Predicted Cloudburst'] == 'No' %}bg-green-50 border border-green-200{% else %}bg-gray-100 border border-gray-200{% endif %}">
|
163 |
+
<h3 class="text-xl font-medium text-gray-800 mb-3">Immediate Cloudburst Risk</h3>
|
164 |
+
{% if current_prediction['Predicted Cloudburst'] == "Error" %}
|
165 |
+
<p class="text-3xl font-bold status-error my-4">Prediction Error</p>
|
166 |
+
{% else %}
|
167 |
+
<p class="text-6xl font-bold my-4 {% if current_prediction['Predicted Cloudburst'] == 'Yes' %}risk-yes{% else %}risk-no{% endif %}">
|
168 |
+
{{ current_prediction['Predicted Cloudburst'] }}
|
169 |
+
</p>
|
170 |
+
<p class="text-3xl text-gray-700">
|
171 |
+
Risk Score: <strong class="{% if current_prediction['Predicted Cloudburst (%)'] > 70 %}risk-yes{% elif current_prediction['Predicted Cloudburst (%)'] > 40 %}text-orange-500{% else %}risk-no{% endif %}">{{ current_prediction['Predicted Cloudburst (%)'] }}%</strong>
|
172 |
+
</p>
|
173 |
+
{% endif %}
|
174 |
+
<p class="mt-3 text-xs text-gray-500">A "Yes" indicates conditions are favorable for potential cloudburst activity.</p>
|
175 |
+
</div>
|
176 |
+
</div>
|
177 |
+
</section>
|
178 |
+
{% endif %}
|
179 |
+
|
180 |
+
{% if shap_explanation and shap_explanation[0].feature not in ["SHAP N/A", "SHAP Disabled", "Prediction Disabled", "SHAP Error", "Prediction Error"] and not prediction_error and current_prediction and current_prediction['Predicted Cloudburst'] != "Error" %}
|
181 |
+
<section class="p-6 rounded-xl shadow-xl content-card">
|
182 |
+
<h2 class="text-3xl font-semibold section-title mb-5 text-center"><i class="fas fa-magnifying-glass-plus mr-2"></i>Key Factors Driving Current Risk (SHAP)</h2>
|
183 |
+
<div class="grid md:grid-cols-2 gap-6 items-center">
|
184 |
+
<div class="min-h-[350px] md:min-h-[400px]">
|
185 |
+
<canvas id="shapChart"></canvas>
|
186 |
+
</div>
|
187 |
+
<div class="scrollable-content pr-2">
|
188 |
+
{% set base_value_item = shap_explanation | selectattr('feature', 'equalto', 'Base Value (Average Prediction)') | first %}
|
189 |
+
{% if base_value_item and base_value_item.impact is number %}
|
190 |
+
<p class="text-sm text-gray-600 mb-3 p-3 bg-sky-50 rounded-md border border-sky-200">
|
191 |
+
<i class="fas fa-calculator mr-1 text-cyan-600"></i> The model's average (base) prediction for cloudburst probability is around <strong class="text-cyan-700">{{ "%.1f" | format(base_value_item.impact) }}%</strong>.
|
192 |
+
The features below show how current conditions shift the prediction from this base.
|
193 |
+
</p>
|
194 |
+
{% endif %}
|
195 |
+
<ul class="space-y-1.5 text-sm">
|
196 |
+
{% for item in shap_explanation if item.feature != 'Base Value (Average Prediction)' and item.impact is number %}
|
197 |
+
<li class="flex justify-between items-center p-3 rounded-md {% if item.impact > 0.1 %}bg-red-50 border border-red-100{% elif item.impact < -0.1 %}bg-green-50 border border-green-100{% else %}bg-gray-50 border border-gray-100{% endif %}">
|
198 |
+
<span class="truncate pr-2" title="{{ item.feature }}"><i class="fas {% if item.impact > 0.1 %}fa-arrow-trend-up text-red-500{% elif item.impact < -0.1 %}fa-arrow-trend-down text-green-500{% else %}fa-minus text-gray-400{% endif %} mr-2"></i>{{ item.feature }}</span>
|
199 |
+
<span class="font-semibold whitespace-nowrap {% if item.impact > 0.1 %}text-red-600{% elif item.impact < -0.1 %}text-green-600{% else %}text-gray-600{% endif %}">
|
200 |
+
{{ "%+.1f" | format(item.impact) }}%
|
201 |
+
</span>
|
202 |
+
</li>
|
203 |
+
{% endfor %}
|
204 |
+
</ul>
|
205 |
+
<p class="text-xs text-gray-500 mt-3">Positive values increase risk; negative values decrease it. Impacts are % change in probability.</p>
|
206 |
+
</div>
|
207 |
+
</div>
|
208 |
+
</section>
|
209 |
+
{% elif shap_explanation and (shap_explanation[0].feature in ["SHAP N/A", "SHAP Disabled", "SHAP Error", "Prediction Error"] or (current_prediction and current_prediction['Predicted Cloudburst'] == "Error")) %}
|
210 |
+
<section class="p-6 rounded-xl shadow-xl content-card">
|
211 |
+
<h2 class="text-3xl font-semibold section-title mb-2 text-center">Prediction Factors Analysis (SHAP)</h2>
|
212 |
+
<div class="bg-amber-50 border-l-4 border-amber-500 text-amber-700 p-4 rounded" role="alert">
|
213 |
+
<p class="font-bold"><i class="fas fa-triangle-exclamation mr-2"></i>SHAP Explanation Unavailable</p>
|
214 |
+
<p>{{ shap_explanation[0].impact }} {% if current_prediction and current_prediction['Predicted Cloudburst'] == "Error"%}(Prediction failed){%endif%} Check server logs for details if applicable.</p>
|
215 |
+
</div>
|
216 |
+
</section>
|
217 |
+
{% endif %}
|
218 |
+
|
219 |
+
{% if future_predictions and future_predictions|length > 0 %}
|
220 |
+
<section class="p-6 rounded-xl shadow-xl content-card">
|
221 |
+
<h2 class="text-3xl font-semibold section-title mb-5 text-center"><i class="fas fa-calendar-alt mr-2"></i>Cloudburst Risk Outlook: Next {{ future_predictions|length }} Days</h2>
|
222 |
+
<div class="mb-6 min-h-[300px] md:min-h-[350px]">
|
223 |
+
<canvas id="futureProbChart"></canvas>
|
224 |
+
</div>
|
225 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
226 |
+
{% for pred in future_predictions %}
|
227 |
+
{% set risk_level = 'low' %}
|
228 |
+
{% if pred.probability is number %}
|
229 |
+
{% if pred.probability > 70 %}{% set risk_level = 'high' %}
|
230 |
+
{% elif pred.probability > 40 %}{% set risk_level = 'moderate' %}
|
231 |
+
{% endif %}
|
232 |
+
{% elif pred.status in ['Pred. Err', 'Data Err'] %}{% set risk_level = 'error' %}
|
233 |
+
{% endif %}
|
234 |
+
<div class="future-day-card p-4 rounded-lg shadow risk-{{risk_level}}">
|
235 |
+
<p class="font-bold text-gray-700 text-md">{{ pred.date }}</p>
|
236 |
+
<div class="flex items-center my-2">
|
237 |
+
<i class="fas {{ weather_code_mapping[pred.weather_code]['icon'] if pred.weather_code in weather_code_mapping else 'fa-question-circle' }} text-3xl mr-3
|
238 |
+
{% if risk_level == 'high' %}text-red-500{% elif risk_level == 'moderate' %}text-yellow-600{% elif risk_level == 'low' %}text-cyan-600{% else %}text-gray-500{% endif %}"></i>
|
239 |
+
<div>
|
240 |
+
<p class="text-sm text-gray-600">{{ pred.weather_description }}</p>
|
241 |
+
<p class="text-xs text-gray-500">{{ pred.min_temp }} / {{ pred.max_temp }}</p>
|
242 |
+
</div>
|
243 |
+
</div>
|
244 |
+
<div class="mt-1 text-center">
|
245 |
+
{% if pred.probability is number %}
|
246 |
+
<p class="text-lg font-semibold {% if risk_level == 'high' %}text-red-700{% elif risk_level == 'moderate' %}text-amber-700{% else %}text-green-700{% endif %}">
|
247 |
+
Risk: {{ pred.probability }}%
|
248 |
+
</p>
|
249 |
+
{% else %}
|
250 |
+
<p class="text-sm font-semibold status-error">{{ pred.status }}</p>
|
251 |
+
{% endif %}
|
252 |
+
</div>
|
253 |
+
</div>
|
254 |
+
{% endfor %}
|
255 |
+
</div>
|
256 |
+
</section>
|
257 |
+
{% endif %}
|
258 |
+
|
259 |
+
{% if gemini_analysis %}
|
260 |
+
<section class="p-6 rounded-xl shadow-xl content-card">
|
261 |
+
<h2 class="text-3xl font-semibold section-title mb-5 text-center"><i class="fas fa-brain text-cyan-600 mr-2"></i>AI Expert Analysis & Recommendations</h2>
|
262 |
+
{% if gemini_analysis.error %}
|
263 |
+
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded" role="alert">
|
264 |
+
<p class="font-bold"><i class="fas fa-exclamation-circle mr-2"></i>AI Analysis Error</p>
|
265 |
+
<p>{{ gemini_analysis.error }}</p>
|
266 |
+
</div>
|
267 |
+
{% elif gemini_analysis.analysis %}
|
268 |
+
<div class="gemini-analysis-content scrollable-content p-3 bg-white border border-gray-200 rounded-md shadow-inner">
|
269 |
+
{{ gemini_analysis.analysis | safe }} {# CRITICAL: | safe filter renders HTML from Markdown #}
|
270 |
+
</div>
|
271 |
+
<p class="text-xs text-center text-gray-500 mt-4">Analysis by Google Gemini. May require interpretation.</p>
|
272 |
+
{% else %}
|
273 |
+
<p class="text-gray-600 text-center py-4">AI analysis is currently unavailable.</p>
|
274 |
+
{% endif %}
|
275 |
+
</section>
|
276 |
+
{% endif %}
|
277 |
+
</div>
|
278 |
+
{% endif %} {# end show_results #}
|
279 |
+
|
280 |
+
<footer class="text-center mt-12 py-6 border-t border-cyan-200">
|
281 |
+
<p class="text-sm text-cyan-800">© {{ current_year }} Cloudburst Predictor Pro. Advanced Weather Intelligence.</p>
|
282 |
+
<p class="text-xs text-cyan-700 mt-1">Data: Open-Meteo. AI: Google Gemini. Explainability: SHAP & DiCE. Map: Leaflet & OpenStreetMap.</p>
|
283 |
+
</footer>
|
284 |
+
</div>
|
285 |
+
|
286 |
+
<script>
|
287 |
+
const weatherCodeMapping = JSON.parse('{{ weather_code_mapping_json | safe }}');
|
288 |
+
document.addEventListener('DOMContentLoaded', function () {
|
289 |
+
const latInput = document.getElementById('lat');
|
290 |
+
const lonInput = document.getElementById('lon');
|
291 |
+
let initialLat = parseFloat(latInput.value) || {{ lat_initial|default(20.5937) }};
|
292 |
+
let initialLon = parseFloat(lonInput.value) || {{ lon_initial|default(78.9629) }};
|
293 |
+
let initialZoom = latInput.value && lonInput.value && !isNaN(initialLat) && !isNaN(initialLon) ? 10 : 5;
|
294 |
+
|
295 |
+
const map = L.map('map').setView([initialLat, initialLon], initialZoom);
|
296 |
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
297 |
+
attribution: '© OpenStreetMap contributors', maxZoom: 18
|
298 |
+
}).addTo(map);
|
299 |
+
let marker = null;
|
300 |
+
if (latInput.value && lonInput.value && !isNaN(initialLat) && !isNaN(initialLon)) {
|
301 |
+
marker = L.marker([initialLat, initialLon], {draggable: true}).addTo(map);
|
302 |
+
marker.on('dragend', updateInputsFromMarker);
|
303 |
+
}
|
304 |
+
|
305 |
+
L.Control.geocoder({ defaultMarkGeocode: false, placeholder: "Search city or address..." })
|
306 |
+
.on('markgeocode', function(e) {
|
307 |
+
const latlng = e.geocode.center;
|
308 |
+
map.setView(latlng, 11);
|
309 |
+
if (marker) { marker.setLatLng(latlng); }
|
310 |
+
else { marker = L.marker(latlng, {draggable: true}).addTo(map); marker.on('dragend', updateInputsFromMarker); }
|
311 |
+
latInput.value = latlng.lat.toFixed(5);
|
312 |
+
lonInput.value = latlng.lng.toFixed(5);
|
313 |
+
}).addTo(map);
|
314 |
+
|
315 |
+
function updateInputsFromMarker(e) {
|
316 |
+
const latlng = e.target.getLatLng();
|
317 |
+
latInput.value = latlng.lat.toFixed(5);
|
318 |
+
lonInput.value = latlng.lng.toFixed(5);
|
319 |
+
}
|
320 |
+
|
321 |
+
map.on('click', function(e) {
|
322 |
+
if (marker) { marker.setLatLng(e.latlng); }
|
323 |
+
else { marker = L.marker(e.latlng, {draggable: true}).addTo(map); marker.on('dragend', updateInputsFromMarker); }
|
324 |
+
latInput.value = e.latlng.lat.toFixed(5);
|
325 |
+
lonInput.value = e.latlng.lng.toFixed(5);
|
326 |
+
});
|
327 |
+
|
328 |
+
{% if show_results %}
|
329 |
+
const chartOptions = { responsive: true, maintainAspectRatio: false, animation: {duration: 600, easing: 'easeInOutQuart'} };
|
330 |
+
|
331 |
+
{% if shap_chart_data_json and shap_chart_data_json != "{}" %}
|
332 |
+
try {
|
333 |
+
const shapData = JSON.parse('{{ shap_chart_data_json | safe }}');
|
334 |
+
if (shapData.labels && shapData.labels.length > 0) {
|
335 |
+
const shapCtx = document.getElementById('shapChart')?.getContext('2d');
|
336 |
+
if (shapCtx) {
|
337 |
+
new Chart(shapCtx, {
|
338 |
+
type: 'bar',
|
339 |
+
data: {
|
340 |
+
labels: shapData.labels,
|
341 |
+
datasets: [{
|
342 |
+
label: 'SHAP Impact (% Change in Prob.)',
|
343 |
+
data: shapData.data,
|
344 |
+
backgroundColor: shapData.data.map(v => v >= 0 ? 'rgba(255, 99, 132, 0.75)' : 'rgba(75, 192, 192, 0.75)'),
|
345 |
+
borderColor: shapData.data.map(v => v >= 0 ? 'rgba(255, 99, 132, 1)' : 'rgba(75, 192, 192, 1)'),
|
346 |
+
borderWidth: 1
|
347 |
+
}]
|
348 |
+
},
|
349 |
+
options: { ...chartOptions, indexAxis: 'y',
|
350 |
+
scales: { x: { beginAtZero: true, title: { display: true, text: 'Contribution to Cloudburst Probability (%)', font: {size: 12, weight: '500'}}, grid: { color: 'rgba(0,0,0,0.05)'} } ,
|
351 |
+
y: { ticks: { font: {size: 10} }, grid: { display: false } }
|
352 |
+
},
|
353 |
+
plugins: { legend: { display: false }, tooltip: {callbacks: {label: (c) => `${c.dataset.label}: ${c.raw.toFixed(1)}%`}} }
|
354 |
+
}
|
355 |
+
});
|
356 |
+
}
|
357 |
+
} else {
|
358 |
+
document.getElementById('shapChart')?.parentElement?.insertAdjacentHTML('beforeend', '<p class="text-center text-sm text-gray-500 py-4">No SHAP data to display or data is invalid.</p>');
|
359 |
+
}
|
360 |
+
} catch (e) { console.error("SHAP chart error:", e); document.getElementById('shapChart')?.parentElement?.insertAdjacentHTML('beforeend', '<p class="text-center text-sm text-red-500 py-4">Error loading SHAP chart. See console.</p>'); }
|
361 |
+
{% endif %}
|
362 |
+
|
363 |
+
{% if future_prob_chart_data_json and future_prob_chart_data_json != "{}" %}
|
364 |
+
try {
|
365 |
+
const futureProbData = JSON.parse('{{ future_prob_chart_data_json | safe }}');
|
366 |
+
if (futureProbData.labels && futureProbData.labels.length > 0) {
|
367 |
+
const futureProbCtx = document.getElementById('futureProbChart')?.getContext('2d');
|
368 |
+
if (futureProbCtx) {
|
369 |
+
new Chart(futureProbCtx, {
|
370 |
+
type: 'line',
|
371 |
+
data: {
|
372 |
+
labels: futureProbData.labels,
|
373 |
+
datasets: [{
|
374 |
+
label: 'Cloudburst Risk (%)',
|
375 |
+
data: futureProbData.data,
|
376 |
+
borderColor: '#0288D1', backgroundColor: 'rgba(2, 136, 209, 0.2)',
|
377 |
+
tension: 0.35, fill: true, pointRadius: 5, pointHoverRadius: 7, pointBackgroundColor: '#01579B', borderWidth: 2
|
378 |
+
}]
|
379 |
+
},
|
380 |
+
options: { ...chartOptions,
|
381 |
+
scales: { y: { beginAtZero: true, max: 100, title: { display: true, text: 'Risk Probability (%)', font: {size: 12, weight: '500'}}, grid: { color: 'rgba(0,0,0,0.05)'} },
|
382 |
+
x: { ticks: { font: {size: 10} }, grid: { display: false } }
|
383 |
+
},
|
384 |
+
plugins: { legend: { display: true, position: 'top', labels:{boxWidth:15, padding:15, font:{size:11, weight: '500'}} }, tooltip: {callbacks: {label: (c) => `${c.dataset.label}: ${c.raw}%`}} }
|
385 |
+
}
|
386 |
+
});
|
387 |
+
}
|
388 |
+
} else {
|
389 |
+
document.getElementById('futureProbChart')?.parentElement?.insertAdjacentHTML('beforeend', '<p class="text-center text-sm text-gray-500 py-4">No future probability data to display or data is invalid.</p>');
|
390 |
+
}
|
391 |
+
} catch (e) { console.error("Future prob chart error:", e); document.getElementById('futureProbChart')?.parentElement?.insertAdjacentHTML('beforeend', '<p class="text-center text-sm text-red-500 py-4">Error loading future risk chart. See console.</p>');}
|
392 |
+
{% endif %}
|
393 |
+
{% endif %}
|
394 |
+
});
|
395 |
+
</script>
|
396 |
+
</body>
|
397 |
+
</html>
|
templates/index2.html
ADDED
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6 |
+
<title>Cloudburst Prediction System</title>
|
7 |
+
<!-- Tailwind CSS -->
|
8 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
|
9 |
+
<!-- Leaflet CSS -->
|
10 |
+
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
11 |
+
<!-- Font Awesome -->
|
12 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" />
|
13 |
+
<style>
|
14 |
+
/* Custom Animations and Styles */
|
15 |
+
body {
|
16 |
+
background: linear-gradient(135deg, #e6f2ff 0%, #ffffff 100%);
|
17 |
+
}
|
18 |
+
.card-glow {
|
19 |
+
transition: all 0.3s ease;
|
20 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
21 |
+
}
|
22 |
+
.card-glow:hover {
|
23 |
+
transform: translateY(-10px);
|
24 |
+
box-shadow: 0 15px 25px rgba(0,0,0,0.15);
|
25 |
+
}
|
26 |
+
.shine-effect {
|
27 |
+
position: relative;
|
28 |
+
overflow: hidden;
|
29 |
+
}
|
30 |
+
.shine-effect::before {
|
31 |
+
content: '';
|
32 |
+
position: absolute;
|
33 |
+
top: 0;
|
34 |
+
left: -100%;
|
35 |
+
width: 100%;
|
36 |
+
height: 100%;
|
37 |
+
background: linear-gradient(120deg, transparent, rgba(255,255,255,0.3), transparent);
|
38 |
+
transition: all 0.6s;
|
39 |
+
}
|
40 |
+
.shine-effect:hover::before {
|
41 |
+
left: 100%;
|
42 |
+
}
|
43 |
+
#map {
|
44 |
+
height: 400px;
|
45 |
+
width: 100%;
|
46 |
+
border-radius: 12px;
|
47 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
48 |
+
}
|
49 |
+
.animate-float {
|
50 |
+
animation: float 3s ease-in-out infinite;
|
51 |
+
}
|
52 |
+
@keyframes float {
|
53 |
+
0%, 100% { transform: translateY(0); }
|
54 |
+
50% { transform: translateY(-10px); }
|
55 |
+
}
|
56 |
+
.cloud-bg {
|
57 |
+
background: linear-gradient(135deg, #87CEEB 0%, #B0E0E6 100%);
|
58 |
+
}
|
59 |
+
</style>
|
60 |
+
</head>
|
61 |
+
<body class="font-sans antialiased">
|
62 |
+
<!-- Header with Animated Elements -->
|
63 |
+
<header class="cloud-bg py-6 shadow-lg relative overflow-hidden">
|
64 |
+
<div class="absolute left-4 top-4 animate-float">
|
65 |
+
<i class="fas fa-cloud text-white text-5xl opacity-80"></i>
|
66 |
+
</div>
|
67 |
+
<div class="container mx-auto text-center relative z-10">
|
68 |
+
<h1 class="text-5xl font-extrabold text-white drop-shadow-lg mb-2 animate-pulse">
|
69 |
+
Cloudburst Prediction System
|
70 |
+
</h1>
|
71 |
+
<p class="text-white text-lg opacity-80 animate-bounce">
|
72 |
+
Advanced Weather Forecasting Technology
|
73 |
+
</p>
|
74 |
+
</div>
|
75 |
+
</header>
|
76 |
+
|
77 |
+
<!-- Main Content Container -->
|
78 |
+
<div class="container mx-auto px-4 py-8">
|
79 |
+
<!-- Input and Map Section -->
|
80 |
+
<div class="grid md:grid-cols-2 gap-8">
|
81 |
+
<!-- Map Section -->
|
82 |
+
<div class="bg-white rounded-2xl p-6 shadow-xl card-glow shine-effect">
|
83 |
+
<div class="mb-4">
|
84 |
+
<input type="text" id="searchInput" placeholder="Search location"
|
85 |
+
class="w-full border-2 border-blue-200 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all">
|
86 |
+
<button id="searchBtn" class="mt-3 w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600 transition-all">
|
87 |
+
<i class="fas fa-search mr-2"></i>Search Location
|
88 |
+
</button>
|
89 |
+
</div>
|
90 |
+
<div id="map" class="rounded-xl"></div>
|
91 |
+
</div>
|
92 |
+
|
93 |
+
<!-- Coordinates Input Section -->
|
94 |
+
<div class="bg-white rounded-2xl p-6 shadow-xl card-glow shine-effect">
|
95 |
+
<form action="/forecast" method="get" class="space-y-4">
|
96 |
+
<div>
|
97 |
+
<label class="block text-gray-700 font-semibold mb-2">
|
98 |
+
<i class="fas fa-map-marker-alt mr-2"></i>Latitude
|
99 |
+
</label>
|
100 |
+
<input type="text" id="lat" name="lat" required
|
101 |
+
class="w-full border-2 border-blue-200 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
102 |
+
</div>
|
103 |
+
<div>
|
104 |
+
<label class="block text-gray-700 font-semibold mb-2">
|
105 |
+
<i class="fas fa-map-marker-alt mr-2"></i>Longitude
|
106 |
+
</label>
|
107 |
+
<input type="text" id="lon" name="lon" required
|
108 |
+
class="w-full border-2 border-blue-200 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
109 |
+
</div>
|
110 |
+
<button type="submit" class="w-full bg-green-500 text-white py-3 rounded-lg hover:bg-green-600 transition-all">
|
111 |
+
<i class="fas fa-paper-plane mr-2"></i>Get Forecast
|
112 |
+
</button>
|
113 |
+
</form>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
|
117 |
+
<!-- Forecast Results (Conditionally Rendered) -->
|
118 |
+
{% if result %}
|
119 |
+
<div class="mt-8 space-y-8">
|
120 |
+
<!-- Weather Parameters Grid -->
|
121 |
+
<div class="bg-white rounded-2xl p-6 shadow-xl card-glow">
|
122 |
+
<h2 class="text-3xl font-bold text-center mb-6 text-blue-600">
|
123 |
+
Weather Parameters
|
124 |
+
</h2>
|
125 |
+
<div class="grid md:grid-cols-3 gap-6">
|
126 |
+
{% for key, value in result.items() %}
|
127 |
+
{% if key not in ['Predicted Cloudburst (%)', 'Predicted Cloudburst'] %}
|
128 |
+
<div class="bg-blue-50 border border-blue-100 rounded-xl p-4 transform transition hover:scale-105 hover:shadow-lg">
|
129 |
+
<h3 class="text-lg font-semibold text-blue-700 mb-2">{{ key }}</h3>
|
130 |
+
<p class="text-gray-700 font-medium">{{ value }}</p>
|
131 |
+
</div>
|
132 |
+
{% endif %}
|
133 |
+
{% endfor %}
|
134 |
+
</div>
|
135 |
+
</div>
|
136 |
+
|
137 |
+
<!-- Prediction Visualization (Left/Right Layout) -->
|
138 |
+
<div class="bg-white rounded-2xl p-6 shadow-xl">
|
139 |
+
<div class="grid md:grid-cols-2 gap-8 items-center">
|
140 |
+
<!-- Left: Cloudburst Probability -->
|
141 |
+
<div class="text-center">
|
142 |
+
<h2 class="text-2xl font-semibold text-gray-700 mb-4">
|
143 |
+
Cloudburst Probability
|
144 |
+
</h2>
|
145 |
+
<div class="text-6xl font-bold text-blue-600">
|
146 |
+
{{ result["Predicted Cloudburst (%)"] }}%
|
147 |
+
</div>
|
148 |
+
</div>
|
149 |
+
|
150 |
+
<!-- Right: Cloudburst Prediction Status -->
|
151 |
+
<div class="text-center">
|
152 |
+
<h2 class="text-2xl font-semibold text-gray-700 mb-4">
|
153 |
+
Cloudburst Prediction Status
|
154 |
+
</h2>
|
155 |
+
{% if result['Predicted Cloudburst'] == "Yes" %}
|
156 |
+
<div class="text-6xl font-bold text-red-600">
|
157 |
+
YES
|
158 |
+
</div>
|
159 |
+
{% else %}
|
160 |
+
<div class="text-6xl font-bold text-green-600">
|
161 |
+
NO
|
162 |
+
</div>
|
163 |
+
{% endif %}
|
164 |
+
</div>
|
165 |
+
</div>
|
166 |
+
</div>
|
167 |
+
</div>
|
168 |
+
{% endif %}
|
169 |
+
</div>
|
170 |
+
|
171 |
+
<!-- Scripts -->
|
172 |
+
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
173 |
+
<script>
|
174 |
+
// Leaflet Map Initialization
|
175 |
+
var map = L.map('map').setView([20.5937, 78.9629], 5);
|
176 |
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
177 |
+
attribution: '© OpenStreetMap contributors'
|
178 |
+
}).addTo(map);
|
179 |
+
|
180 |
+
var marker;
|
181 |
+
|
182 |
+
// Map Click Event
|
183 |
+
map.on('click', function(e) {
|
184 |
+
var lat = e.latlng.lat.toFixed(6);
|
185 |
+
var lon = e.latlng.lng.toFixed(6);
|
186 |
+
document.getElementById('lat').value = lat;
|
187 |
+
document.getElementById('lon').value = lon;
|
188 |
+
if (marker) {
|
189 |
+
marker.setLatLng(e.latlng);
|
190 |
+
} else {
|
191 |
+
marker = L.marker(e.latlng).addTo(map);
|
192 |
+
}
|
193 |
+
});
|
194 |
+
|
195 |
+
// Search Functionality
|
196 |
+
document.getElementById("searchBtn").addEventListener("click", function() {
|
197 |
+
var address = document.getElementById("searchInput").value;
|
198 |
+
if(address) {
|
199 |
+
fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${address}`)
|
200 |
+
.then(response => response.json())
|
201 |
+
.then(data => {
|
202 |
+
if(data && data.length > 0) {
|
203 |
+
var lat = data[0].lat;
|
204 |
+
var lon = data[0].lon;
|
205 |
+
map.setView([lat, lon], 13);
|
206 |
+
if(marker) {
|
207 |
+
marker.setLatLng([lat, lon]);
|
208 |
+
} else {
|
209 |
+
marker = L.marker([lat, lon]).addTo(map);
|
210 |
+
}
|
211 |
+
document.getElementById('lat').value = parseFloat(lat).toFixed(6);
|
212 |
+
document.getElementById('lon').value = parseFloat(lon).toFixed(6);
|
213 |
+
}
|
214 |
+
});
|
215 |
+
}
|
216 |
+
});
|
217 |
+
</script>
|
218 |
+
</body>
|
219 |
+
</html>
|