rajkhanke commited on
Commit
643b9da
·
verified ·
1 Parent(s): b6c90a0

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +119 -0
  2. dam.csv +18 -0
  3. requirements.txt +5 -0
  4. templates/index.html +1004 -0
app.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import pandas as pd
4
+ import requests
5
+ from flask import Flask, render_template, request, jsonify
6
+ from dotenv import load_dotenv
7
+ import google.generativeai as genai
8
+
9
+ load_dotenv()
10
+
11
+ app = Flask(__name__)
12
+
13
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
14
+ CSV_PATH = "dam.csv"
15
+
16
+ # Initialize Gemini API
17
+ genai.configure(api_key=GOOGLE_API_KEY)
18
+ gemini_model = genai.GenerativeModel('gemini-1.5-flash')
19
+
20
+ # Load dams CSV with latitude, longitude, dam_name columns
21
+ dams_df = pd.read_csv(CSV_PATH)
22
+ dams_df['latitude'] = pd.to_numeric(dams_df['latitude'], errors='coerce')
23
+ dams_df['longitude'] = pd.to_numeric(dams_df['longitude'], errors='coerce')
24
+ dams_df.dropna(subset=['latitude', 'longitude', 'dam_name'], inplace=True)
25
+
26
+ @app.route('/')
27
+ def index():
28
+ dams = dams_df.to_dict(orient='records')
29
+ return render_template('index.html', dams=dams)
30
+
31
+ @app.route('/dam_details', methods=['POST'])
32
+ def dam_details():
33
+ data = request.json
34
+ dam_name = data.get('dam_name')
35
+ lat = data.get('latitude')
36
+ lon = data.get('longitude')
37
+ current_level = data.get('current_water_level')
38
+
39
+ # Gemini: Get dam info
40
+ prompt = f"""
41
+ Provide the following information for the dam "{dam_name}" in JSON format:
42
+ {{
43
+ "capacity_mcm": "N/A if unknown",
44
+ "number_of_spillway_gates": "N/A if unknown",
45
+ "gate_type": "N/A if unknown",
46
+ "dam_height_meters": "N/A if unknown",
47
+ "purpose": "N/A if unknown",
48
+ "river_name": "N/A if unknown",
49
+ "country": "N/A if unknown"
50
+ }}
51
+ """
52
+ try:
53
+ response = gemini_model.generate_content(prompt)
54
+ text = response.text.strip()
55
+ # Extract JSON from response text
56
+ json_start = text.find('{')
57
+ json_end = text.rfind('}') + 1
58
+ dam_info = json.loads(text[json_start:json_end])
59
+ except Exception as e:
60
+ return jsonify({"error": f"Gemini API error: {e}"})
61
+
62
+ # Fetch rainfall forecast
63
+ try:
64
+ forecast_url = "https://api.open-meteo.com/v1/forecast"
65
+ params = {
66
+ "latitude": lat,
67
+ "longitude": lon,
68
+ "daily": "precipitation_sum",
69
+ "forecast_days": 10,
70
+ "timezone": "auto"
71
+ }
72
+ r = requests.get(forecast_url, params=params, timeout=10)
73
+ r.raise_for_status()
74
+ forecast_data = r.json()
75
+ dates = forecast_data['daily']['time']
76
+ rain = forecast_data['daily']['precipitation_sum']
77
+ rainfall_list = [{"date": d, "rainfall_mm": v} for d, v in zip(dates, rain)]
78
+ except Exception as e:
79
+ return jsonify({"error": f"Rainfall API error: {e}"})
80
+
81
+ # Prepare prompt for operational advice
82
+ rain_summary = "\n".join([f"{r['date']}: {r['rainfall_mm']} mm" for r in rainfall_list])
83
+ advice_prompt = f"""
84
+ You are an expert dam operations advisor. For the dam "{dam_name}", here is the info:
85
+ Capacity (MCM): {dam_info.get('capacity_mcm', 'N/A')}
86
+ Spillway gates: {dam_info.get('number_of_spillway_gates', 'N/A')} ({dam_info.get('gate_type', 'N/A')})
87
+ Height: {dam_info.get('dam_height_meters', 'N/A')} meters
88
+ Purpose: {dam_info.get('purpose', 'N/A')}
89
+ River: {dam_info.get('river_name', 'N/A')}
90
+ Country: {dam_info.get('country', 'N/A')}
91
+
92
+ Current water level: {current_level} % of capacity.
93
+
94
+ Rainfall forecast for next 10 days:
95
+ {rain_summary}
96
+
97
+ Based on this data, provide operational recommendations with key performance indicators (KPIs).
98
+ Format your response with:
99
+ KEY RECOMMENDATIONS:
100
+ - Overall Status: [NORMAL, WATCH, WARNING, EMERGENCY]
101
+ - Action Required: [MAINTAIN CURRENT, OPEN GATES, CLOSE GATES, MONITOR CLOSELY]
102
+ - Urgency and timing if opening gates
103
+ - Suggested % of gate opening
104
+ - Other notes
105
+ """
106
+ try:
107
+ advice_resp = gemini_model.generate_content(advice_prompt)
108
+ advice_text = advice_resp.text.strip()
109
+ except Exception as e:
110
+ advice_text = f"Error generating advice: {e}"
111
+
112
+ return jsonify({
113
+ "dam_info": dam_info,
114
+ "rainfall_forecast": rainfall_list,
115
+ "operational_advice": advice_text
116
+ })
117
+
118
+ if __name__ == "__main__":
119
+ app.run(debug=True)
dam.csv ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dam_name,latitude,longitude
2
+ Bhakra Dam,31.4166,76.4333
3
+ Hirakud Dam,21.5700,83.8700
4
+ Nagarjuna Sagar Dam,16.5750,79.3130
5
+ Mettur Dam,11.7969,77.8008
6
+ Koyna Dam,17.3998,73.7514
7
+ Tungabhadra Dam,15.3268,76.3362
8
+ Hoover Dam,36.0156,-114.7378
9
+ Three Gorges Dam,30.8231,111.0037
10
+ Glen Canyon Dam,36.9375,-111.4844
11
+ Alqueva Dam,38.2000,-7.4833
12
+ Kariba Dam, -16.5200, 28.7900
13
+ Aswan High Dam,23.9667,32.8833
14
+ Itaipu Dam,-25.4081,-54.5889
15
+ Tehri Dam,30.3780,78.4800
16
+ Sardar Sarovar Dam,21.8300,73.7400
17
+ Krishnarajasagara Dam,12.4247,76.5773
18
+ Linganamakki Dam,14.1819,74.8486
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask
2
+ pandas
3
+ requests
4
+ python-dotenv
5
+ google-generativeai
templates/index.html ADDED
@@ -0,0 +1,1004 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Dam Operation Simulator</title>
6
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" />
7
+ <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" crossorigin="" />
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet" />
9
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet" />
10
+ <style>
11
+ :root {
12
+ --primary: #0891b2;
13
+ --primary-dark: #0e7490;
14
+ --secondary: #0ea5e9;
15
+ --accent: #06b6d4;
16
+ --light: #ecfeff;
17
+ --dark: #164e63;
18
+ --danger: #f43f5e;
19
+ --success: #10b981;
20
+ --warning: #f59e0b;
21
+ }
22
+
23
+ body {
24
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
25
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
26
+ min-height: 100vh;
27
+ padding: 0;
28
+ margin: 0;
29
+ }
30
+
31
+ .page-container {
32
+ max-width: 1400px;
33
+ margin: 0 auto;
34
+ padding: 1.5rem;
35
+ }
36
+
37
+ .app-header {
38
+ background: linear-gradient(to right, var(--primary), var(--secondary));
39
+ color: white;
40
+ padding: 1.5rem;
41
+ border-radius: 10px;
42
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
43
+ margin-bottom: 2rem;
44
+ position: relative;
45
+ overflow: hidden;
46
+ }
47
+
48
+ .app-header h1 {
49
+ font-weight: 700;
50
+ margin: 0;
51
+ position: relative;
52
+ z-index: 2;
53
+ }
54
+
55
+ .water-ripple {
56
+ position: absolute;
57
+ top: 0;
58
+ left: 0;
59
+ right: 0;
60
+ bottom: 0;
61
+ background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPgogIDxkZWZzPgogICAgPHBhdHRlcm4gaWQ9IndhdmUiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiIHdpZHRoPSIxMDAiIGhlaWdodD0iMjAiIHBhdHRlcm5UcmFuc2Zvcm09InJvdGF0ZSgwKSI+CiAgICAgIDxwYXRoIGQ9Ik0wLDEwIHH1LDEwIDI1LDAgdC0yNSwwIHoiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4xKSIgLz4KICAgIDwvcGF0dGVybj4KICA8L2RlZnM+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCN3YXZlKSIgLz4KPC9zdmc+');
62
+ opacity: 0.2;
63
+ z-index: 1;
64
+ }
65
+
66
+ .card {
67
+ border-radius: 10px;
68
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
69
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
70
+ background-color: white;
71
+ overflow: hidden;
72
+ }
73
+
74
+ .card:hover {
75
+ transform: translateY(-5px);
76
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
77
+ }
78
+
79
+ .card-header {
80
+ background: linear-gradient(to right, var(--primary), var(--accent));
81
+ color: white;
82
+ padding: 1rem;
83
+ font-weight: 600;
84
+ border-bottom: none;
85
+ }
86
+
87
+ #map {
88
+ height: 500px;
89
+ border-radius: 10px;
90
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
91
+ }
92
+
93
+ .btn-primary {
94
+ background: var(--primary);
95
+ border-color: var(--primary);
96
+ position: relative;
97
+ overflow: hidden;
98
+ transition: background-color 0.3s ease;
99
+ }
100
+
101
+ .btn-primary:hover {
102
+ background: var(--primary-dark);
103
+ border-color: var(--primary-dark);
104
+ }
105
+
106
+ .btn-primary:active {
107
+ transform: scale(0.98);
108
+ }
109
+
110
+ .btn-ripple {
111
+ position: relative;
112
+ overflow: hidden;
113
+ }
114
+
115
+ .btn-ripple .ripple {
116
+ position: absolute;
117
+ border-radius: 50%;
118
+ background-color: rgba(255, 255, 255, 0.4);
119
+ transform: scale(0);
120
+ animation: ripple 0.6s linear;
121
+ }
122
+
123
+ @keyframes ripple {
124
+ to {
125
+ transform: scale(4);
126
+ opacity: 0;
127
+ }
128
+ }
129
+
130
+ .water-level-container {
131
+ background-color: #f0f9ff;
132
+ border-radius: 10px;
133
+ padding: 1rem;
134
+ margin-bottom: 1.5rem;
135
+ border: 1px solid #bae6fd;
136
+ position: relative;
137
+ }
138
+
139
+ .water-level-value {
140
+ font-size: 2rem;
141
+ font-weight: bold;
142
+ color: var(--primary);
143
+ text-align: center;
144
+ margin-bottom: 0.5rem;
145
+ transition: color 0.3s ease;
146
+ }
147
+
148
+ .water-tank {
149
+ height: 150px;
150
+ width: 50px;
151
+ border: 2px solid #94a3b8;
152
+ border-radius: 5px;
153
+ margin: 0 auto;
154
+ position: relative;
155
+ overflow: hidden;
156
+ }
157
+
158
+ .water-fill {
159
+ position: absolute;
160
+ bottom: 0;
161
+ left: 0;
162
+ right: 0;
163
+ background: linear-gradient(to bottom, var(--primary), var(--secondary));
164
+ height: 60%;
165
+ transition: height 0.5s ease;
166
+ }
167
+
168
+ .water-waves {
169
+ position: absolute;
170
+ top: 0;
171
+ left: 0;
172
+ right: 0;
173
+ height: 8px;
174
+ background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPgogIDxkZWZzPgogICAgPHBhdHRlcm4gaWQ9IndhdmUiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiIHdpZHRoPSIyMCIgaGVpZ2h0PSI4IiBwYXR0ZXJuVHJhbnNmb3JtPSJyb3RhdGUoMCkiPgogICAgICA8cGF0aCBkPSJNMCw0IGMxLDIgNCwyIDUsMyBzNCwtNSAxMCwtNSBzNSw1IDUsMSIgc3Ryb2tlPSJyZ2JhKDI1NSwyNTUsMjU1LDAuNSkiIGZpbGw9Im5vbmUiIC8+CiAgICA8L3BhdHRlcm4+CiAgPC9kZWZzPgogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjd2F2ZSkiIC8+Cjwvc3ZnPg==');
175
+ animation: waveAnimation 2s linear infinite;
176
+ }
177
+
178
+ @keyframes waveAnimation {
179
+ 0% {
180
+ background-position: 0 0;
181
+ }
182
+ 100% {
183
+ background-position: 20px 0;
184
+ }
185
+ }
186
+
187
+ .info-box { /* For Dam Information Table */
188
+ max-height: 400px;
189
+ overflow-y: auto;
190
+ border-radius: 10px;
191
+ padding: 1rem;
192
+ background-color: white;
193
+ border: 1px solid #e2e8f0;
194
+ /* These were making the table text monospace, removed for consistency with recommendations */
195
+ /* white-space: pre-wrap;
196
+ font-family: monospace; */
197
+ font-size: 0.9rem;
198
+ line-height: 1.5;
199
+ }
200
+
201
+ .info-box::-webkit-scrollbar {
202
+ width: 8px;
203
+ }
204
+
205
+ .info-box::-webkit-scrollbar-track {
206
+ background: #f1f1f1;
207
+ border-radius: 10px;
208
+ }
209
+
210
+ .info-box::-webkit-scrollbar-thumb {
211
+ background: var(--primary);
212
+ border-radius: 10px;
213
+ }
214
+
215
+ .info-box::-webkit-scrollbar-thumb:hover {
216
+ background: var(--primary-dark);
217
+ }
218
+
219
+ /* Styles for Operational Recommendations content */
220
+ #operational-advice-content {
221
+ max-height: 450px; /* Or your desired height */
222
+ overflow-y: auto;
223
+ padding-right: 10px; /* For scrollbar */
224
+ }
225
+ #operational-advice-content::-webkit-scrollbar {
226
+ width: 8px;
227
+ }
228
+ #operational-advice-content::-webkit-scrollbar-track {
229
+ background: #f1f1f1;
230
+ border-radius: 10px;
231
+ }
232
+ #operational-advice-content::-webkit-scrollbar-thumb {
233
+ background: var(--primary);
234
+ border-radius: 10px;
235
+ }
236
+ #operational-advice-content::-webkit-scrollbar-thumb:hover {
237
+ background: var(--primary-dark);
238
+ }
239
+ .recommendation-item h6 strong {
240
+ color: var(--primary-dark);
241
+ }
242
+
243
+
244
+ .loading-animation {
245
+ display: flex;
246
+ justify-content: center;
247
+ align-items: center;
248
+ padding: 2rem;
249
+ }
250
+
251
+ .loading-drops {
252
+ display: flex;
253
+ align-items: flex-end;
254
+ }
255
+
256
+ .drop {
257
+ width: 8px;
258
+ height: 30px;
259
+ margin: 0 5px;
260
+ background-color: var(--primary);
261
+ border-radius: 50% 50% 0 0 / 80% 80% 0 0;
262
+ animation: drip 1.5s ease-in-out infinite;
263
+ }
264
+
265
+ .drop:nth-child(2) {
266
+ animation-delay: 0.2s;
267
+ height: 35px;
268
+ }
269
+
270
+ .drop:nth-child(3) {
271
+ animation-delay: 0.4s;
272
+ height: 40px;
273
+ }
274
+
275
+ .drop:nth-child(4) {
276
+ animation-delay: 0.6s;
277
+ height: 35px;
278
+ }
279
+
280
+ .drop:nth-child(5) {
281
+ animation-delay: 0.8s;
282
+ height: 30px;
283
+ }
284
+
285
+ @keyframes drip {
286
+ 0%, 100% {
287
+ transform: scaleY(0.8);
288
+ }
289
+ 50% {
290
+ transform: scaleY(1.2);
291
+ }
292
+ }
293
+
294
+ .dam-control-panel {
295
+ margin-bottom: 1.5rem;
296
+ }
297
+
298
+ .form-select {
299
+ background-color: white;
300
+ border: 1px solid #bae6fd;
301
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
302
+ }
303
+
304
+ .form-select:focus {
305
+ border-color: var(--primary);
306
+ box-shadow: 0 0 0 0.25rem rgba(8, 145, 178, 0.25);
307
+ }
308
+
309
+ .form-range {
310
+ height: 1.5rem;
311
+ }
312
+
313
+ .form-range::-webkit-slider-thumb {
314
+ background: var(--primary);
315
+ }
316
+
317
+ .form-range::-moz-range-thumb {
318
+ background: var(--primary);
319
+ }
320
+
321
+ .form-range::-webkit-slider-runnable-track {
322
+ background: linear-gradient(to right, #e0f2fe, #0ea5e9);
323
+ border-radius: 0.5rem;
324
+ height: 0.5rem;
325
+ }
326
+
327
+ .form-range::-moz-range-track {
328
+ background: linear-gradient(to right, #e0f2fe, #0ea5e9);
329
+ border-radius: 0.5rem;
330
+ height: 0.5rem;
331
+ }
332
+
333
+ .badge-status {
334
+ padding: 0.5rem 1rem;
335
+ border-radius: 20px;
336
+ font-weight: 600;
337
+ display: inline-block;
338
+ margin-top: 0.5rem;
339
+ }
340
+
341
+ .badge-normal {
342
+ background-color: var(--success);
343
+ color: white;
344
+ }
345
+
346
+ .badge-watch {
347
+ background-color: var(--warning);
348
+ color: white;
349
+ }
350
+
351
+ .badge-warning { /* This was --danger, but visually a bit strong, using a slightly lighter red */
352
+ background-color: #dc2626; /* Tailwind red-600 */
353
+ color: white;
354
+ }
355
+
356
+ .badge-emergency {
357
+ background-color: #b91c1c; /* Tailwind red-700 / Darker red */
358
+ color: white;
359
+ animation: pulse 1.5s infinite;
360
+ }
361
+
362
+ @keyframes pulse {
363
+ 0% {
364
+ transform: scale(1);
365
+ }
366
+ 50% {
367
+ transform: scale(1.05);
368
+ }
369
+ 100% {
370
+ transform: scale(1);
371
+ }
372
+ }
373
+
374
+ .rainfall-chart {
375
+ margin-top: 1rem;
376
+ height: 150px;
377
+ position: relative;
378
+ }
379
+
380
+ .rainfall-bar {
381
+ position: absolute;
382
+ bottom: 0;
383
+ background: linear-gradient(to top, var(--primary), var(--secondary));
384
+ border-radius: 5px 5px 0 0;
385
+ transition: height 0.5s ease;
386
+ width: 8%; /* Adjusted for better spacing with more bars */
387
+ margin-right: 1%;
388
+ }
389
+
390
+ .rainfall-date {
391
+ position: absolute;
392
+ bottom: -20px;
393
+ font-size: 0.7rem;
394
+ width: 8%; /* Should match bar width */
395
+ text-align: center;
396
+ transform: rotate(-45deg);
397
+ transform-origin: top left;
398
+ white-space: nowrap;
399
+ }
400
+
401
+ .rainfall-value {
402
+ position: absolute;
403
+ top: -20px;
404
+ font-size: 0.7rem;
405
+ width: 100%;
406
+ text-align: center;
407
+ font-weight: bold;
408
+ color: var(--primary-dark);
409
+ }
410
+ </style>
411
+ </head>
412
+ <body>
413
+ <div class="page-container">
414
+ <div class="app-header animate__animated animate__fadeIn">
415
+ <div class="water-ripple"></div>
416
+ <h1><i class="fas fa-water me-2"></i> Dam Operation Simulator</h1>
417
+ <p class="mb-0 mt-2">Monitor and simulate water levels, rainfall forecasts and get operational advice</p>
418
+ </div>
419
+
420
+ <div class="row g-4">
421
+ <div class="col-lg-8 animate__animated animate__fadeInLeft">
422
+ <div class="card h-100">
423
+ <div class="card-header">
424
+ <i class="fas fa-map-location-dot me-2"></i> Interactive Dam Map
425
+ </div>
426
+ <div class="card-body p-2">
427
+ <div id="map"></div>
428
+ </div>
429
+ </div>
430
+ </div>
431
+
432
+ <div class="col-lg-4 animate__animated animate__fadeInRight">
433
+ <div class="card dam-control-panel">
434
+ <div class="card-header">
435
+ <i class="fas fa-sliders me-2"></i> Dam Controls
436
+ </div>
437
+ <div class="card-body">
438
+ <div class="mb-3">
439
+ <label for="dam-select" class="form-label">
440
+ <i class="fas fa-database me-2"></i> Select Dam:
441
+ </label>
442
+ <select id="dam-select" class="form-select">
443
+ <option value="" selected disabled>Select a dam</option>
444
+ {% for dam in dams %}
445
+ <option value="{{ dam.dam_name }}" data-lat="{{ dam.latitude }}" data-lon="{{ dam.longitude }}">
446
+ {{ dam.dam_name }}
447
+ </option>
448
+ {% endfor %}
449
+ </select>
450
+ </div>
451
+
452
+ <div class="water-level-container mb-3">
453
+ <label for="water-level" class="form-label">
454
+ <i class="fas fa-fill-drip me-2"></i> Current Water Level:
455
+ </label>
456
+ <div class="water-level-value" id="water-level-val">60%</div>
457
+
458
+ <div class="d-flex align-items-center">
459
+ <div class="flex-grow-1">
460
+ <input type="range" id="water-level" min="0" max="100" value="60" step="1" class="form-range" />
461
+ </div>
462
+ <div class="ms-3">
463
+ <div class="water-tank">
464
+ <div class="water-fill" id="water-tank-fill">
465
+ <div class="water-waves"></div>
466
+ </div>
467
+ </div>
468
+ </div>
469
+ </div>
470
+ </div>
471
+
472
+ <button id="get-info-btn" class="btn btn-primary w-100 btn-ripple">
473
+ <i class="fas fa-sync me-2"></i> Get Dam Info & Advice
474
+ </button>
475
+ </div>
476
+ </div>
477
+
478
+ <div class="card">
479
+ <div class="card-header">
480
+ <i class="fas fa-info-circle me-2"></i> Dam Information
481
+ </div>
482
+ <div class="card-body p-0">
483
+ <div class="info-box" id="dam-info-box">
484
+ <div class="text-center text-muted py-5">
485
+ <i class="fas fa-water fa-3x mb-3"></i>
486
+ <p>Select a dam and click "Get Dam Info & Advice" to view details</p>
487
+ </div>
488
+ </div>
489
+ </div>
490
+ </div>
491
+ </div>
492
+ </div>
493
+
494
+ <div id="rainfall-visualization" class="card mt-4 animate__animated animate__fadeInUp d-none">
495
+ <div class="card-header">
496
+ <i class="fas fa-cloud-rain me-2"></i> 10-Day Rainfall Forecast
497
+ </div>
498
+ <div class="card-body p-3">
499
+ <div class="rainfall-chart" id="rainfall-chart">
500
+ <!-- Rainfall bars will be added here via JS -->
501
+ </div>
502
+ </div>
503
+ </div>
504
+
505
+ <div id="operation-advice-card" class="card mt-4 animate__animated animate__fadeInUp d-none">
506
+ <div class="card-header">
507
+ <i class="fas fa-clipboard-list me-2"></i> Operational Recommendations
508
+ </div>
509
+ <div class="card-body">
510
+ <div id="status-badge" class="text-center mb-3">
511
+ <!-- Status badge will be inserted here -->
512
+ </div>
513
+ <div id="operational-advice-content">
514
+ <!-- Operational advice will be inserted here -->
515
+ </div>
516
+ </div>
517
+ </div>
518
+ </div>
519
+
520
+ <script src="https://unpkg.com/[email protected]/dist/leaflet.js" crossorigin=""></script>
521
+ <script>
522
+ // Ensure dams is defined, even if empty, to prevent errors if server doesn't provide it.
523
+ const dams = typeof {{ dams|tojson }} !== 'undefined' ? {{ dams|tojson }} : [];
524
+ let map, activeMarker;
525
+ let markers = {}; // Keep track of markers
526
+
527
+ // Initialize map with custom marker icons
528
+ function initMap() {
529
+ map = L.map('map').setView([20, 0], 2); // Default view
530
+
531
+ L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
532
+ maxZoom: 18,
533
+ attribution: '© OpenStreetMap contributors'
534
+ }).addTo(map);
535
+
536
+ const damIcon = L.icon({
537
+ iconUrl: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzA4OTFiMiIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPSJNMTIgM0w2IDE2aDEyTDEyIDN6TTEyIDZsMy41IDdsLTcgMEwxMiA2eiIvPjxyZWN0IHg9IjQiIHk9IjE4IiB3aWR0aD0iMTYiIGhlaWdodD0iMyIgcng9IjEiLz48L3N2Zz4=',
538
+ iconSize: [32, 32],
539
+ iconAnchor: [16, 32],
540
+ popupAnchor: [0, -32]
541
+ });
542
+
543
+ const activeIcon = L.icon({
544
+ iconUrl: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2Y1OWUwYiIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPSJNMTIgM0w2IDE2aDEyTDEyIDN6TTEyIDZsMy41IDdsLTcgMEwxMiA2eiIvPjxyZWN0IHg9IjQiIHk9IjE4IiB3aWR0aD0iMTYiIGhlaWdodD0iMyIgcng9IjEiLz48L3N2Zz4=',
545
+ iconSize: [38, 38],
546
+ iconAnchor: [19, 38],
547
+ popupAnchor: [0, -32]
548
+ });
549
+
550
+ dams.forEach(dam => {
551
+ if (dam.latitude && dam.longitude) {
552
+ const marker = L.marker([dam.latitude, dam.longitude], { icon: damIcon })
553
+ .addTo(map)
554
+ .bindPopup(`
555
+ <div class="text-center">
556
+ <strong>${dam.dam_name}</strong>
557
+ <hr class="my-2">
558
+ <button class="btn btn-sm btn-primary select-dam-btn" data-dam-name="${dam.dam_name}">Select Dam</button>
559
+ </div>
560
+ `);
561
+
562
+ marker.on('click', () => {
563
+ if (activeMarker && activeMarker !== marker) {
564
+ activeMarker.setIcon(damIcon);
565
+ }
566
+ marker.setIcon(activeIcon);
567
+ activeMarker = marker;
568
+ });
569
+
570
+ marker.on('popupopen', (e) => {
571
+ const popupNode = e.popup.getElement();
572
+ const selectBtn = popupNode.querySelector('.select-dam-btn');
573
+ if (selectBtn) {
574
+ selectBtn.addEventListener('click', () => {
575
+ const damNameToSelect = selectBtn.getAttribute('data-dam-name');
576
+ document.getElementById('dam-select').value = damNameToSelect;
577
+ // Manually trigger change event for the select
578
+ document.getElementById('dam-select').dispatchEvent(new Event('change'));
579
+ marker.closePopup();
580
+ });
581
+ }
582
+ });
583
+ markers[dam.dam_name] = marker;
584
+ }
585
+ });
586
+ return markers; // markers is already a global variable, but returning is fine
587
+ }
588
+
589
+ // Center map to selected dam
590
+ function centerMapToDam(lat, lon, damName) {
591
+ map.setView([lat, lon], 10);
592
+
593
+ const damIcon = L.icon({ // Re-define here or make global
594
+ iconUrl: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzA4OTFiMiIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPSJNMTIgM0w2IDE2aDEyTDEyIDN6TTEyIDZsMy41IDdsLTcgMEwxMiA2eiIvPjxyZWN0IHg9IjQiIHk9IjE4IiB3aWR0aD0iMTYiIGhlaWdodD0iMyIgcng9IjEiLz48L3N2Zz4=',
595
+ iconSize: [32, 32], iconAnchor: [16, 32], popupAnchor: [0, -32]
596
+ });
597
+ const activeIcon = L.icon({ // Re-define here or make global
598
+ iconUrl: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2Y1OWUwYiIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPSJNMTIgM0w2IDE2aDEyTDEyIDN6TTEyIDZsMy41IDdsLTcgMEwxMiA2eiIvPjxyZWN0IHg9IjQiIHk9IjE4IiB3aWR0aD0iMTYiIGhlaWdodD0iMyIgcng9IjEiLz48L3N2Zz4=',
599
+ iconSize: [38, 38], iconAnchor: [19, 38], popupAnchor: [0, -32]
600
+ });
601
+
602
+ if (activeMarker) {
603
+ activeMarker.setIcon(damIcon);
604
+ }
605
+ activeMarker = markers[damName];
606
+ if (activeMarker) {
607
+ activeMarker.setIcon(activeIcon);
608
+ activeMarker.openPopup(); // Optionally open popup
609
+ }
610
+ }
611
+
612
+ function setupWaterLevelControl() {
613
+ const waterLevelInput = document.getElementById('water-level');
614
+ const waterLevelVal = document.getElementById('water-level-val');
615
+ const waterTankFill = document.getElementById('water-tank-fill');
616
+
617
+ waterLevelInput.addEventListener('input', () => {
618
+ const val = waterLevelInput.value;
619
+ waterLevelVal.textContent = val + '%';
620
+ waterTankFill.style.height = val + '%';
621
+
622
+ if (val > 85) {
623
+ waterLevelVal.style.color = 'var(--danger)';
624
+ waterTankFill.style.background = `linear-gradient(to bottom, var(--danger), ${getComputedStyle(document.documentElement).getPropertyValue('--danger').trim()}dd)`;
625
+ } else if (val > 70) {
626
+ waterLevelVal.style.color = 'var(--warning)';
627
+ waterTankFill.style.background = `linear-gradient(to bottom, var(--warning), ${getComputedStyle(document.documentElement).getPropertyValue('--warning').trim()}dd)`;
628
+ } else {
629
+ waterLevelVal.style.color = 'var(--primary)';
630
+ waterTankFill.style.background = 'linear-gradient(to bottom, var(--primary), var(--secondary))';
631
+ }
632
+ });
633
+ waterTankFill.style.height = waterLevelInput.value + '%'; // Initialize
634
+ }
635
+
636
+ function setupRippleEffect() {
637
+ document.querySelectorAll('.btn-ripple').forEach(button => {
638
+ button.addEventListener('click', function(e) {
639
+ const rect = e.target.getBoundingClientRect();
640
+ const x = e.clientX - rect.left;
641
+ const y = e.clientY - rect.top;
642
+
643
+ const ripple = document.createElement('span');
644
+ ripple.classList.add('ripple');
645
+ ripple.style.left = `${x}px`;
646
+ ripple.style.top = `${y}px`;
647
+ this.appendChild(ripple);
648
+ setTimeout(() => ripple.remove(), 600);
649
+ });
650
+ });
651
+ }
652
+
653
+ function showLoading(element) {
654
+ element.innerHTML = `
655
+ <div class="loading-animation">
656
+ <div class="loading-drops">
657
+ <div class="drop"></div><div class="drop"></div><div class="drop"></div><div class="drop"></div><div class="drop"></div>
658
+ </div>
659
+ </div>
660
+ <div class="text-center mt-3 text-muted">Loading data...</div>`;
661
+ }
662
+
663
+ function displayRainfallChart(rainfallData) {
664
+ const container = document.getElementById('rainfall-chart');
665
+ container.innerHTML = '';
666
+ if (!rainfallData || rainfallData.length === 0) {
667
+ container.innerHTML = "<p class='text-center text-muted'>No rainfall data available.</p>";
668
+ document.getElementById('rainfall-visualization').classList.remove('d-none');
669
+ return;
670
+ }
671
+
672
+ const maxRainfall = Math.max(0.1, ...rainfallData.map(day => day.rainfall_mm)); // Avoid division by zero
673
+ const chartHeight = Math.max(50, container.offsetHeight - 40); // Min height 50, reserve 40px for labels
674
+
675
+ rainfallData.forEach((day, index) => {
676
+ const value = day.rainfall_mm;
677
+ const barHeight = value > 0 ? Math.max(2, (value / maxRainfall) * chartHeight) : 2; // Min height 2px for 0 rainfall
678
+
679
+ const barWrapper = document.createElement('div');
680
+ barWrapper.style.position = 'absolute';
681
+ barWrapper.style.left = (index * (100 / rainfallData.length)) + '%';
682
+ barWrapper.style.width = (100 / rainfallData.length - 1) + '%'; // -1 for margin
683
+ barWrapper.style.height = '100%';
684
+ barWrapper.style.display = 'flex';
685
+ barWrapper.style.flexDirection = 'column';
686
+ barWrapper.style.alignItems = 'center';
687
+
688
+
689
+ const bar = document.createElement('div');
690
+ bar.className = 'rainfall-bar animate__animated animate__fadeInUp';
691
+ bar.style.position = 'relative'; // For value label positioning
692
+ bar.style.width = '80%'; // Bar width within its wrapper
693
+ bar.style.height = barHeight + 'px';
694
+ bar.style.animationDelay = (index * 0.05) + 's';
695
+ bar.style.marginLeft = 'auto'; // Center bar in wrapper
696
+ bar.style.marginRight = 'auto';
697
+
698
+
699
+ const valueLabel = document.createElement('div');
700
+ valueLabel.className = 'rainfall-value';
701
+ valueLabel.textContent = value + ' mm';
702
+ bar.appendChild(valueLabel);
703
+
704
+ const dateLabel = document.createElement('div');
705
+ dateLabel.className = 'rainfall-date';
706
+ dateLabel.textContent = day.date.substring(5);
707
+ dateLabel.style.position = 'relative'; // Reset absolute
708
+ dateLabel.style.transform = 'none'; // Reset transform
709
+ dateLabel.style.textAlign = 'center';
710
+ dateLabel.style.width = '100%';
711
+ dateLabel.style.marginTop = '5px'; // Space between bar and date
712
+
713
+
714
+ barWrapper.appendChild(bar);
715
+ barWrapper.appendChild(dateLabel);
716
+ container.appendChild(barWrapper);
717
+ });
718
+ document.getElementById('rainfall-visualization').classList.remove('d-none');
719
+ }
720
+
721
+ function displayOperationalAdvice(adviceText) {
722
+ const adviceContentContainer = document.getElementById('operational-advice-content');
723
+ const statusBadgeContainer = document.getElementById('status-badge');
724
+ adviceContentContainer.innerHTML = '';
725
+ statusBadgeContainer.innerHTML = '';
726
+
727
+ let mutableAdviceText = adviceText.trim();
728
+
729
+ let status = null;
730
+ let actionForBadge = null;
731
+ let actionTextForDisplay = "";
732
+
733
+ const statusBlockMatch = mutableAdviceText.match(/^\*\*Overall Status\*\*\s*(\w+)/i);
734
+ if (statusBlockMatch) {
735
+ status = statusBlockMatch[1].toUpperCase();
736
+ mutableAdviceText = mutableAdviceText.substring(statusBlockMatch[0].length).trim();
737
+ if (mutableAdviceText.startsWith("- ")) {
738
+ mutableAdviceText = mutableAdviceText.substring(2).trim();
739
+ }
740
+ }
741
+
742
+ const actionBlockMatch = mutableAdviceText.match(/^\*\*Action Required\*\*\s*(.*?)(?=\s*-\s*\*\*|$)/i);
743
+ if (actionBlockMatch) {
744
+ actionTextForDisplay = actionBlockMatch[1].trim();
745
+ if (actionTextForDisplay.match(/^(MAINTAIN CURRENT|OPEN GATES|CLOSE GATES|MONITOR CLOSELY)$/i)) {
746
+ actionForBadge = actionTextForDisplay.toUpperCase();
747
+ }
748
+ mutableAdviceText = mutableAdviceText.substring(actionBlockMatch[0].length).trim();
749
+ if (mutableAdviceText.startsWith("- ")) {
750
+ mutableAdviceText = mutableAdviceText.substring(2).trim();
751
+ }
752
+ }
753
+
754
+ if (status) {
755
+ let badgeClass = 'badge-normal';
756
+ if (status === 'WATCH') badgeClass = 'badge-watch';
757
+ if (status === 'WARNING') badgeClass = 'badge-warning';
758
+ if (status === 'EMERGENCY') badgeClass = 'badge-emergency';
759
+
760
+ let iconHtml = '';
761
+ if (status === 'NORMAL') iconHtml = '<i class="fas fa-check-circle me-2"></i>';
762
+ else if (status === 'WATCH') iconHtml = '<i class="fas fa-exclamation-circle me-2"></i>';
763
+ else if (status === 'WARNING') iconHtml = '<i class="fas fa-triangle-exclamation me-2"></i>';
764
+ else if (status === 'EMERGENCY') iconHtml = '<i class="fas fa-radiation me-2"></i>';
765
+
766
+ statusBadgeContainer.innerHTML = `
767
+ <span class="badge-status ${badgeClass}">
768
+ ${iconHtml}
769
+ Status: ${status}
770
+ </span>
771
+ ${actionForBadge ? `<div class="mt-2"><strong>Action:</strong> ${actionForBadge}</div>` : (actionTextForDisplay ? `<div class="mt-2"><strong>Action Required:</strong> ${actionTextForDisplay.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')}</div>` : '')}
772
+ `;
773
+ } else if (actionTextForDisplay) {
774
+ statusBadgeContainer.innerHTML = `
775
+ <div class="mt-2"><strong>Action Required:</strong> ${actionTextForDisplay.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')}</div>
776
+ `;
777
+ }
778
+
779
+ let formattedAdviceHTML = "";
780
+ if (mutableAdviceText) {
781
+ const segments = mutableAdviceText.split(/\s*-\s*(?=\*\*)/);
782
+
783
+ segments.forEach(segment => {
784
+ if (segment.trim() === "") return;
785
+
786
+ const mainPointMatch = segment.match(/^\*\*(.*?)\*\*(.*)/s);
787
+ if (mainPointMatch) {
788
+ let title = mainPointMatch[1].trim();
789
+ if (title.endsWith(":")) title = title.slice(0, -1);
790
+
791
+ let content = mainPointMatch[2].trim();
792
+ if (content.startsWith(":")) content = content.substring(1).trim();
793
+
794
+ formattedAdviceHTML += `<div class="recommendation-item mb-3">`;
795
+ formattedAdviceHTML += ` <h6 class="mb-1"><strong>${title}</strong></h6>`;
796
+
797
+ let hasSubpoints = false;
798
+ const subPointRegex = /\s*-\s*(?=\*\*)/; // Sub-points are separated by " - **"
799
+
800
+ if (content.match(subPointRegex)) {
801
+ hasSubpoints = true;
802
+ }
803
+
804
+ if (hasSubpoints) {
805
+ formattedAdviceHTML += `<ul class="list-unstyled ms-3 mt-1">`;
806
+ // The first part of content before " - **" is part of the main content description, if any
807
+ const firstPartMatch = content.match(/^(.*?)(?=\s*-\s*\*\*)/s);
808
+ if (firstPartMatch && firstPartMatch[1].trim()) {
809
+ formattedAdviceHTML += `<li class="mb-1">${firstPartMatch[1].trim().replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>')}</li>`;
810
+ }
811
+
812
+ const subSegments = content.split(subPointRegex);
813
+ subSegments.forEach((subSegmentData, idx) => {
814
+ // Skip the first part if it was handled above, or if it's empty
815
+ if (idx === 0 && firstPartMatch && firstPartMatch[1].trim()) return;
816
+ if (idx === 0 && (!firstPartMatch || !firstPartMatch[1].trim()) && !subSegmentData.trim().startsWith("**")) return;
817
+
818
+
819
+ let currentSubSegment = subSegmentData.trim();
820
+ if (currentSubSegment === "") return;
821
+
822
+ const subPointMatch = currentSubSegment.match(/^\*\*(.*?)\*\*(.*)/s);
823
+ if (subPointMatch) {
824
+ let subTitle = subPointMatch[1].trim();
825
+ if (subTitle.endsWith(":")) subTitle = subTitle.slice(0, -1);
826
+ let subDetails = subPointMatch[2].trim();
827
+ if (subDetails.startsWith(":")) subDetails = subDetails.substring(1).trim();
828
+
829
+ formattedAdviceHTML += `<li class="mb-1"><strong>${subTitle}:</strong> ${subDetails.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>')}</li>`;
830
+ } else {
831
+ formattedAdviceHTML += `<li class="mb-1">${currentSubSegment.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>')}</li>`;
832
+ }
833
+ });
834
+ formattedAdviceHTML += `</ul>`;
835
+ } else {
836
+ formattedAdviceHTML += ` <div class="ms-3 mt-1">${content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>')}</div>`;
837
+ }
838
+ formattedAdviceHTML += `</div>`;
839
+ } else {
840
+ formattedAdviceHTML += `<p class="mb-2">${segment.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>')}</p>`;
841
+ }
842
+ });
843
+ } else if (!status && !actionTextForDisplay) {
844
+ formattedAdviceHTML = "<p class='text-muted text-center py-3'>No specific operational recommendations available.</p>";
845
+ } else if (mutableAdviceText.trim() === "" && (status || actionTextForDisplay)) {
846
+ formattedAdviceHTML = "<p class='text-muted text-center py-3'>Refer to status and action above. No further detailed recommendations.</p>";
847
+ }
848
+
849
+ adviceContentContainer.innerHTML = formattedAdviceHTML;
850
+ document.getElementById('operation-advice-card').classList.remove('d-none');
851
+ }
852
+
853
+
854
+ function displayDamInfo(data) {
855
+ const infoContainer = document.getElementById('dam-info-box');
856
+ let infoHTML = `
857
+ <div class="animate__animated animate__fadeIn">
858
+ <table class="table table-sm table-borderless table-hover">
859
+ <tbody>
860
+ <tr><th>Capacity</th><td>${data.dam_info.capacity_mcm || 'N/A'} MCM</td></tr>
861
+ <tr><th>Gates</th><td>${data.dam_info.number_of_spillway_gates || 'N/A'} (${data.dam_info.gate_type || 'N/A'})</td></tr>
862
+ <tr><th>Height</th><td>${data.dam_info.dam_height_meters || 'N/A'} m</td></tr>
863
+ <tr><th>Purpose</th><td>${data.dam_info.purpose || 'N/A'}</td></tr>
864
+ <tr><th>River</th><td>${data.dam_info.river_name || 'N/A'}</td></tr>
865
+ <tr><th>Country</th><td>${data.dam_info.country || 'N/A'}</td></tr>
866
+ </tbody>
867
+ </table>
868
+ </div>`;
869
+ infoContainer.innerHTML = infoHTML;
870
+
871
+ displayRainfallChart(data.rainfall_forecast);
872
+ displayOperationalAdvice(data.operational_advice);
873
+ }
874
+
875
+ async function fetchDamDetails() {
876
+ const damSelect = document.getElementById('dam-select');
877
+ const damName = damSelect.value;
878
+
879
+ if (!damName) {
880
+ showToast("Please select a dam first.");
881
+ return;
882
+ }
883
+
884
+ const selectedOption = damSelect.selectedOptions[0];
885
+ const lat = selectedOption.getAttribute('data-lat');
886
+ const lon = selectedOption.getAttribute('data-lon');
887
+ const current_level = document.getElementById('water-level').value;
888
+
889
+ const damInfoBox = document.getElementById('dam-info-box');
890
+ const opAdviceContent = document.getElementById('operational-advice-content');
891
+ const rainfallChart = document.getElementById('rainfall-chart');
892
+
893
+ showLoading(damInfoBox);
894
+ opAdviceContent.innerHTML = '<div class="text-center text-muted py-3">Loading recommendations...</div>'; // Placeholder
895
+ rainfallChart.innerHTML = '<div class="text-center text-muted py-3">Loading forecast...</div>'; // Placeholder
896
+
897
+
898
+ document.getElementById('rainfall-visualization').classList.add('d-none');
899
+ document.getElementById('operation-advice-card').classList.add('d-none');
900
+
901
+ try {
902
+ const res = await fetch('/dam_details', { // Replace with your actual endpoint
903
+ method: 'POST',
904
+ headers: {'Content-Type': 'application/json'},
905
+ body: JSON.stringify({
906
+ dam_name: damName,
907
+ latitude: lat,
908
+ longitude: lon,
909
+ current_water_level: current_level
910
+ })
911
+ });
912
+
913
+ if (!res.ok) {
914
+ const errorData = await res.json().catch(() => ({error: "Server error with no JSON response"}));
915
+ throw new Error(errorData.error || `HTTP error! status: ${res.status}`);
916
+ }
917
+
918
+ const data = await res.json();
919
+
920
+ if (data.error) {
921
+ damInfoBox.innerHTML = `<div class="alert alert-danger m-2"><i class="fas fa-triangle-exclamation me-2"></i> Error: ${data.error}</div>`;
922
+ opAdviceContent.innerHTML = '';
923
+ rainfallChart.innerHTML = '';
924
+ return;
925
+ }
926
+ displayDamInfo(data);
927
+
928
+ } catch (err) {
929
+ console.error("Fetch error:", err);
930
+ damInfoBox.innerHTML = `<div class="alert alert-danger m-2"><i class="fas fa-triangle-exclamation me-2"></i> Fetch error: ${err.message || err}</div>`;
931
+ opAdviceContent.innerHTML = '';
932
+ rainfallChart.innerHTML = '';
933
+ // Optionally hide the cards again on error
934
+ document.getElementById('rainfall-visualization').classList.add('d-none');
935
+ document.getElementById('operation-advice-card').classList.add('d-none');
936
+ }
937
+ }
938
+
939
+ function showToast(message) {
940
+ let toastContainer = document.querySelector('.toast-container');
941
+ if (!toastContainer) {
942
+ toastContainer = document.createElement('div');
943
+ toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
944
+ document.body.appendChild(toastContainer);
945
+ }
946
+
947
+ const toastId = `toast-${Date.now()}`;
948
+ const toastHTML = `
949
+ <div id="${toastId}" class="toast show animate__animated animate__fadeInRight" role="alert" aria-live="assertive" aria-atomic="true">
950
+ <div class="toast-header bg-primary text-white">
951
+ <i class="fas fa-info-circle me-2"></i>
952
+ <strong class="me-auto">Dam Simulator</strong>
953
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
954
+ </div>
955
+ <div class="toast-body">${message}</div>
956
+ </div>`;
957
+ toastContainer.insertAdjacentHTML('beforeend', toastHTML);
958
+
959
+ const toastElement = document.getElementById(toastId);
960
+ const bsToast = new bootstrap.Toast(toastElement, { delay: 5000 });
961
+ bsToast.show();
962
+ toastElement.addEventListener('hidden.bs.toast', () => toastElement.remove());
963
+ }
964
+
965
+ document.addEventListener('DOMContentLoaded', function() {
966
+ markers = initMap(); // Initialize map and global markers
967
+ setupWaterLevelControl();
968
+ setupRippleEffect();
969
+
970
+ document.getElementById('dam-select').addEventListener('change', (e) => {
971
+ const selectedOption = e.target.selectedOptions[0];
972
+ const lat = selectedOption.getAttribute('data-lat');
973
+ const lon = selectedOption.getAttribute('data-lon');
974
+ if (lat && lon) {
975
+ centerMapToDam(parseFloat(lat), parseFloat(lon), e.target.value);
976
+ }
977
+ });
978
+
979
+ document.getElementById('get-info-btn').addEventListener('click', fetchDamDetails);
980
+
981
+ // Example: Populate dams if not using server-side template
982
+ // This is for frontend-only testing if `{{ dams|tojson }}` is not available
983
+ if (dams.length === 0 && typeof {{ dams|tojson }} === 'undefined') {
984
+ const damSelect = document.getElementById('dam-select');
985
+ const sampleDams = [
986
+ { dam_name: "Sample Dam 1", latitude: 30.0, longitude: 70.0 },
987
+ { dam_name: "Sample Dam 2", latitude: 35.0, longitude: 75.0 }
988
+ ];
989
+ sampleDams.forEach(dam => {
990
+ const option = document.createElement('option');
991
+ option.value = dam.dam_name;
992
+ option.textContent = dam.dam_name;
993
+ option.setAttribute('data-lat', dam.latitude);
994
+ option.setAttribute('data-lon', dam.longitude);
995
+ damSelect.appendChild(option);
996
+ // also add to JS dams array for map markers
997
+ dams.push(dam);
998
+ });
999
+ markers = initMap(); // Re-init map if dams were added late
1000
+ }
1001
+ });
1002
+ </script>
1003
+ </body>
1004
+ </html>