adema5051 commited on
Commit
95cb050
·
verified ·
1 Parent(s): e3671cf

Upload 8 files

Browse files
api/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """
2
+ API module for Flood Vulnerability Assessment
3
+ """
4
+ from .models import SingleAssessment
5
+ from .batch import process_single_row, process_single_row_multihazard
6
+
7
+ __all__ = ['SingleAssessment', 'process_single_row', 'process_single_row_multihazard']
api/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (471 Bytes). View file
 
api/__pycache__/batch.cpython-311.pyc ADDED
Binary file (7.59 kB). View file
 
api/__pycache__/models.cpython-311.pyc ADDED
Binary file (2.2 kB). View file
 
api/batch.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Batch processing functions for CSV file assessments
3
+ """
4
+ from spatial_queries import get_terrain_metrics, distance_to_water
5
+ from vulnerability import calculate_vulnerability_index, calculate_multi_hazard_vulnerability
6
+ from height_predictor.inference import get_predictor
7
+ from height_predictor.get_height_gba import GlobalBuildingAtlasHeight
8
+
9
+ # Initialize GBA getter (singleton pattern)
10
+ gba_getter = GlobalBuildingAtlasHeight()
11
+
12
+
13
+ def process_single_row(row, use_predicted_height=False, use_gba_height=False):
14
+ """Process a single row from CSV - used for parallel processing."""
15
+ try:
16
+ lat = row['latitude']
17
+ lon = row['longitude']
18
+ height = row.get('height', 0.0)
19
+ basement = row.get('basement', 0.0)
20
+
21
+ if use_gba_height:
22
+ try:
23
+ result = gba_getter.get_height_m(lat, lon, buffer_m=5.0)
24
+ if result.get('status') == 'success' and result.get('predicted_height') is not None:
25
+ h = result['predicted_height']
26
+ if h >= 0: # Only use valid positive heights
27
+ height = h
28
+ except Exception as e:
29
+ print(f"GBA height failed for {lat},{lon}: {e}")
30
+ elif use_predicted_height:
31
+ try:
32
+ predictor = get_predictor()
33
+ pred = predictor.predict_from_coordinates(lat, lon)
34
+ if pred['status'] == 'success' and pred['predicted_height'] is not None:
35
+ height = pred['predicted_height']
36
+ except Exception as e:
37
+ print(f"Height prediction failed for {lat},{lon}: {e}")
38
+
39
+ terrain = get_terrain_metrics(lat, lon)
40
+ water_dist = distance_to_water(lat, lon)
41
+
42
+ result = calculate_vulnerability_index(
43
+ lat=lat,
44
+ lon=lon,
45
+ height=height,
46
+ basement=basement,
47
+ terrain_metrics=terrain,
48
+ water_distance=water_dist
49
+ )
50
+
51
+ # CSV output - essential columns
52
+ return {
53
+ 'latitude': lat,
54
+ 'longitude': lon,
55
+ 'height': height,
56
+ 'basement': basement,
57
+ 'vulnerability_index': result['vulnerability_index'],
58
+ 'ci_lower_95': result['confidence_interval']['lower_bound_95'],
59
+ 'ci_upper_95': result['confidence_interval']['upper_bound_95'],
60
+ 'vulnerability_level': result['risk_level'],
61
+ 'confidence': result['uncertainty_analysis']['confidence'],
62
+ 'confidence_interpretation': result['uncertainty_analysis']['interpretation'],
63
+ 'elevation_m': result['elevation_m'],
64
+ 'tpi_m': result['relative_elevation_m'],
65
+ 'slope_degrees': result['slope_degrees'],
66
+ 'distance_to_water_m': result['distance_to_water_m'],
67
+ 'quality_flags': ','.join(result['uncertainty_analysis']['data_quality_flags']) if result['uncertainty_analysis']['data_quality_flags'] else ''
68
+ }
69
+
70
+ except Exception as e:
71
+ return {
72
+ 'latitude': row.get('latitude'),
73
+ 'longitude': row.get('longitude'),
74
+ 'height': row.get('height', 0.0),
75
+ 'basement': row.get('basement', 0.0),
76
+ 'error': str(e),
77
+ 'vulnerability_index': None,
78
+ 'ci_lower_95': None,
79
+ 'ci_upper_95': None,
80
+ 'risk_level': None,
81
+ 'confidence': None,
82
+ 'confidence_interpretation': None,
83
+ 'elevation_m': None,
84
+ 'tpi_m': None,
85
+ 'slope_degrees': None,
86
+ 'distance_to_water_m': None,
87
+ 'quality_flags': ''
88
+ }
89
+
90
+
91
+ def process_single_row_multihazard(row, use_predicted_height=False, use_gba_height=False):
92
+ """Process a single row with multi-hazard assessment."""
93
+ try:
94
+ lat = row['latitude']
95
+ lon = row['longitude']
96
+ height = row.get('height', 0.0)
97
+ basement = row.get('basement', 0.0)
98
+
99
+ if use_gba_height:
100
+ try:
101
+ result = gba_getter.get_height_m(lat, lon, buffer_m=5.0)
102
+ if result.get('status') == 'success' and result.get('predicted_height') is not None:
103
+ h = result['predicted_height']
104
+ if h >= 0: # Only use valid positive heights
105
+ height = h
106
+ except Exception as e:
107
+ print(f"GBA height failed for {lat},{lon}: {e}")
108
+ elif use_predicted_height:
109
+ try:
110
+ predictor = get_predictor()
111
+ pred = predictor.predict_from_coordinates(lat, lon)
112
+ if pred['status'] == 'success' and pred['predicted_height'] is not None:
113
+ height = pred['predicted_height']
114
+ except Exception as e:
115
+ print(f"Height prediction failed for {lat},{lon}: {e}")
116
+
117
+ terrain = get_terrain_metrics(lat, lon)
118
+ water_dist = distance_to_water(lat, lon)
119
+
120
+ result = calculate_multi_hazard_vulnerability(
121
+ lat=lat,
122
+ lon=lon,
123
+ height=height,
124
+ basement=basement,
125
+ terrain_metrics=terrain,
126
+ water_distance=water_dist
127
+ )
128
+
129
+ return {
130
+ 'latitude': lat,
131
+ 'longitude': lon,
132
+ 'height': height,
133
+ 'basement': basement,
134
+ 'vulnerability_index': result['vulnerability_index'],
135
+ 'ci_lower_95': result['confidence_interval']['lower_bound_95'],
136
+ 'ci_upper_95': result['confidence_interval']['upper_bound_95'],
137
+ 'vulnerability_level': result['risk_level'],
138
+ 'confidence': result['uncertainty_analysis']['confidence'],
139
+ 'confidence_interpretation': result['uncertainty_analysis']['interpretation'],
140
+ 'elevation_m': result['elevation_m'],
141
+ 'tpi_m': result['relative_elevation_m'],
142
+ 'slope_degrees': result['slope_degrees'],
143
+ 'distance_to_water_m': result['distance_to_water_m'],
144
+ 'dominant_hazard': result['dominant_hazard'],
145
+ 'fluvial_risk': result['hazard_breakdown']['fluvial_riverine'],
146
+ 'coastal_risk': result['hazard_breakdown']['coastal_surge'],
147
+ 'pluvial_risk': result['hazard_breakdown']['pluvial_drainage'],
148
+ 'combined_risk': result['hazard_breakdown']['combined_index'],
149
+ 'quality_flags': ','.join(result['uncertainty_analysis']['data_quality_flags'])
150
+ if result['uncertainty_analysis']['data_quality_flags'] else ''
151
+ }
152
+
153
+ except Exception as e:
154
+ return {
155
+ 'latitude': row.get('latitude'),
156
+ 'longitude': row.get('longitude'),
157
+ 'height': row.get('height', 0.0),
158
+ 'basement': row.get('basement', 0.0),
159
+ 'error': str(e),
160
+ 'vulnerability_index': None
161
+ }
api/models.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic models for API request/response validation
3
+ """
4
+ from pydantic import BaseModel, field_validator
5
+ from typing import Optional
6
+
7
+
8
+ class SingleAssessment(BaseModel):
9
+ """Model for single location vulnerability assessment request"""
10
+ latitude: float
11
+ longitude: float
12
+ height: Optional[float] = 0.0
13
+ basement: Optional[float] = 0.0
14
+
15
+ @field_validator('latitude')
16
+ @classmethod
17
+ def check_lat(cls, v: float) -> float:
18
+ if not -90 <= v <= 90:
19
+ raise ValueError('Latitude must be between -90 and 90')
20
+ return v
21
+
22
+ @field_validator('longitude')
23
+ @classmethod
24
+ def check_lon(cls, v: float) -> float:
25
+ if not -180 <= v <= 180:
26
+ raise ValueError('Longitude must be between -180 and 180')
27
+ return v
28
+
29
+ @field_validator('basement')
30
+ @classmethod
31
+ def check_basement(cls, v: float) -> float:
32
+ if v > 0:
33
+ raise ValueError('Basement height must be 0 or negative (e.g., -1, -2, -3)')
34
+ return v
static/css/styles.css ADDED
@@ -0,0 +1,1042 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ html,
8
+ body {
9
+ height: 100%;
10
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11
+ }
12
+
13
+ body {
14
+ background: #000;
15
+ color: #fff;
16
+ position: relative;
17
+ overflow-x: hidden;
18
+ }
19
+
20
+ /* Animated background */
21
+ .hero-background {
22
+ position: fixed;
23
+ top: 0;
24
+ left: 0;
25
+ width: 100%;
26
+ height: 100%;
27
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
28
+ z-index: 0;
29
+ }
30
+
31
+ .hero-background::before {
32
+ content: '';
33
+ position: absolute;
34
+ top: 0;
35
+ left: 0;
36
+ width: 100%;
37
+ height: 100%;
38
+ background:
39
+ radial-gradient(circle at 20% 50%, rgba(59, 130, 246, 0.15) 0%, transparent 50%),
40
+ radial-gradient(circle at 80% 50%, rgba(139, 92, 246, 0.15) 0%, transparent 50%);
41
+ animation: pulse 8s ease-in-out infinite;
42
+ }
43
+
44
+ @keyframes pulse {
45
+
46
+ 0%,
47
+ 100% {
48
+ opacity: 1;
49
+ }
50
+
51
+ 50% {
52
+ opacity: 0.5;
53
+ }
54
+ }
55
+
56
+ .page-wrapper {
57
+ position: relative;
58
+ z-index: 1;
59
+ min-height: 100vh;
60
+ }
61
+
62
+ /* Header with hero section */
63
+ .hero-header {
64
+ position: relative;
65
+ padding: 4rem 2rem;
66
+ text-align: center;
67
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0.9) 0%, rgba(15, 23, 42, 0.7) 100%);
68
+ border-bottom: 2px solid rgba(59, 130, 246, 0.3);
69
+ }
70
+
71
+ .hero-header h1 {
72
+ font-size: 3.5em;
73
+ font-weight: 800;
74
+ margin-bottom: 1rem;
75
+ background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 50%, #60a5fa 100%);
76
+ -webkit-background-clip: text;
77
+ -webkit-text-fill-color: transparent;
78
+ background-clip: text;
79
+ background-size: 200% auto;
80
+ animation: shine 3s linear infinite;
81
+ }
82
+
83
+ @keyframes shine {
84
+ to {
85
+ background-position: 200% center;
86
+ }
87
+ }
88
+
89
+ .hero-header .subtitle {
90
+ font-size: 1.3em;
91
+ color: #94a3b8;
92
+ font-weight: 300;
93
+ max-width: 700px;
94
+ margin: 0 auto;
95
+ }
96
+
97
+ /* Side navigation */
98
+ .main-container {
99
+ display: flex;
100
+ max-width: 1400px;
101
+ margin: 0 auto;
102
+ padding: 2rem;
103
+ gap: 2rem;
104
+ }
105
+
106
+ .side-nav {
107
+ width: 280px;
108
+ flex-shrink: 0;
109
+ position: sticky;
110
+ top: 2rem;
111
+ height: fit-content;
112
+ }
113
+
114
+ .nav-card {
115
+ background: rgba(30, 41, 59, 0.8);
116
+ backdrop-filter: blur(20px);
117
+ border-radius: 20px;
118
+ padding: 1.5rem;
119
+ border: 1px solid rgba(59, 130, 246, 0.2);
120
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
121
+ }
122
+
123
+ .nav-card h3 {
124
+ color: #e2e8f0;
125
+ font-size: 1.1em;
126
+ margin-bottom: 1.5rem;
127
+ padding-bottom: 1rem;
128
+ border-bottom: 1px solid rgba(59, 130, 246, 0.2);
129
+ }
130
+
131
+ .nav-link {
132
+ display: block;
133
+ padding: 1rem 1.25rem;
134
+ margin-bottom: 0.5rem;
135
+ background: transparent;
136
+ border: none;
137
+ color: #94a3b8;
138
+ text-align: left;
139
+ cursor: pointer;
140
+ border-radius: 12px;
141
+ font-size: 0.95em;
142
+ transition: all 0.3s ease;
143
+ position: relative;
144
+ overflow: hidden;
145
+ width: 100%;
146
+ }
147
+
148
+ .nav-link::before {
149
+ content: '';
150
+ position: absolute;
151
+ left: 0;
152
+ top: 0;
153
+ height: 100%;
154
+ width: 3px;
155
+ background: linear-gradient(180deg, #3b82f6, #8b5cf6);
156
+ transform: scaleY(0);
157
+ transition: transform 0.3s ease;
158
+ }
159
+
160
+ .nav-link:hover {
161
+ background: rgba(59, 130, 246, 0.1);
162
+ color: #e2e8f0;
163
+ transform: translateX(5px);
164
+ }
165
+
166
+ .nav-link.active {
167
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2));
168
+ color: #fff;
169
+ font-weight: 600;
170
+ }
171
+
172
+ .nav-link.active::before {
173
+ transform: scaleY(1);
174
+ }
175
+
176
+ /* Content area */
177
+ .content-area {
178
+ flex: 1;
179
+ min-width: 0;
180
+ }
181
+
182
+ .assessment-card {
183
+ display: none;
184
+ background: rgba(30, 41, 59, 0.8);
185
+ backdrop-filter: blur(20px);
186
+ border-radius: 20px;
187
+ padding: 3rem;
188
+ border: 1px solid rgba(59, 130, 246, 0.2);
189
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
190
+ animation: slideIn 0.4s ease;
191
+ }
192
+
193
+ .assessment-card.active {
194
+ display: block;
195
+ }
196
+
197
+ @keyframes slideIn {
198
+ from {
199
+ opacity: 0;
200
+ transform: translateY(20px);
201
+ }
202
+
203
+ to {
204
+ opacity: 1;
205
+ transform: translateY(0);
206
+ }
207
+ }
208
+
209
+ .card-header {
210
+ margin-bottom: 2rem;
211
+ }
212
+
213
+ .card-header h2 {
214
+ font-size: 2em;
215
+ color: #e2e8f0;
216
+ margin-bottom: 0.5rem;
217
+ }
218
+
219
+ .card-header p {
220
+ color: #94a3b8;
221
+ font-size: 1.05em;
222
+ }
223
+
224
+ /* Form styling */
225
+ .form-grid {
226
+ display: grid;
227
+ grid-template-columns: repeat(2, 1fr);
228
+ gap: 1.5rem;
229
+ margin-bottom: 2rem;
230
+ }
231
+
232
+ .input-card {
233
+ background: rgba(15, 23, 42, 0.6);
234
+ border: 1px solid rgba(59, 130, 246, 0.2);
235
+ border-radius: 16px;
236
+ padding: 1.5rem;
237
+ transition: all 0.3s ease;
238
+ }
239
+
240
+ .input-card:hover {
241
+ border-color: rgba(59, 130, 246, 0.4);
242
+ background: rgba(15, 23, 42, 0.8);
243
+ transform: translateY(-2px);
244
+ }
245
+
246
+ .input-card label {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 0.5rem;
250
+ margin-bottom: 0.75rem;
251
+ color: #cbd5e1;
252
+ font-weight: 600;
253
+ font-size: 0.95em;
254
+ }
255
+
256
+ .input-card label::before {
257
+ content: '';
258
+ width: 4px;
259
+ height: 16px;
260
+ background: linear-gradient(180deg, #3b82f6, #8b5cf6);
261
+ border-radius: 2px;
262
+ }
263
+
264
+ .input-card input,
265
+ .input-card select {
266
+ width: 100%;
267
+ padding: 0.875rem;
268
+ background: rgba(0, 0, 0, 0.3);
269
+ border: 1px solid rgba(148, 163, 184, 0.2);
270
+ border-radius: 10px;
271
+ color: #e2e8f0;
272
+ font-size: 1em;
273
+ transition: all 0.3s ease;
274
+ }
275
+
276
+ .input-card input:focus,
277
+ .input-card select:focus {
278
+ outline: none;
279
+ border-color: #3b82f6;
280
+ background: rgba(0, 0, 0, 0.5);
281
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
282
+ }
283
+
284
+ .input-card .helper-text {
285
+ margin-top: 0.5rem;
286
+ font-size: 0.8em;
287
+ color: #64748b;
288
+ font-style: italic;
289
+ }
290
+
291
+ /* Height group with predict button */
292
+ .height-group {
293
+ display: flex;
294
+ align-items: stretch;
295
+ gap: 0;
296
+ background: rgba(0, 0, 0, 0.3);
297
+ border: 1px solid rgba(148, 163, 184, 0.2);
298
+ border-radius: 10px;
299
+ overflow: visible;
300
+ transition: all 0.3s ease;
301
+ }
302
+
303
+ .height-group input {
304
+ border-radius: 10px 0 0 10px;
305
+ }
306
+
307
+ .height-group button:last-child {
308
+ border-radius: 0 10px 10px 0;
309
+ }
310
+
311
+ .input-card {
312
+ overflow: visible;
313
+ }
314
+
315
+ .height-group:focus-within {
316
+ border-color: #3b82f6;
317
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
318
+ }
319
+
320
+ .height-group input {
321
+ flex: 1;
322
+ padding: 0.875rem;
323
+ background: transparent !important;
324
+ border: none !important;
325
+ border-radius: 0 !important;
326
+ color: #e2e8f0;
327
+ font-size: 1em;
328
+ box-shadow: none !important;
329
+ }
330
+
331
+ .height-group input:focus {
332
+ outline: none;
333
+ }
334
+
335
+ .height-group button {
336
+ width: auto !important;
337
+ padding: 0.875rem 1.5rem !important;
338
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6) !important;
339
+ color: white;
340
+ border: none;
341
+ border-left: 1px solid rgba(148, 163, 184, 0.3);
342
+ cursor: pointer;
343
+ font-size: 0.9em;
344
+ font-weight: 600;
345
+ white-space: nowrap;
346
+ transition: opacity 0.2s ease;
347
+ border-radius: 0 10px 10px 0 !important;
348
+ transform: none !important;
349
+ }
350
+
351
+ .height-group button:hover {
352
+ opacity: 0.9;
353
+ transform: none !important;
354
+ }
355
+
356
+ .height-group button:disabled {
357
+ opacity: 0.5;
358
+ cursor: not-allowed;
359
+ }
360
+
361
+ .gba-height-btn {
362
+ margin-top: 0.4rem;
363
+ align-self: flex-start;
364
+
365
+ background: rgba(96, 165, 250, 0.08);
366
+ border: 1px solid rgba(96, 165, 250, 0.25);
367
+ border-radius: 8px;
368
+
369
+ padding: 0.25rem 0.6rem;
370
+ font-size: 0.7em;
371
+ font-weight: 500;
372
+
373
+ color: #93c5fd;
374
+ cursor: pointer;
375
+
376
+ transition: background 0.2s ease, border 0.2s ease, color 0.2s ease;
377
+ }
378
+
379
+ .secondary-btn-style {
380
+ margin-top: 0.4rem;
381
+ align-self: flex-start;
382
+
383
+ background: rgba(96, 165, 250, 0.08);
384
+ border: 1px solid rgba(96, 165, 250, 0.25);
385
+ border-radius: 8px;
386
+
387
+ padding: 0.25rem 0.6rem;
388
+ font-size: 0.7em;
389
+ font-weight: 500;
390
+
391
+ color: #93c5fd;
392
+ cursor: pointer;
393
+
394
+ transition: background 0.2s ease, border 0.2s ease, color 0.2s ease;
395
+ }
396
+
397
+ .gba-height-btn:hover,
398
+ .secondary-btn-style:hover {
399
+ background: rgba(96, 165, 250, 0.18);
400
+ border-color: rgba(96, 165, 250, 0.5);
401
+ color: #bfdbfe;
402
+ }
403
+
404
+
405
+ .gba-height-btn:disabled,
406
+ .secondary-btn-style:disabled {
407
+ opacity: 0.4;
408
+ cursor: not-allowed;
409
+ }
410
+
411
+
412
+ .height-group .gba-height-btn {
413
+ margin-top: 0;
414
+ align-self: auto;
415
+ border-radius: 0;
416
+ border-left: 1px solid rgba(148, 163, 184, 0.3);
417
+ height: auto;
418
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
419
+ color: white;
420
+ border: none;
421
+ padding: 0.875rem 1.5rem;
422
+ }
423
+
424
+ .height-group .gba-height-btn:hover {
425
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
426
+ opacity: 0.9;
427
+ color: white;
428
+ border-color: transparent;
429
+ }
430
+
431
+ /* Custom tooltip styling for buttons */
432
+ .tooltip-btn {
433
+ position: relative;
434
+ }
435
+
436
+ .tooltip-btn::after {
437
+ content: attr(data-tooltip);
438
+ position: absolute;
439
+ bottom: calc(100% + 10px);
440
+ left: 50%;
441
+ transform: translateX(-50%);
442
+ padding: 10px 14px;
443
+ background: rgba(15, 23, 42, 0.95);
444
+ color: #e2e8f0;
445
+ font-size: 1em;
446
+ font-weight: 400;
447
+ border-radius: 8px;
448
+ border: 1px solid rgba(59, 130, 246, 0.3);
449
+ white-space: normal;
450
+ width: max-content;
451
+ max-width: 280px;
452
+ text-align: center;
453
+ opacity: 0;
454
+ visibility: hidden;
455
+ transition: opacity 0.2s ease, visibility 0.2s ease;
456
+ z-index: 1000;
457
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
458
+ pointer-events: none;
459
+ line-height: 1.4;
460
+ }
461
+
462
+ .tooltip-btn::before {
463
+ content: '';
464
+ position: absolute;
465
+ bottom: calc(100% + 2px);
466
+ left: 50%;
467
+ transform: translateX(-50%);
468
+ border-width: 8px;
469
+ border-style: solid;
470
+ border-color: rgba(15, 23, 42, 0.95) transparent transparent transparent;
471
+ opacity: 0;
472
+ visibility: hidden;
473
+ transition: opacity 0.2s ease, visibility 0.2s ease;
474
+ z-index: 1001;
475
+ }
476
+
477
+ .tooltip-btn:hover::after,
478
+ .tooltip-btn:hover::before {
479
+ opacity: 1;
480
+ visibility: visible;
481
+ }
482
+
483
+ /* Checkbox styling */
484
+ .checkbox-row {
485
+ display: flex;
486
+ align-items: center;
487
+ gap: 0.75rem;
488
+ padding: 1rem;
489
+ background: rgba(15, 23, 42, 0.6);
490
+ border: 1px solid rgba(59, 130, 246, 0.2);
491
+ border-radius: 12px;
492
+ cursor: pointer;
493
+ transition: all 0.3s ease;
494
+ }
495
+
496
+ .checkbox-row:hover {
497
+ background: rgba(15, 23, 42, 0.8);
498
+ border-color: rgba(59, 130, 246, 0.4);
499
+ }
500
+
501
+ .checkbox-row input[type="checkbox"] {
502
+ width: 20px;
503
+ height: 20px;
504
+ cursor: pointer;
505
+ accent-color: #3b82f6;
506
+ }
507
+
508
+ .checkbox-row label {
509
+ margin: 0 !important;
510
+ color: #cbd5e1;
511
+ font-weight: 500;
512
+ cursor: pointer;
513
+ flex: 1;
514
+ }
515
+
516
+ .checkbox-row label::before {
517
+ display: none;
518
+ }
519
+
520
+ /* Action buttons */
521
+ .action-section {
522
+ margin-top: 2rem;
523
+ padding-top: 2rem;
524
+ border-top: 1px solid rgba(59, 130, 246, 0.2);
525
+ }
526
+
527
+ .primary-button {
528
+ position: relative;
529
+ width: 100%;
530
+ padding: 1.25rem 2rem;
531
+ background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
532
+ color: white;
533
+ border: none;
534
+ border-radius: 12px;
535
+ font-size: 1.1em;
536
+ font-weight: 700;
537
+ cursor: pointer;
538
+ overflow: hidden;
539
+ transition: all 0.3s ease;
540
+ }
541
+
542
+ .primary-button::before {
543
+ content: '';
544
+ position: absolute;
545
+ top: 0;
546
+ left: -100%;
547
+ width: 100%;
548
+ height: 100%;
549
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
550
+ transition: left 0.5s;
551
+ }
552
+
553
+ .primary-button:hover {
554
+ transform: translateY(-3px);
555
+ box-shadow: 0 10px 30px rgba(59, 130, 246, 0.4);
556
+ }
557
+
558
+ .primary-button:hover::before {
559
+ left: 100%;
560
+ }
561
+
562
+ .primary-button:active {
563
+ transform: translateY(-1px);
564
+ }
565
+
566
+ .primary-button:disabled {
567
+ background: #334155;
568
+ cursor: not-allowed;
569
+ transform: none;
570
+ }
571
+
572
+ /* Loading state */
573
+ .loading-state {
574
+ display: none;
575
+ text-align: center;
576
+ padding: 3rem;
577
+ }
578
+
579
+ .loading-spinner {
580
+ width: 60px;
581
+ height: 60px;
582
+ margin: 0 auto 1rem;
583
+ border: 4px solid rgba(59, 130, 246, 0.2);
584
+ border-top-color: #3b82f6;
585
+ border-radius: 50%;
586
+ animation: spin 1s linear infinite;
587
+ }
588
+
589
+ @keyframes spin {
590
+ to {
591
+ transform: rotate(360deg);
592
+ }
593
+ }
594
+
595
+ .loading-state p {
596
+ color: #94a3b8;
597
+ font-size: 1.1em;
598
+ }
599
+
600
+ /* Results section */
601
+ .results-section {
602
+ display: none;
603
+ margin-top: 2rem;
604
+ }
605
+
606
+ .results-header {
607
+ text-align: center;
608
+ padding: 2rem;
609
+ margin-bottom: 2rem;
610
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(139, 92, 246, 0.15));
611
+ border-radius: 16px;
612
+ border: 1px solid rgba(59, 130, 246, 0.3);
613
+ }
614
+
615
+ .results-header h2 {
616
+ font-size: 2em;
617
+ margin-bottom: 1rem;
618
+ color: #e2e8f0;
619
+ }
620
+
621
+ .risk-badge {
622
+ display: inline-block;
623
+ padding: 0.75rem 2rem;
624
+ border-radius: 30px;
625
+ font-weight: 700;
626
+ font-size: 1.2em;
627
+ text-transform: uppercase;
628
+ letter-spacing: 1px;
629
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
630
+ }
631
+
632
+ .risk-very-high {
633
+ background: linear-gradient(135deg, #dc2626, #b91c1c);
634
+ box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
635
+ }
636
+
637
+ .risk-high {
638
+ background: linear-gradient(135deg, #ea580c, #c2410c);
639
+ box-shadow: 0 4px 15px rgba(234, 88, 12, 0.4);
640
+ }
641
+
642
+ .risk-moderate {
643
+ background: linear-gradient(135deg, #ca8a04, #a16207);
644
+ box-shadow: 0 4px 15px rgba(202, 138, 4, 0.4);
645
+ }
646
+
647
+ .risk-low {
648
+ background: linear-gradient(135deg, #16a34a, #15803d);
649
+ box-shadow: 0 4px 15px rgba(22, 163, 74, 0.4);
650
+ }
651
+
652
+ .risk-very-low {
653
+ background: linear-gradient(135deg, #0891b2, #0e7490);
654
+ box-shadow: 0 4px 15px rgba(8, 145, 178, 0.4);
655
+ }
656
+
657
+ /* Stats grid */
658
+ .stats-grid {
659
+ display: grid;
660
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
661
+ gap: 1.5rem;
662
+ margin-bottom: 2rem;
663
+ }
664
+
665
+ .stat-card {
666
+ background: rgba(15, 23, 42, 0.6);
667
+ border: 1px solid rgba(59, 130, 246, 0.2);
668
+ border-radius: 16px;
669
+ padding: 1.5rem;
670
+ transition: all 0.3s ease;
671
+ }
672
+
673
+ .stat-card:hover {
674
+ border-color: rgba(59, 130, 246, 0.5);
675
+ transform: translateY(-5px);
676
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
677
+ }
678
+
679
+ .stat-label {
680
+ font-size: 0.9em;
681
+ color: #94a3b8;
682
+ text-transform: uppercase;
683
+ letter-spacing: 0.5px;
684
+ margin-bottom: 0.5rem;
685
+ }
686
+
687
+ .stat-value {
688
+ font-size: 2em;
689
+ font-weight: 700;
690
+ background: linear-gradient(135deg, #60a5fa, #a78bfa);
691
+ -webkit-background-clip: text;
692
+ -webkit-text-fill-color: transparent;
693
+ background-clip: text;
694
+ }
695
+
696
+ /* Detail sections */
697
+ .detail-section {
698
+ background: rgba(15, 23, 42, 0.6);
699
+ border: 1px solid rgba(59, 130, 246, 0.2);
700
+ border-radius: 16px;
701
+ padding: 2rem;
702
+ margin-bottom: 1.5rem;
703
+ }
704
+
705
+ .detail-section h3 {
706
+ font-size: 1.5em;
707
+ color: #e2e8f0;
708
+ margin-bottom: 1.5rem;
709
+ padding-bottom: 1rem;
710
+ border-bottom: 1px solid rgba(59, 130, 246, 0.2);
711
+ }
712
+
713
+ .metric-row {
714
+ display: flex;
715
+ justify-content: space-between;
716
+ align-items: center;
717
+ padding: 1rem 0;
718
+ border-bottom: 1px solid rgba(59, 130, 246, 0.1);
719
+ }
720
+
721
+ .metric-row:last-child {
722
+ border-bottom: none;
723
+ }
724
+
725
+ .metric-label {
726
+ color: #94a3b8;
727
+ font-weight: 500;
728
+ }
729
+
730
+ .metric-value {
731
+ color: #e2e8f0;
732
+ font-weight: 700;
733
+ font-size: 1.1em;
734
+ }
735
+
736
+ /* Confidence visualization */
737
+ .confidence-section {
738
+ background: rgba(15, 23, 42, 0.6);
739
+ border: 1px solid rgba(59, 130, 246, 0.2);
740
+ border-radius: 16px;
741
+ padding: 2rem;
742
+ margin-bottom: 1.5rem;
743
+ }
744
+
745
+ .confidence-bar-wrapper {
746
+ display: flex;
747
+ align-items: center;
748
+ gap: 1rem;
749
+ margin: 1.5rem 0;
750
+ }
751
+
752
+ .confidence-bar-wrapper span {
753
+ font-size: 0.85em;
754
+ color: #64748b;
755
+ font-weight: 600;
756
+ }
757
+
758
+ .confidence-bar {
759
+ flex: 1;
760
+ height: 40px;
761
+ background: rgba(0, 0, 0, 0.4);
762
+ border-radius: 20px;
763
+ overflow: hidden;
764
+ position: relative;
765
+ border: 1px solid rgba(59, 130, 246, 0.2);
766
+ }
767
+
768
+ .confidence-fill {
769
+ height: 100%;
770
+ transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
771
+ position: relative;
772
+ }
773
+
774
+ .confidence-high {
775
+ background: linear-gradient(90deg, #16a34a, #22c55e);
776
+ box-shadow: 0 0 20px rgba(34, 197, 94, 0.4);
777
+ }
778
+
779
+ .confidence-moderate-fill {
780
+ background: linear-gradient(90deg, #ca8a04, #fbbf24);
781
+ box-shadow: 0 0 20px rgba(251, 191, 36, 0.4);
782
+ }
783
+
784
+ .confidence-low-fill {
785
+ background: linear-gradient(90deg, #ea580c, #f97316);
786
+ box-shadow: 0 0 20px rgba(249, 115, 22, 0.4);
787
+ }
788
+
789
+ .confidence-text {
790
+ position: absolute;
791
+ left: 50%;
792
+ top: 50%;
793
+ transform: translate(-50%, -50%);
794
+ font-weight: 800;
795
+ color: white;
796
+ font-size: 1.1em;
797
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
798
+ z-index: 2;
799
+ }
800
+
801
+ /* Hazard breakdown */
802
+ .hazard-grid {
803
+ display: grid;
804
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
805
+ gap: 1.5rem;
806
+ margin: 1.5rem 0;
807
+ }
808
+
809
+ .hazard-card {
810
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(139, 92, 246, 0.1));
811
+ border: 1px solid rgba(59, 130, 246, 0.3);
812
+ border-radius: 16px;
813
+ padding: 2rem;
814
+ text-align: center;
815
+ transition: all 0.3s ease;
816
+ }
817
+
818
+ .hazard-card:hover {
819
+ transform: scale(1.05);
820
+ border-color: rgba(59, 130, 246, 0.5);
821
+ box-shadow: 0 10px 30px rgba(59, 130, 246, 0.3);
822
+ }
823
+
824
+ .hazard-type {
825
+ font-size: 0.9em;
826
+ color: #94a3b8;
827
+ text-transform: uppercase;
828
+ letter-spacing: 0.5px;
829
+ margin-bottom: 1rem;
830
+ }
831
+
832
+ .hazard-value {
833
+ font-size: 3em;
834
+ font-weight: 800;
835
+ background: linear-gradient(135deg, #60a5fa, #a78bfa);
836
+ -webkit-background-clip: text;
837
+ -webkit-text-fill-color: transparent;
838
+ background-clip: text;
839
+ }
840
+
841
+ /* Explanation section */
842
+ .explanation-section {
843
+ background: rgba(15, 23, 42, 0.6);
844
+ border: 1px solid rgba(59, 130, 246, 0.2);
845
+ border-radius: 16px;
846
+ padding: 2rem;
847
+ margin-bottom: 1.5rem;
848
+ }
849
+
850
+ .factor-item {
851
+ display: flex;
852
+ align-items: center;
853
+ gap: 1rem;
854
+ margin: 1rem 0;
855
+ padding: 1rem;
856
+ background: rgba(0, 0, 0, 0.3);
857
+ border-radius: 12px;
858
+ }
859
+
860
+ .factor-name {
861
+ min-width: 200px;
862
+ color: #cbd5e1;
863
+ font-weight: 600;
864
+ }
865
+
866
+ .factor-bar {
867
+ flex: 1;
868
+ height: 28px;
869
+ background: rgba(59, 130, 246, 0.1);
870
+ border-radius: 14px;
871
+ overflow: hidden;
872
+ border: 1px solid rgba(59, 130, 246, 0.2);
873
+ }
874
+
875
+ .factor-fill {
876
+ height: 100%;
877
+ background: linear-gradient(90deg, #3b82f6, #8b5cf6);
878
+ transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
879
+ box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
880
+ }
881
+
882
+ .factor-percentage {
883
+ min-width: 60px;
884
+ text-align: right;
885
+ color: #e2e8f0;
886
+ font-weight: 700;
887
+ font-size: 1.05em;
888
+ }
889
+
890
+ /* Quality warnings */
891
+ .quality-warning {
892
+ background: linear-gradient(135deg, rgba(202, 138, 4, 0.2), rgba(161, 98, 7, 0.2));
893
+ border: 1px solid rgba(202, 138, 4, 0.4);
894
+ border-left: 4px solid #ca8a04;
895
+ border-radius: 12px;
896
+ padding: 1.5rem;
897
+ margin: 1.5rem 0;
898
+ }
899
+
900
+ .quality-warning h4 {
901
+ color: #fbbf24;
902
+ margin-bottom: 1rem;
903
+ display: flex;
904
+ align-items: center;
905
+ gap: 0.5rem;
906
+ }
907
+
908
+ .quality-warning ul {
909
+ list-style: none;
910
+ padding: 0;
911
+ }
912
+
913
+ .quality-warning li {
914
+ padding: 0.5rem 0;
915
+ color: #fde047;
916
+ padding-left: 1.5rem;
917
+ position: relative;
918
+ }
919
+
920
+ .quality-warning li::before {
921
+ content: '⚠';
922
+ position: absolute;
923
+ left: 0;
924
+ }
925
+
926
+ /* Error state */
927
+ .error-message {
928
+ background: linear-gradient(135deg, rgba(220, 38, 38, 0.2), rgba(185, 28, 28, 0.2));
929
+ border: 1px solid rgba(220, 38, 38, 0.4);
930
+ border-left: 4px solid #dc2626;
931
+ color: #fca5a5;
932
+ padding: 1.5rem;
933
+ border-radius: 12px;
934
+ margin: 1.5rem 0;
935
+ display: none;
936
+ }
937
+
938
+ /* Footer */
939
+ footer {
940
+ margin-top: 4rem;
941
+ padding: 2rem;
942
+ text-align: center;
943
+ color: #64748b;
944
+ border-top: 1px solid rgba(59, 130, 246, 0.2);
945
+ background: rgba(15, 23, 42, 0.6);
946
+ }
947
+
948
+ /* File upload styling */
949
+ input[type="file"] {
950
+ cursor: pointer;
951
+ padding: 1rem !important;
952
+ }
953
+
954
+ input[type="file"]::file-selector-button {
955
+ padding: 0.5rem 1rem;
956
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
957
+ color: white;
958
+ border: none;
959
+ border-radius: 8px;
960
+ cursor: pointer;
961
+ margin-right: 1rem;
962
+ transition: all 0.3s;
963
+ }
964
+
965
+ input[type="file"]::file-selector-button:hover {
966
+ transform: translateY(-2px);
967
+ box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
968
+ }
969
+
970
+ /* Pulse animation for predicted height */
971
+ @keyframes height-pulse {
972
+
973
+ 0%,
974
+ 100% {
975
+ background: rgba(0, 0, 0, 0.3);
976
+ }
977
+
978
+ 50% {
979
+ background: rgba(59, 130, 246, 0.3);
980
+ }
981
+ }
982
+
983
+ .height-pulse {
984
+ animation: height-pulse 0.8s ease;
985
+ }
986
+
987
+ input[type="text"]::-webkit-calendar-picker-indicator,
988
+ input[type="number"]::-webkit-calendar-picker-indicator {
989
+ filter: invert(1);
990
+ cursor: pointer;
991
+ }
992
+
993
+ /* Responsive design */
994
+ @media (max-width: 1024px) {
995
+ .main-container {
996
+ flex-direction: column;
997
+ }
998
+
999
+ .side-nav {
1000
+ width: 100%;
1001
+ position: static;
1002
+ }
1003
+
1004
+ .nav-card {
1005
+ display: flex;
1006
+ overflow-x: auto;
1007
+ padding: 1rem;
1008
+ }
1009
+
1010
+ .nav-card h3 {
1011
+ display: none;
1012
+ }
1013
+
1014
+ .nav-link {
1015
+ white-space: nowrap;
1016
+ margin-right: 0.5rem;
1017
+ margin-bottom: 0;
1018
+ }
1019
+ }
1020
+
1021
+ @media (max-width: 768px) {
1022
+ .hero-header h1 {
1023
+ font-size: 2em;
1024
+ }
1025
+
1026
+ .form-grid {
1027
+ grid-template-columns: 1fr;
1028
+ }
1029
+
1030
+ .stats-grid {
1031
+ grid-template-columns: 1fr;
1032
+ }
1033
+
1034
+ .assessment-card {
1035
+ padding: 1.5rem;
1036
+ }
1037
+
1038
+ .factor-name {
1039
+ min-width: 120px;
1040
+ font-size: 0.9em;
1041
+ }
1042
+ }
static/js/app.js ADDED
@@ -0,0 +1,603 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // CACHE MANAGEMENT
2
+
3
+ const CACHE_KEY = 'flood_assessment_cache';
4
+ const MAX_CACHE_SIZE = 5;
5
+
6
+ // Get cached entries from localStorage
7
+ function getCachedEntries() {
8
+ try {
9
+ const cached = localStorage.getItem(CACHE_KEY);
10
+ return cached ? JSON.parse(cached) : [];
11
+ } catch (e) {
12
+ console.error('Error reading cache:', e);
13
+ return [];
14
+ }
15
+ }
16
+
17
+ function saveToCacheFunction(latitude, longitude, height, basement) {
18
+ try {
19
+ let entries = getCachedEntries();
20
+
21
+ const newEntry = {
22
+ latitude: parseFloat(latitude).toFixed(6),
23
+ longitude: parseFloat(longitude).toFixed(6),
24
+ height: parseFloat(height).toFixed(2),
25
+ basement: parseFloat(basement).toFixed(2),
26
+ timestamp: Date.now()
27
+ };
28
+
29
+ // Check if entry already exists
30
+ const exists = entries.some(entry =>
31
+ entry.latitude === newEntry.latitude &&
32
+ entry.longitude === newEntry.longitude &&
33
+ entry.height === newEntry.height &&
34
+ entry.basement === newEntry.basement
35
+ );
36
+
37
+ if (!exists) {
38
+ entries.unshift(newEntry);
39
+
40
+ if (entries.length > MAX_CACHE_SIZE) {
41
+ entries = entries.slice(0, MAX_CACHE_SIZE);
42
+ }
43
+
44
+ localStorage.setItem(CACHE_KEY, JSON.stringify(entries));
45
+ updateDatalistSuggestions();
46
+ }
47
+ } catch (e) {
48
+ console.error('Error saving to cache:', e);
49
+ }
50
+ }
51
+
52
+ // Update datalist suggestions from cache
53
+ function updateDatalistSuggestions() {
54
+ const entries = getCachedEntries();
55
+
56
+ updateDatalist('lat-suggestions', entries, 'latitude');
57
+ updateDatalist('lon-suggestions', entries, 'longitude');
58
+ updateDatalist('height-suggestions', entries, 'height');
59
+ updateDatalist('basement-suggestions', entries, 'basement');
60
+ }
61
+
62
+ // Update a specific datalist
63
+ function updateDatalist(datalistId, entries, field) {
64
+ const datalist = document.getElementById(datalistId);
65
+ if (!datalist) return;
66
+
67
+ datalist.innerHTML = '';
68
+
69
+ entries.forEach((entry) => {
70
+ const option = document.createElement('option');
71
+ option.value = entry[field];
72
+
73
+ if (field === 'latitude') {
74
+ option.label = `${entry.latitude} | Lon: ${entry.longitude}, H: ${entry.height}m, B: ${entry.basement}m`;
75
+ } else if (field === 'longitude') {
76
+ option.label = `${entry.longitude} | Lat: ${entry.latitude}, H: ${entry.height}m, B: ${entry.basement}m`;
77
+ } else if (field === 'height') {
78
+ option.label = `${entry.height}m | At: ${entry.latitude}, ${entry.longitude}`;
79
+ } else if (field === 'basement') {
80
+ option.label = `${entry.basement}m | At: ${entry.latitude}, ${entry.longitude}`;
81
+ }
82
+
83
+ datalist.appendChild(option);
84
+ });
85
+ }
86
+
87
+ // Setup autofill functionality
88
+ function setupAutoFill() {
89
+ const forms = [
90
+ { suffix: '', latId: 'latitude', lonId: 'longitude' },
91
+ { suffix: '2', latId: 'latitude2', lonId: 'longitude2' },
92
+ { suffix: '3', latId: 'latitude3', lonId: 'longitude3' }
93
+ ];
94
+
95
+ forms.forEach(({ suffix, latId, lonId }) => {
96
+ const latInput = document.getElementById(latId);
97
+ const lonInput = document.getElementById(lonId);
98
+
99
+ if (!latInput || !lonInput) return;
100
+
101
+ const tryAutoFill = () => {
102
+ const latValue = parseNumber(latInput.value);
103
+ const lonValue = parseNumber(lonInput.value);
104
+
105
+ if (isNaN(latValue) || isNaN(lonValue)) return;
106
+
107
+ const entries = getCachedEntries();
108
+ const normalizedLat = latValue.toFixed(6);
109
+ const normalizedLon = lonValue.toFixed(6);
110
+
111
+ // Find entry matching BOTH lat and lon
112
+ const match = entries.find(entry =>
113
+ entry.latitude === normalizedLat &&
114
+ entry.longitude === normalizedLon
115
+ );
116
+
117
+ if (match) {
118
+ const heightInput = document.getElementById('height' + suffix);
119
+ const basementInput = document.getElementById('basement' + suffix);
120
+
121
+ if (heightInput && basementInput) {
122
+ heightInput.value = match.height;
123
+ basementInput.value = match.basement;
124
+
125
+ [latInput, lonInput, heightInput, basementInput].forEach(input => {
126
+ input.classList.add('height-pulse');
127
+ setTimeout(() => input.classList.remove('height-pulse'), 800);
128
+ });
129
+ }
130
+ }
131
+ };
132
+
133
+ latInput.addEventListener('change', () => {
134
+ setTimeout(tryAutoFill, 100);
135
+ });
136
+
137
+ lonInput.addEventListener('change', () => {
138
+ setTimeout(tryAutoFill, 100);
139
+ });
140
+ });
141
+ }
142
+
143
+ // UTILITY FUNCTIONS
144
+
145
+ // Parse numbers with comma or dot as decimal separator
146
+ function parseNumber(value) {
147
+ if (typeof value === 'string') {
148
+ value = value.replace(',', '.');
149
+ }
150
+ return parseFloat(value);
151
+ }
152
+
153
+ // Format quality flag for display
154
+ function formatFlag(flag) {
155
+ const flagMessages = {
156
+ 'missing_elevation': 'Elevation data unavailable',
157
+ 'missing_tpi': 'Topographic position data incomplete',
158
+ 'missing_slope': 'Slope data incomplete',
159
+ 'water_distance_unknown': 'Water proximity uncertain',
160
+ 'far_from_water_search_limited': 'Far from major water bodies (search radius limited)',
161
+ 'steep_terrain_dem_error_high': 'Steep terrain increases measurement uncertainty',
162
+ 'coastal_surge_risk_not_modeled': 'Coastal surge dynamics not fully captured'
163
+ };
164
+ return flagMessages[flag] || flag.replace(/_/g, ' ');
165
+ }
166
+
167
+ // UI INTERACTIONS
168
+
169
+ // Switch between assessment tabs
170
+ function switchTab(tabName) {
171
+ document.querySelectorAll('.assessment-card').forEach(card => {
172
+ card.classList.remove('active');
173
+ });
174
+ document.querySelectorAll('.nav-link').forEach(link => {
175
+ link.classList.remove('active');
176
+ });
177
+
178
+ document.getElementById(tabName + '-card').classList.add('active');
179
+ event.target.classList.add('active');
180
+ }
181
+
182
+ // API COMMUNICATION
183
+
184
+ // Predict building height from coordinates
185
+ async function predictHeight(latId, lonId, heightId, errorId, button) {
186
+ const latInput = document.getElementById(latId);
187
+ const lonInput = document.getElementById(lonId);
188
+ const heightInput = document.getElementById(heightId);
189
+ const errorBox = document.getElementById(errorId);
190
+
191
+ if (!latInput || !lonInput || !heightInput || !errorBox) {
192
+ return;
193
+ }
194
+
195
+ errorBox.style.display = 'none';
196
+ errorBox.textContent = '';
197
+
198
+ const latitude = parseFloat(latInput.value);
199
+ const longitude = parseFloat(lonInput.value);
200
+
201
+ if (isNaN(latitude) || isNaN(longitude)) {
202
+ errorBox.textContent = 'Please enter latitude and longitude first.';
203
+ errorBox.style.display = 'block';
204
+ return;
205
+ }
206
+
207
+ const originalText = button.textContent;
208
+ button.disabled = true;
209
+ button.textContent = 'Predicting...';
210
+
211
+ try {
212
+ const response = await fetch('/predict_height', {
213
+ method: 'POST',
214
+ headers: { 'Content-Type': 'application/json' },
215
+ body: JSON.stringify({
216
+ latitude,
217
+ longitude,
218
+ height: 0,
219
+ basement: 0
220
+ })
221
+ });
222
+
223
+ const data = await response.json();
224
+ if (!response.ok || data.status !== 'success' || data.predicted_height == null) {
225
+ const message = data.detail || data.error || 'Height prediction failed.';
226
+ throw new Error(message);
227
+ }
228
+
229
+ const h = Number(data.predicted_height);
230
+ heightInput.value = h.toFixed(2);
231
+ heightInput.classList.add('height-pulse');
232
+ setTimeout(() => {
233
+ heightInput.classList.remove('height-pulse');
234
+ }, 800);
235
+ } catch (err) {
236
+ errorBox.textContent = err.message || 'Height prediction failed.';
237
+ errorBox.style.display = 'block';
238
+ } finally {
239
+ button.disabled = false;
240
+ button.textContent = originalText;
241
+ }
242
+ }
243
+
244
+ // Get height from Global Building Atlas (GBA)
245
+ async function getHeightFromGBA(latId, lonId, heightId, errorId, button) {
246
+ const lat = parseNumber(document.getElementById(latId).value);
247
+ const lon = parseNumber(document.getElementById(lonId).value);
248
+
249
+ const errorBox = document.getElementById(errorId);
250
+ if (errorBox) errorBox.textContent = '';
251
+
252
+ if (Number.isNaN(lat) || Number.isNaN(lon)) {
253
+ if (errorBox) errorBox.textContent = 'Please enter valid latitude and longitude.';
254
+ return;
255
+ }
256
+
257
+ const originalText = button.textContent;
258
+ button.disabled = true;
259
+ button.textContent = 'Fetching...';
260
+
261
+ try {
262
+ const resp = await fetch('/get_height_gba', {
263
+ method: 'POST',
264
+ headers: { 'Content-Type': 'application/json' },
265
+ body: JSON.stringify({ latitude: lat, longitude: lon, height: 0, basement: 0 })
266
+ });
267
+
268
+ const data = await resp.json();
269
+ if (!resp.ok) {
270
+ const msg = data?.detail || 'Failed to get GBA height';
271
+ throw new Error(msg);
272
+ }
273
+
274
+ const h = data.predicted_height;
275
+ if (h === null || h === undefined) {
276
+ throw new Error('No height returned');
277
+ }
278
+
279
+ document.getElementById(heightId).value = Number(h).toFixed(2);
280
+ } catch (e) {
281
+ if (errorBox) {
282
+ errorBox.textContent = String(e.message || e);
283
+ errorBox.style.display = 'block';
284
+ }
285
+ } finally {
286
+ button.disabled = false;
287
+ button.textContent = originalText;
288
+ }
289
+ }
290
+
291
+ // Assess location vulnerability
292
+ async function assessLocation(event, endpoint, resultsId) {
293
+ event.preventDefault();
294
+
295
+ const tabName = resultsId.split('-')[0];
296
+ const suffix = endpoint === '/assess' ? '' : (endpoint === '/explain' ? '2' : '3');
297
+ const latitude = parseFloat(document.getElementById('latitude' + suffix).value);
298
+ const longitude = parseFloat(document.getElementById('longitude' + suffix).value);
299
+ const height = parseFloat(document.getElementById('height' + suffix).value) || 0;
300
+ const basement = parseFloat(document.getElementById('basement' + suffix).value) || 0;
301
+
302
+ document.getElementById(tabName + '-loading').style.display = 'block';
303
+ document.getElementById(resultsId).style.display = 'none';
304
+ document.getElementById(tabName + '-error').style.display = 'none';
305
+
306
+ try {
307
+ const response = await fetch(endpoint, {
308
+ method: 'POST',
309
+ headers: { 'Content-Type': 'application/json' },
310
+ body: JSON.stringify({ latitude, longitude, height, basement })
311
+ });
312
+
313
+ const data = await response.json();
314
+
315
+ if (data.status === 'success') {
316
+ // Save to cache on successful assessment
317
+ saveToCacheFunction(latitude, longitude, height, basement);
318
+ displayResults(data, resultsId, endpoint);
319
+ } else {
320
+ throw new Error(data.detail || 'Assessment failed');
321
+ }
322
+ } catch (error) {
323
+ document.getElementById(tabName + '-error').textContent = error.message;
324
+ document.getElementById(tabName + '-error').style.display = 'block';
325
+ } finally {
326
+ document.getElementById(tabName + '-loading').style.display = 'none';
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Upload and process batch CSV file
332
+ */
333
+ async function uploadBatch() {
334
+ const fileInput = document.getElementById('csvFile');
335
+ const file = fileInput.files[0];
336
+
337
+ if (!file) {
338
+ alert('Please select a CSV file');
339
+ return;
340
+ }
341
+
342
+ document.getElementById('batch-loading').style.display = 'block';
343
+ document.getElementById('batch-results').style.display = 'none';
344
+ document.getElementById('batch-error').style.display = 'none';
345
+
346
+ const formData = new FormData();
347
+ formData.append('file', file);
348
+
349
+ try {
350
+ const mode = document.getElementById('batchMode').value;
351
+ const heightSource = document.getElementById('heightSource').value;
352
+ let endpoint = mode === 'multihazard' ? '/assess_batch_multihazard' : '/assess_batch';
353
+
354
+ if (heightSource === 'gba') {
355
+ const sep = endpoint.includes('?') ? '&' : '?';
356
+ endpoint = endpoint + sep + 'use_gba_height=true';
357
+ } else if (heightSource === 'predicted') {
358
+ const sep = endpoint.includes('?') ? '&' : '?';
359
+ endpoint = endpoint + sep + 'use_predicted_height=true';
360
+ }
361
+
362
+ const response = await fetch(endpoint, {
363
+ method: 'POST',
364
+ body: formData
365
+ });
366
+
367
+ if (response.ok) {
368
+ const blob = await response.blob();
369
+ const url = window.URL.createObjectURL(blob);
370
+ const a = document.createElement('a');
371
+ a.href = url;
372
+ const filename = mode === 'multihazard'
373
+ ? 'multihazard_results.csv'
374
+ : 'vulnerability_results.csv';
375
+ a.download = filename;
376
+ document.body.appendChild(a);
377
+ a.click();
378
+ window.URL.revokeObjectURL(url);
379
+
380
+ document.getElementById('batch-results').innerHTML = `
381
+ <div class="results-header">
382
+ <h2>✓ Processing Complete</h2>
383
+ <p style="color: #94a3b8; margin-top: 1rem;">Results downloaded as ${filename}</p>
384
+ </div>
385
+ `;
386
+ document.getElementById('batch-results').style.display = 'block';
387
+ } else {
388
+ throw new Error('Batch processing failed');
389
+ }
390
+ } catch (error) {
391
+ document.getElementById('batch-error').textContent = error.message;
392
+ document.getElementById('batch-error').style.display = 'block';
393
+ } finally {
394
+ document.getElementById('batch-loading').style.display = 'none';
395
+ }
396
+ }
397
+
398
+ // ========================================
399
+ // RESULTS RENDERING
400
+ // ========================================
401
+
402
+ /**
403
+ * Display assessment results
404
+ */
405
+ function displayResults(data, resultsId, endpoint) {
406
+ const resultsDiv = document.getElementById(resultsId);
407
+ const assessment = data.assessment;
408
+
409
+ let html = '<div class="results-header">';
410
+ html += '<h2>Assessment Complete</h2>';
411
+
412
+ const riskClass = 'risk-' + assessment.risk_level.replace(/_/g, '-');
413
+ html += `<div class="risk-badge ${riskClass}">${assessment.risk_level.replace(/_/g, ' ')}</div>`;
414
+ html += '</div>';
415
+
416
+ html += '<div class="stats-grid">';
417
+
418
+ if (assessment.confidence_interval) {
419
+ const ci = assessment.confidence_interval;
420
+ html += `
421
+ <div class="stat-card">
422
+ <div class="stat-label">Vulnerability Index</div>
423
+ <div class="stat-value">${ci.point_estimate}</div>
424
+ <p style="font-size: 0.85em; color: #64748b; margin-top: 0.5rem;">
425
+ 95% CI: ${ci.lower_bound_95}–${ci.upper_bound_95}
426
+ </p>
427
+ </div>
428
+ `;
429
+ } else {
430
+ html += `
431
+ <div class="stat-card">
432
+ <div class="stat-label">Vulnerability Index</div>
433
+ <div class="stat-value">${assessment.vulnerability_index}</div>
434
+ </div>
435
+ `;
436
+ }
437
+
438
+ html += `
439
+ <div class="stat-card">
440
+ <div class="stat-label">Elevation</div>
441
+ <div class="stat-value">${assessment.elevation_m}m</div>
442
+ </div>
443
+ `;
444
+
445
+ if (assessment.distance_to_water_m !== null) {
446
+ html += `
447
+ <div class="stat-card">
448
+ <div class="stat-label">Distance to Water</div>
449
+ <div class="stat-value">${assessment.distance_to_water_m}m</div>
450
+ </div>
451
+ `;
452
+ }
453
+
454
+ html += '</div>';
455
+
456
+ if (assessment.uncertainty_analysis) {
457
+ const ua = assessment.uncertainty_analysis;
458
+ const confidenceValue = parseFloat(ua.confidence) || 0;
459
+ const barWidth = Math.round(confidenceValue * 100);
460
+
461
+ let confidenceClass = 'confidence-low-fill';
462
+ if (confidenceValue >= 0.75) confidenceClass = 'confidence-high';
463
+ else if (confidenceValue >= 0.55) confidenceClass = 'confidence-moderate-fill';
464
+
465
+ html += `
466
+ <div class="confidence-section">
467
+ <h3>Assessment Confidence</h3>
468
+ <div class="confidence-bar-wrapper">
469
+ <span>Low</span>
470
+ <div class="confidence-bar">
471
+ <div class="confidence-fill ${confidenceClass}" style="width: ${barWidth}%;"></div>
472
+ <span class="confidence-text">${barWidth}%</span>
473
+ </div>
474
+ <span>High</span>
475
+ </div>
476
+ <p style="margin-top: 1rem; color: #94a3b8; font-style: italic;">
477
+ ${ua.interpretation}
478
+ </p>
479
+ </div>
480
+ `;
481
+
482
+ if (ua.data_quality_flags && ua.data_quality_flags.length > 0) {
483
+ const criticalFlags = ua.data_quality_flags.filter(flag =>
484
+ flag === 'steep_terrain_dem_error_high' ||
485
+ flag === 'coastal_surge_risk_not_modeled'
486
+ );
487
+
488
+ if (criticalFlags.length > 0) {
489
+ html += '<div class="quality-warning"><h4>⚠ Data Quality Notes</h4><ul>';
490
+ criticalFlags.forEach(flag => {
491
+ html += `<li>${formatFlag(flag)}</li>`;
492
+ });
493
+ html += '</ul></div>';
494
+ }
495
+ }
496
+ }
497
+
498
+ html += '<div class="detail-section"><h3>Terrain Analysis</h3>';
499
+ html += `
500
+ <div class="metric-row">
501
+ <span class="metric-label">Elevation</span>
502
+ <span class="metric-value">${assessment.elevation_m} m</span>
503
+ </div>
504
+ <div class="metric-row">
505
+ <span class="metric-label">Relative Elevation (TPI)</span>
506
+ <span class="metric-value">${assessment.relative_elevation_m !== null ? assessment.relative_elevation_m + ' m' : 'N/A'}</span>
507
+ </div>
508
+ <div class="metric-row">
509
+ <span class="metric-label">Slope</span>
510
+ <span class="metric-value">${assessment.slope_degrees !== null ? assessment.slope_degrees + '°' : 'N/A'}</span>
511
+ </div>
512
+ <div class="metric-row">
513
+ <span class="metric-label">Distance to Water</span>
514
+ <span class="metric-value">${assessment.distance_to_water_m !== null ? assessment.distance_to_water_m + ' m' : 'N/A'}</span>
515
+ </div>
516
+ `;
517
+ html += '</div>';
518
+
519
+ if (assessment.hazard_breakdown) {
520
+ const hb = assessment.hazard_breakdown;
521
+ html += '<div class="detail-section"><h3>Hazard Breakdown</h3>';
522
+ html += '<div class="hazard-grid">';
523
+ html += `
524
+ <div class="hazard-card">
525
+ <div class="hazard-type">Fluvial/Riverine</div>
526
+ <div class="hazard-value">${hb.fluvial_riverine}</div>
527
+ </div>
528
+ <div class="hazard-card">
529
+ <div class="hazard-type">Coastal Surge</div>
530
+ <div class="hazard-value">${hb.coastal_surge}</div>
531
+ </div>
532
+ <div class="hazard-card">
533
+ <div class="hazard-type">Pluvial/Drainage</div>
534
+ <div class="hazard-value">${hb.pluvial_drainage}</div>
535
+ </div>
536
+ `;
537
+ html += '</div>';
538
+ html += `<p style="margin-top: 1.5rem;"><strong>Dominant Hazard:</strong> ${assessment.dominant_hazard.replace(/_/g, ' ').toUpperCase()}</p>`;
539
+ html += '</div>';
540
+ }
541
+
542
+ if (data.explanation) {
543
+ const exp = data.explanation;
544
+ html += '<div class="explanation-section">';
545
+ html += '<h3>Risk Factor Analysis</h3>';
546
+ html += `<p style="margin-bottom: 1.5rem; color: #cbd5e1;"><strong>Top Risk Driver:</strong> ${exp.top_risk_driver}</p>`;
547
+
548
+ exp.explanations.forEach(factor => {
549
+ html += `
550
+ <div class="factor-item">
551
+ <span class="factor-name">${factor.factor}</span>
552
+ <div class="factor-bar">
553
+ <div class="factor-fill" style="width: ${factor.contribution_pct}%"></div>
554
+ </div>
555
+ <span class="factor-percentage">${factor.contribution_pct}%</span>
556
+ </div>
557
+ `;
558
+ });
559
+
560
+ html += '</div>';
561
+ }
562
+
563
+ resultsDiv.innerHTML = html;
564
+ resultsDiv.style.display = 'block';
565
+ }
566
+
567
+ // ========================================
568
+ // INITIALIZATION
569
+ // ========================================
570
+
571
+ document.addEventListener('DOMContentLoaded', () => {
572
+ // Initialize cache and autofill
573
+ updateDatalistSuggestions();
574
+ setupAutoFill();
575
+
576
+ // Setup predict height buttons
577
+ const buttons = document.querySelectorAll('.predict-height-btn');
578
+ if (buttons.length > 0) {
579
+ buttons.forEach(button => {
580
+ const latId = button.dataset.latId;
581
+ const lonId = button.dataset.lonId;
582
+ const heightId = button.dataset.heightId;
583
+ const errorId = button.dataset.errorId;
584
+
585
+ button.addEventListener('click', () => {
586
+ predictHeight(latId, lonId, heightId, errorId, button);
587
+ });
588
+ });
589
+ }
590
+
591
+ // Setup GBA height buttons
592
+ document.querySelectorAll('.gba-height-btn').forEach(btn => {
593
+ btn.addEventListener('click', () => {
594
+ getHeightFromGBA(
595
+ btn.dataset.latId,
596
+ btn.dataset.lonId,
597
+ btn.dataset.heightId,
598
+ btn.dataset.errorId,
599
+ btn
600
+ );
601
+ });
602
+ });
603
+ });