Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- app.py +119 -0
- dam.csv +18 -0
- requirements.txt +5 -0
- 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>
|