Gunjuzone commited on
Commit
a749f91
·
verified ·
1 Parent(s): 38e1ad2

Upload index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +1485 -998
templates/index.html CHANGED
@@ -1,999 +1,1486 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Flood Vulnerability Assessment</title>
8
- <style>
9
- html,
10
- body {
11
- height: 100%;
12
- margin: 0;
13
- padding: 0;
14
- }
15
-
16
-
17
- body {
18
- display: flex;
19
- flex-direction: column;
20
- min-height: 100vh;
21
-
22
-
23
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
24
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
25
- padding: 20px;
26
- }
27
-
28
-
29
- .container {
30
- flex: 1 0 auto;
31
- width: 100%;
32
- max-width: 950px;
33
- margin: 0 auto;
34
- background: white;
35
- border-radius: 15px;
36
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
37
- overflow: hidden;
38
- }
39
-
40
-
41
- footer {
42
- flex-shrink: 0;
43
- }
44
-
45
- .header {
46
- background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
47
- color: white;
48
- padding: 30px;
49
- text-align: center;
50
- }
51
-
52
- h1 {
53
- font-size: 2em;
54
- margin-bottom: 10px;
55
- }
56
-
57
- .subtitle {
58
- opacity: 0.9;
59
- font-size: 0.95em;
60
- }
61
-
62
- .tabs {
63
- display: flex;
64
- justify-content: space-between;
65
- background: #e6e8ef;
66
- border-radius: 10px;
67
- border: 1px solid #d0d3da;
68
- margin: 20px;
69
- overflow: hidden;
70
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
71
- }
72
-
73
- .tab {
74
- flex: 1;
75
- padding: 14px 0;
76
- text-align: center;
77
- cursor: pointer;
78
- background: #d9dbe3;
79
- font-size: 1em;
80
- font-weight: 500;
81
- color: #2f2f2f;
82
- border-right: 1px solid #c4c6ce;
83
- transition: all 0.25s ease-in-out;
84
- }
85
-
86
- .tab:last-child {
87
- border-right: none;
88
- }
89
-
90
- .tab:hover {
91
- background: #cfd1da;
92
- }
93
-
94
- .tab.active {
95
- background: linear-gradient(135deg, #667eea 0%, #5a67d8 100%);
96
- color: white;
97
- font-weight: 600;
98
- box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.1);
99
- z-index: 1;
100
- }
101
-
102
- .tab-content {
103
- display: none;
104
- padding: 30px;
105
- }
106
-
107
- .tab-content.active {
108
- display: block;
109
- }
110
-
111
- .form-group {
112
- margin-bottom: 20px;
113
- }
114
-
115
- label {
116
- display: block;
117
- margin-bottom: 8px;
118
- font-weight: 600;
119
- color: #2c3e50;
120
- }
121
-
122
- input,
123
- select {
124
- width: 100%;
125
- padding: 12px;
126
- border: 2px solid #e0e0e0;
127
- border-radius: 8px;
128
- font-size: 1em;
129
- transition: border 0.3s;
130
- }
131
-
132
- input:focus,
133
- select:focus {
134
- outline: none;
135
- border-color: #667eea;
136
- }
137
-
138
- .helper-text {
139
- font-size: 0.85em;
140
- color: #666;
141
- margin-top: 5px;
142
- }
143
-
144
- .height-group {
145
- display: flex;
146
- align-items: center;
147
- width: 100%;
148
- border: 2px solid #d0d3da;
149
- border-radius: 8px;
150
- overflow: hidden;
151
- background: #ffffff;
152
- }
153
-
154
- .height-group:focus-within {
155
- border-color: #667eea;
156
- }
157
-
158
- .height-group input {
159
- flex: 1;
160
- padding: 14px 18px;
161
- font-size: 1rem;
162
- border: none !important;
163
- outline: none !important;
164
- border-radius: 0 !important;
165
- background: transparent;
166
- color: #333;
167
- }
168
-
169
- .height-group input::-webkit-inner-spin-button,
170
- .height-group input::-webkit-outer-spin-button {
171
- margin: 0;
172
- }
173
-
174
- .height-group button {
175
- width: auto !important;
176
- padding: 14px 24px;
177
- font-size: 0.95rem;
178
- font-weight: 600;
179
- border: none;
180
- border-left: 2px solid #d0d3da;
181
- cursor: pointer;
182
- color: white;
183
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
184
- border-radius: 0 !important;
185
- height: 100%;
186
- white-space: nowrap;
187
- transition: opacity 0.2s ease;
188
- }
189
-
190
- .height-group button:hover {
191
- opacity: 0.9;
192
- transform: none !important;
193
- }
194
-
195
- .height-group button:active {
196
- transform: none !important;
197
- }
198
-
199
- button {
200
- width: 100%;
201
- padding: 15px;
202
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
203
- color: white;
204
- border: none;
205
- border-radius: 8px;
206
- font-size: 1.1em;
207
- font-weight: 600;
208
- cursor: pointer;
209
- transition: transform 0.2s;
210
- }
211
-
212
- button:hover {
213
- transform: translateY(-2px);
214
- }
215
-
216
- button:active {
217
- transform: translateY(0);
218
- }
219
-
220
- button:disabled {
221
- background: #ccc;
222
- cursor: not-allowed;
223
- transform: none;
224
- }
225
-
226
- .loading {
227
- display: none;
228
- text-align: center;
229
- padding: 20px;
230
- color: #667eea;
231
- }
232
-
233
- .results {
234
- margin-top: 30px;
235
- padding: 25px;
236
- background: #f8f9fa;
237
- border-radius: 10px;
238
- display: none;
239
- }
240
-
241
- .risk-badge {
242
- display: inline-block;
243
- padding: 8px 16px;
244
- border-radius: 20px;
245
- font-weight: 600;
246
- margin: 10px 0;
247
- }
248
-
249
- .risk-very-high {
250
- background: #dc3545;
251
- color: white;
252
- }
253
-
254
- .risk-high {
255
- background: #fd7e14;
256
- color: white;
257
- }
258
-
259
- .risk-moderate {
260
- background: #ffc107;
261
- color: #000;
262
- }
263
-
264
- .risk-low {
265
- background: #28a745;
266
- color: white;
267
- }
268
-
269
- .risk-very-low {
270
- background: #17a2b8;
271
- color: white;
272
- }
273
-
274
- .metric {
275
- display: flex;
276
- justify-content: space-between;
277
- padding: 12px 0;
278
- border-bottom: 1px solid #dee2e6;
279
- }
280
-
281
- .metric:last-child {
282
- border-bottom: none;
283
- }
284
-
285
- .metric-label {
286
- font-weight: 600;
287
- color: #495057;
288
- }
289
-
290
- .metric-value {
291
- color: #212529;
292
- }
293
-
294
- .confidence-badge {
295
- display: inline-block;
296
- padding: 6px 14px;
297
- border-radius: 15px;
298
- font-size: 0.85em;
299
- font-weight: 600;
300
- margin-left: 10px;
301
- vertical-align: middle;
302
- }
303
-
304
- .confidence-good {
305
- background: #10b981;
306
- color: white;
307
- }
308
-
309
- .confidence-moderate {
310
- background: #f59e0b;
311
- color: white;
312
- }
313
-
314
- .confidence-low {
315
- background: #ef4444;
316
- color: white;
317
- }
318
-
319
- .quality-flags {
320
- background: #fff3cd;
321
- border-left: 4px solid #ffc107;
322
- padding: 15px;
323
- margin: 20px 0;
324
- border-radius: 5px;
325
- }
326
-
327
- .quality-flags h4 {
328
- margin-bottom: 10px;
329
- color: #856404;
330
- }
331
-
332
- .quality-flags ul {
333
- list-style: none;
334
- padding: 0;
335
- }
336
-
337
- .quality-flags li {
338
- padding: 5px 0;
339
- color: #856404;
340
- }
341
-
342
- .explanation-card {
343
- background: white;
344
- padding: 15px;
345
- margin: 10px 0;
346
- border-radius: 8px;
347
- border-left: 4px solid #667eea;
348
- }
349
-
350
- .factor-contribution {
351
- display: flex;
352
- align-items: center;
353
- margin: 8px 0;
354
- }
355
-
356
- .contribution-bar {
357
- flex: 1;
358
- height: 20px;
359
- background: #e9ecef;
360
- border-radius: 4px;
361
- overflow: hidden;
362
- margin: 0 10px;
363
- }
364
-
365
- .contribution-fill {
366
- height: 100%;
367
- background: linear-gradient(90deg, #667eea, #764ba2);
368
- transition: width 0.5s;
369
- }
370
-
371
- .hazard-breakdown {
372
- display: grid;
373
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
374
- gap: 15px;
375
- margin: 20px 0;
376
- }
377
-
378
- .hazard-card {
379
- background: white;
380
- padding: 15px;
381
- border-radius: 8px;
382
- text-align: center;
383
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
384
- }
385
-
386
- .hazard-value {
387
- font-size: 2em;
388
- font-weight: 700;
389
- color: #667eea;
390
- margin: 10px 0;
391
- }
392
-
393
- .confidence-fill {
394
- height: 100%;
395
- border-radius: 5px;
396
- transition: width 0.5s;
397
- }
398
-
399
- .confidence-high {
400
- background: #28a745;
401
- }
402
-
403
- .confidence-moderate-fill {
404
- background: #ffc107;
405
- }
406
-
407
- .confidence-low-fill {
408
- background: #fd7e14;
409
- }
410
-
411
- .error {
412
- background: #f8d7da;
413
- color: #721c24;
414
- padding: 15px;
415
- border-radius: 8px;
416
- margin: 20px 0;
417
- display: none;
418
- }
419
-
420
- .checkbox-row {
421
- display: flex;
422
- align-items: center;
423
- gap: 10px;
424
- }
425
-
426
- .checkbox-row input[type="checkbox"] {
427
- width: auto;
428
- height: auto;
429
- margin: 0;
430
- }
431
-
432
- .checkbox-row label {
433
- margin: 0;
434
- font-weight: 600;
435
- color: #2c3e50;
436
- display: inline-block;
437
- }
438
- </style>
439
- </head>
440
-
441
- <body>
442
- <div class="container">
443
- <div class="header">
444
- <h1>🌊 Flood Vulnerability Assessment</h1>
445
- <p class="subtitle">Advanced multi-hazard flood risk analysis powered by GEE</p>
446
- </div>
447
-
448
- <div class="tabs">
449
- <button class="tab active" onclick="switchTab('basic')">Basic Assessment</button>
450
- <button class="tab" onclick="switchTab('explained')">With Explanation</button>
451
- <button class="tab" onclick="switchTab('multihazard')">Multi-Hazard</button>
452
- <button class="tab" onclick="switchTab('batch')">Batch Upload</button>
453
- </div>
454
-
455
- <!-- Basic Assessment Tab -->
456
- <div id="basic-tab" class="tab-content active">
457
- <form id="assessForm" onsubmit="assessLocation(event, '/assess', 'basic-results')">
458
- <div class="form-group">
459
- <label for="latitude">Latitude</label>
460
- <input type="text" id="latitude" name="latitude" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*"
461
- required placeholder="e.g., 40.7128">
462
- <p class="helper-text">Range: -90 to 90</p>
463
- </div>
464
-
465
- <div class="form-group">
466
- <label for="longitude">Longitude</label>
467
- <input type="text" id="longitude" name="longitude" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*"
468
- autocomplete="off" required placeholder="e.g., -74.0060">
469
- <p class="helper-text">Range: -180 to 180</p>
470
- </div>
471
-
472
- <div class="form-group">
473
- <label for="height">Building Height (meters above ground)</label>
474
- <div class="height-group">
475
- <input type="number" id="height" step="any" value="0" placeholder="e.g., 5.0">
476
- <button type="button" id="predict-height-btn" class="predict-height-btn" data-lat-id="latitude"
477
- data-lon-id="longitude" data-height-id="height" data-error-id="basic-error"> Predict
478
- </button>
479
- </div>
480
- <p class="helper-text">0 = ground level, 5 = typical 2-story building</p>
481
- </div>
482
-
483
- <div class="form-group">
484
- <label for="basement">Basement Depth (meters below ground)</label>
485
- <input type="text" id="basement" name="basement" value="0" max="0" inputmode="text"
486
- pattern="-?[0-9]*[.,]?[0-9]*" placeholder="e.g., -2.0">
487
- <p class="helper-text">0 = no basement, -2 = 2 meters below ground (increases risk)</p>
488
- </div>
489
-
490
- <button type="submit">Assess Vulnerability</button>
491
- </form>
492
-
493
- <div class="loading" id="basic-loading">
494
- <p>⏳ Analyzing terrain and water proximity...</p>
495
- </div>
496
-
497
- <div class="error" id="basic-error"></div>
498
- <div class="results" id="basic-results"></div>
499
- </div>
500
-
501
- <!-- Explained Assessment Tab -->
502
- <div id="explained-tab" class="tab-content">
503
- <form onsubmit="assessLocation(event, '/explain', 'explained-results')">
504
- <div class="form-group">
505
- <label for="latitude2">Latitude</label>
506
- <input type="text" id="latitude2" name="latitude2" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off"
507
- required placeholder="e.g., 40.7128">
508
- </div>
509
-
510
- <div class="form-group">
511
- <label for="longitude2">Longitude</label>
512
- <input type="text" id="longitude2" name="longitude2" pattern="-?[0-9]*[.,]?[0-9]*"
513
- autocomplete="off" required placeholder="e.g., -74.0060">
514
- </div>
515
-
516
- <div class="form-group">
517
- <label for="height2">Building Height (meters)</label>
518
- <div class="height-group">
519
- <input type="number" id="height2" step="any" value="0" placeholder="e.g., 5.0">
520
- <button type="button" class="predict-height-btn" data-lat-id="latitude2"
521
- data-lon-id="longitude2" data-height-id="height2" data-error-id="explained-error"> Predict
522
- </button>
523
- </div>
524
- <p class="helper-text">0 = ground level, 5 = typical 2-story building</p>
525
- </div>
526
-
527
- <div class="form-group">
528
- <label for="basement2">Basement Depth (meters, negative)</label>
529
- <input type="text" id="basement2" name="basement2" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*"
530
- step="0.1" value="0" max="0">
531
- </div>
532
-
533
- <button type="submit">Assess with Explanation</button>
534
- </form>
535
-
536
- <div class="loading" id="explained-loading">
537
- <p>⏳ Analyzing and generating explanation...</p>
538
- </div>
539
-
540
- <div class="error" id="explained-error"></div>
541
- <div class="results" id="explained-results"></div>
542
- </div>
543
-
544
- <!-- Multi-Hazard Tab -->
545
- <div id="multihazard-tab" class="tab-content">
546
- <form onsubmit="assessLocation(event, '/assess_multihazard', 'multihazard-results')">
547
- <div class="form-group">
548
- <label for="latitude3">Latitude</label>
549
- <input type="text" id="latitude3" name="latitude3" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off"
550
- required placeholder="e.g., 40.7128">
551
- </div>
552
-
553
- <div class="form-group">
554
- <label for="longitude3">Longitude</label>
555
- <input type="text" id="longitude3" name="longitude3" pattern="-?[0-9]*[.,]?[0-9]*"
556
- autocomplete="off" required placeholder="e.g., -74.0060">
557
- </div>
558
-
559
- <div class="form-group">
560
- <label for="height3">Building Height (meters)</label>
561
- <div class="height-group">
562
- <input type="number" id="height3" step="any" value="0" placeholder="e.g., 5.0">
563
- <button type="button" class="predict-height-btn" data-lat-id="latitude3"
564
- data-lon-id="longitude3" data-height-id="height3" data-error-id="multihazard-error"> Predict
565
- </button>
566
- </div>
567
- <p class="helper-text">0 = ground level, 5 = typical 2-story building</p>
568
- </div>
569
-
570
- <div class="form-group">
571
- <label for="basement3">Basement Depth (meters, negative)</label>
572
- <input type="text" id="basement3" name="basement3" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*"
573
- step="0.1" value="0" max="0">
574
- </div>
575
-
576
- <button type="submit">Multi-Hazard Assessment</button>
577
- </form>
578
-
579
- <div class="loading" id="multihazard-loading">
580
- <p>⏳ Analyzing multiple flood hazards...</p>
581
- </div>
582
-
583
- <div class="error" id="multihazard-error"></div>
584
- <div class="results" id="multihazard-results"></div>
585
- </div>
586
-
587
- <div id="batch-tab" class="tab-content">
588
-
589
- <div class="form-group">
590
- <label for="batchMode">Batch Mode</label>
591
- <select id="batchMode">
592
- <option value="standard">Basic Assessment Model</option>
593
- <option value="multihazard">Multi-Hazard (Fluvial / Coastal / Pluvial)</option>
594
- </select>
595
- </div>
596
-
597
- <div class="form-group">
598
- <label for="csvFile">Upload CSV File</label>
599
- <input type="file" id="csvFile" accept=".csv">
600
- <p class="helper-text"> CSV must contain: latitude, longitude in decimal degrees (WGS84), e.g. 29.1703,
601
- -95.3128.<br>
602
- Optional columns: height, basement.</p>
603
-
604
- </div>
605
-
606
- <div class="form-group">
607
- <div class="checkbox-row">
608
- <input type="checkbox" id="usePredictedHeight">
609
- <label for="usePredictedHeight">Use satellite-predicted building height</label>
610
- </div>
611
- <p class="helper-text">
612
- For each row, estimate height from coordinates and use it in the vulnerability assessment.
613
- </p>
614
- </div>
615
-
616
- <button onclick="uploadBatch()">Process Batch</button>
617
-
618
- <div class="loading" id="batch-loading">
619
- <p>⏳ Processing batch assessments...</p>
620
- </div>
621
-
622
- <div class="error" id="batch-error"></div>
623
- <div class="results" id="batch-results"></div>
624
-
625
- </div>
626
-
627
-
628
- </div>
629
-
630
- <script>
631
-
632
- document.addEventListener('DOMContentLoaded', () => {
633
- const buttons = document.querySelectorAll('.predict-height-btn');
634
- if (!buttons.length) {
635
- return;
636
- }
637
-
638
- buttons.forEach(button => {
639
- const latId = button.dataset.latId;
640
- const lonId = button.dataset.lonId;
641
- const heightId = button.dataset.heightId;
642
- const errorId = button.dataset.errorId;
643
-
644
- button.addEventListener('click', () => {
645
- predictHeight(latId, lonId, heightId, errorId, button);
646
- });
647
- });
648
- });
649
-
650
- async function predictHeight(latId, lonId, heightId, errorId, button) {
651
- const latInput = document.getElementById(latId);
652
- const lonInput = document.getElementById(lonId);
653
- const heightInput = document.getElementById(heightId);
654
- const errorBox = document.getElementById(errorId);
655
-
656
- if (!latInput || !lonInput || !heightInput || !errorBox) {
657
- return;
658
- }
659
-
660
- errorBox.style.display = 'none';
661
- errorBox.textContent = '';
662
-
663
- const latitude = parseFloat(latInput.value);
664
- const longitude = parseFloat(lonInput.value);
665
-
666
- if (isNaN(latitude) || isNaN(longitude)) {
667
- errorBox.textContent = 'Please enter latitude and longitude first.';
668
- errorBox.style.display = 'block';
669
- return;
670
- }
671
-
672
- const originalText = button.textContent;
673
- button.disabled = true;
674
- button.textContent = 'Predicting...';
675
-
676
- try {
677
- const response = await fetch('/predict_height', {
678
- method: 'POST',
679
- headers: { 'Content-Type': 'application/json' },
680
- body: JSON.stringify({
681
- latitude,
682
- longitude,
683
- height: 0,
684
- basement: 0
685
- })
686
- });
687
-
688
- const data = await response.json();
689
- if (!response.ok || data.status !== 'success' || data.predicted_height == null) {
690
- const message = data.detail || data.error || 'Height prediction failed.';
691
- throw new Error(message);
692
- }
693
-
694
- const h = Number(data.predicted_height);
695
- heightInput.value = h.toFixed(2);
696
- heightInput.classList.add('height-pulse');
697
- setTimeout(() => {
698
- heightInput.classList.remove('height-pulse');
699
- }, 800);
700
- } catch (err) {
701
- errorBox.textContent = err.message || 'Height prediction failed.';
702
- errorBox.style.display = 'block';
703
- } finally {
704
- button.disabled = false;
705
- button.textContent = originalText;
706
- }
707
- }
708
-
709
-
710
- function switchTab(tabName) {
711
- document.querySelectorAll('.tab-content').forEach(tab => {
712
- tab.classList.remove('active');
713
- });
714
- document.querySelectorAll('.tab').forEach(tab => {
715
- tab.classList.remove('active');
716
- });
717
-
718
- document.getElementById(tabName + '-tab').classList.add('active');
719
- event.target.classList.add('active');
720
- }
721
-
722
- async function assessLocation(event, endpoint, resultsId) {
723
- event.preventDefault();
724
-
725
- const tabName = resultsId.split('-')[0];
726
- const suffix = endpoint === '/assess' ? '' : (endpoint === '/explain' ? '2' : '3');
727
- const latitude = parseFloat(document.getElementById('latitude' + suffix).value);
728
- const longitude = parseFloat(document.getElementById('longitude' + suffix).value);
729
- const height = parseFloat(document.getElementById('height' + suffix).value) || 0;
730
- const basement = parseFloat(document.getElementById('basement' + suffix).value) || 0;
731
-
732
- document.getElementById(tabName + '-loading').style.display = 'block';
733
- document.getElementById(resultsId).style.display = 'none';
734
- document.getElementById(tabName + '-error').style.display = 'none';
735
-
736
- try {
737
- const response = await fetch(endpoint, {
738
- method: 'POST',
739
- headers: { 'Content-Type': 'application/json' },
740
- body: JSON.stringify({ latitude, longitude, height, basement })
741
- });
742
-
743
- const data = await response.json();
744
-
745
- if (data.status === 'success') {
746
- displayResults(data, resultsId, endpoint);
747
- } else {
748
- throw new Error(data.detail || 'Assessment failed');
749
- }
750
- } catch (error) {
751
- document.getElementById(tabName + '-error').textContent = error.message;
752
- document.getElementById(tabName + '-error').style.display = 'block';
753
- } finally {
754
- document.getElementById(tabName + '-loading').style.display = 'none';
755
- }
756
- }
757
-
758
- function formatFlag(flag) {
759
- const flagMessages = {
760
- 'missing_elevation': 'Elevation data unavailable',
761
- 'missing_tpi': 'Topographic position data incomplete',
762
- 'missing_slope': 'Slope data incomplete',
763
- 'water_distance_unknown': 'Water proximity uncertain',
764
- 'far_from_water_search_limited': 'Far from major water bodies (search radius limited)',
765
- 'steep_terrain_dem_error_high': ' Steep terrain increases measurement uncertainty',
766
- 'coastal_surge_risk_not_modeled': ' Coastal surge dynamics not fully captured'
767
- };
768
- return flagMessages[flag] || flag.replace(/_/g, ' ');
769
- }
770
- function displayResults(data, resultsId, endpoint) {
771
- const resultsDiv = document.getElementById(resultsId);
772
- const assessment = data.assessment;
773
-
774
- let html = '<h2>Assessment Results</h2>';
775
-
776
- // Risk badge
777
- const riskClass = 'risk-' + assessment.risk_level.replace(/_/g, '-');
778
- html += `<div style="margin: 15px 0;">`;
779
- html += `<div class="risk-badge ${riskClass}">${assessment.risk_level.toUpperCase().replace(/_/g, ' ')}</div>`;
780
- html += `</div>`;
781
-
782
-
783
-
784
- // Vulnerability score
785
- if (assessment.confidence_interval) {
786
- const ci = assessment.confidence_interval;
787
- html += `
788
- <div class="metric">
789
- <span class="metric-label">Vulnerability Index</span>
790
- <span class="metric-value">
791
- ${ci.point_estimate} (95% CI: ${ci.lower_bound_95}–${ci.upper_bound_95})
792
- </span>
793
- </div>
794
- `;
795
- } else {
796
- html += `
797
- <div class="metric">
798
- <span class="metric-label">Vulnerability Index</span>
799
- <span class="metric-value">${assessment.vulnerability_index}</span>
800
- </div>
801
- `;
802
- }
803
-
804
- // Confidence visualization
805
- if (assessment.uncertainty_analysis) {
806
- const ua = assessment.uncertainty_analysis;
807
- const confidenceValue = parseFloat(ua.confidence) || 0;
808
- const barWidth = Math.round(confidenceValue * 100);
809
-
810
- let confidenceClass = 'confidence-low-fill';
811
- if (confidenceValue >= 0.75) confidenceClass = 'confidence-high';
812
- else if (confidenceValue >= 0.55) confidenceClass = 'confidence-moderate-fill';
813
-
814
- html += `
815
- <div style="margin: 25px 0; padding: 20px; background: white; border-radius: 8px;">
816
- <h3 style="margin-bottom: 10px; color: #333; font-size: 1.1em;">Assessment Confidence</h3>
817
- <div style="display: flex; align-items: center; font-size: 0.85em; color: #555;">
818
- <span style="min-width: 40px;">Low</span>
819
- <div style="flex: 1; height: 24px; background: #e9ecef; border-radius: 12px; overflow: hidden; margin: 0 12px; position: relative; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);">
820
- <div class="confidence-fill ${confidenceClass}"
821
- style="width: ${barWidth}%; height: 100%; transition: width 0.4s ease;"></div>
822
- <span style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
823
- font-weight: 700; color: #000; font-size: 0.9em; text-shadow: 0 0 4px rgba(255,255,255,0.9);">
824
- ${barWidth}%
825
- </span>
826
- </div>
827
- <span style="min-width: 40px; text-align: right;">High</span>
828
- </div>
829
- <p style="margin: 12px 0 0; font-size: 0.9em; color: #555; font-style: italic;">
830
- ${ua.interpretation}
831
- </p>
832
- </div>
833
- `;
834
-
835
- // Quality flags
836
- if (ua.data_quality_flags && ua.data_quality_flags.length > 0) {
837
- const criticalFlags = ua.data_quality_flags.filter(flag =>
838
- flag === 'steep_terrain_dem_error_high' ||
839
- flag === 'coastal_surge_risk_not_modeled'
840
- );
841
-
842
- if (criticalFlags.length > 0) {
843
- html += `
844
- <div class="quality-flags">
845
- <h4>Data Quality Notes</h4>
846
- <ul>
847
- `;
848
- criticalFlags.forEach(flag => {
849
- html += `<li>${formatFlag(flag)}</li>`;
850
- });
851
- html += `
852
- </ul>
853
- </div>
854
- `;
855
- }
856
- }
857
- }
858
-
859
- // Terrain metrics
860
- html += '<h3 style="margin-top: 25px;">Terrain Analysis</h3>';
861
- html += `
862
- <div class="metric">
863
- <span class="metric-label">Elevation</span>
864
- <span class="metric-value">${assessment.elevation_m} m</span>
865
- </div>
866
- <div class="metric">
867
- <span class="metric-label">Relative Elevation (TPI)</span>
868
- <span class="metric-value">${assessment.relative_elevation_m !== null ? assessment.relative_elevation_m + ' m' : 'N/A'}</span>
869
- </div>
870
- <div class="metric">
871
- <span class="metric-label">Slope</span>
872
- <span class="metric-value">${assessment.slope_degrees !== null ? assessment.slope_degrees + '°' : 'N/A'}</span>
873
- </div>
874
- <div class="metric">
875
- <span class="metric-label">Distance to Water</span>
876
- <span class="metric-value">
877
- ${assessment.distance_to_water_m !== null
878
- ? assessment.distance_to_water_m + ' m'
879
- : 'N/A'}
880
- </span>
881
- </div>
882
- `;
883
-
884
- // Multi-hazard breakdown
885
- if (assessment.hazard_breakdown) {
886
- const hb = assessment.hazard_breakdown;
887
- html += '<h3 style="margin-top: 25px;">Hazard Breakdown</h3>';
888
- html += '<div class="hazard-breakdown">';
889
- html += `
890
- <div class="hazard-card">
891
- <div>Fluvial/Riverine</div>
892
- <div class="hazard-value">${hb.fluvial_riverine}</div>
893
- </div>
894
- <div class="hazard-card">
895
- <div>Coastal Surge</div>
896
- <div class="hazard-value">${hb.coastal_surge}</div>
897
- </div>
898
- <div class="hazard-card">
899
- <div>Pluvial/Drainage</div>
900
- <div class="hazard-value">${hb.pluvial_drainage}</div>
901
- </div>
902
- `;
903
- html += '</div>';
904
- html += `<p><strong>Dominant Hazard:</strong> ${assessment.dominant_hazard.replace(/_/g, ' ').toUpperCase()}</p>`;
905
- }
906
-
907
- // SHAP explanation
908
- if (data.explanation) {
909
- const exp = data.explanation;
910
- html += '<h3 style="margin-top: 25px;">Risk Factor Explanation</h3>';
911
- html += `<p><strong>Top Risk Driver:</strong> ${exp.top_risk_driver}</p>`;
912
- html += '<div class="explanation-card">';
913
-
914
- exp.explanations.forEach(factor => {
915
- html += `
916
- <div class="factor-contribution">
917
- <span style="min-width: 150px;">${factor.factor}</span>
918
- <div class="contribution-bar">
919
- <div class="contribution-fill" style="width: ${factor.contribution_pct}%"></div>
920
- </div>
921
- <span style="min-width: 60px; text-align: right;">${factor.contribution_pct}%</span>
922
- </div>
923
- `;
924
- });
925
-
926
- html += '</div>';
927
- }
928
-
929
- resultsDiv.innerHTML = html;
930
- resultsDiv.style.display = 'block';
931
- }
932
-
933
- async function uploadBatch() {
934
- const fileInput = document.getElementById('csvFile');
935
- const file = fileInput.files[0];
936
-
937
- if (!file) {
938
- alert('Please select a CSV file');
939
- return;
940
- }
941
-
942
- document.getElementById('batch-loading').style.display = 'block';
943
- document.getElementById('batch-results').style.display = 'none';
944
- document.getElementById('batch-error').style.display = 'none';
945
-
946
- const formData = new FormData();
947
- formData.append('file', file);
948
-
949
- try {
950
- const mode = document.getElementById('batchMode').value;
951
- const usePredicted = document.getElementById('usePredictedHeight').checked;
952
- let endpoint = mode === 'multihazard' ? '/assess_batch_multihazard' : '/assess_batch';
953
-
954
- if (usePredicted) {
955
- const sep = endpoint.includes('?') ? '&' : '?';
956
- endpoint = endpoint + sep + 'use_predicted_height=true';
957
- }
958
-
959
- const response = await fetch(endpoint, {
960
- method: 'POST',
961
- body: formData
962
- });
963
-
964
- if (response.ok) {
965
- const blob = await response.blob();
966
- const url = window.URL.createObjectURL(blob);
967
- const a = document.createElement('a');
968
- a.href = url;
969
- const selectedMode = document.getElementById('batchMode').value;
970
- const filename = selectedMode === 'multihazard'
971
- ? 'multihazard_results.csv'
972
- : 'vulnerability_results.csv';
973
-
974
- a.download = filename;
975
- document.body.appendChild(a);
976
- a.click();
977
- window.URL.revokeObjectURL(url);
978
-
979
- document.getElementById('batch-results').innerHTML = '<p>✅ Batch processing complete! Results downloaded.</p>';
980
- document.getElementById('batch-results').style.display = 'block';
981
- } else {
982
- throw new Error('Batch processing failed');
983
- }
984
- } catch (error) {
985
- document.getElementById('batch-error').textContent = error.message;
986
- document.getElementById('batch-error').style.display = 'block';
987
- } finally {
988
- document.getElementById('batch-loading').style.display = 'none';
989
- }
990
- }
991
- </script>
992
-
993
- <footer
994
- style="text-align: center; padding: 15px; margin-top: 20px; background: #2c3e50; color: white; border-top: 2px solid #667eea; font-size: 0.9em;">
995
- © 2025 Flood Vulnerability Assessment | Made by ...
996
- </footer>
997
- </body>
998
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
999
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Flood Vulnerability Assessment</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ html, body {
15
+ height: 100%;
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ }
18
+
19
+ body {
20
+ background: #000;
21
+ color: #fff;
22
+ position: relative;
23
+ overflow-x: hidden;
24
+ }
25
+
26
+ /* Animated background */
27
+ .hero-background {
28
+ position: fixed;
29
+ top: 0;
30
+ left: 0;
31
+ width: 100%;
32
+ height: 100%;
33
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
34
+ z-index: 0;
35
+ }
36
+
37
+ .hero-background::before {
38
+ content: '';
39
+ position: absolute;
40
+ top: 0;
41
+ left: 0;
42
+ width: 100%;
43
+ height: 100%;
44
+ background:
45
+ radial-gradient(circle at 20% 50%, rgba(59, 130, 246, 0.15) 0%, transparent 50%),
46
+ radial-gradient(circle at 80% 50%, rgba(139, 92, 246, 0.15) 0%, transparent 50%);
47
+ animation: pulse 8s ease-in-out infinite;
48
+ }
49
+
50
+ @keyframes pulse {
51
+ 0%, 100% { opacity: 1; }
52
+ 50% { opacity: 0.5; }
53
+ }
54
+
55
+ .page-wrapper {
56
+ position: relative;
57
+ z-index: 1;
58
+ min-height: 100vh;
59
+ }
60
+
61
+ /* Header with hero section */
62
+ .hero-header {
63
+ position: relative;
64
+ padding: 4rem 2rem;
65
+ text-align: center;
66
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0.9) 0%, rgba(15, 23, 42, 0.7) 100%);
67
+ border-bottom: 2px solid rgba(59, 130, 246, 0.3);
68
+ }
69
+
70
+ .hero-header h1 {
71
+ font-size: 3.5em;
72
+ font-weight: 800;
73
+ margin-bottom: 1rem;
74
+ background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 50%, #60a5fa 100%);
75
+ -webkit-background-clip: text;
76
+ -webkit-text-fill-color: transparent;
77
+ background-clip: text;
78
+ background-size: 200% auto;
79
+ animation: shine 3s linear infinite;
80
+ }
81
+
82
+ @keyframes shine {
83
+ to { background-position: 200% center; }
84
+ }
85
+
86
+ .hero-header .subtitle {
87
+ font-size: 1.3em;
88
+ color: #94a3b8;
89
+ font-weight: 300;
90
+ max-width: 700px;
91
+ margin: 0 auto;
92
+ }
93
+
94
+ /* Side navigation */
95
+ .main-container {
96
+ display: flex;
97
+ max-width: 1400px;
98
+ margin: 0 auto;
99
+ padding: 2rem;
100
+ gap: 2rem;
101
+ }
102
+
103
+ .side-nav {
104
+ width: 280px;
105
+ flex-shrink: 0;
106
+ position: sticky;
107
+ top: 2rem;
108
+ height: fit-content;
109
+ }
110
+
111
+ .nav-card {
112
+ background: rgba(30, 41, 59, 0.8);
113
+ backdrop-filter: blur(20px);
114
+ border-radius: 20px;
115
+ padding: 1.5rem;
116
+ border: 1px solid rgba(59, 130, 246, 0.2);
117
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
118
+ }
119
+
120
+ .nav-card h3 {
121
+ color: #e2e8f0;
122
+ font-size: 1.1em;
123
+ margin-bottom: 1.5rem;
124
+ padding-bottom: 1rem;
125
+ border-bottom: 1px solid rgba(59, 130, 246, 0.2);
126
+ }
127
+
128
+ .nav-link {
129
+ display: block;
130
+ padding: 1rem 1.25rem;
131
+ margin-bottom: 0.5rem;
132
+ background: transparent;
133
+ border: none;
134
+ color: #94a3b8;
135
+ text-align: left;
136
+ cursor: pointer;
137
+ border-radius: 12px;
138
+ font-size: 0.95em;
139
+ transition: all 0.3s ease;
140
+ position: relative;
141
+ overflow: hidden;
142
+ width: 100%;
143
+ }
144
+
145
+ .nav-link::before {
146
+ content: '';
147
+ position: absolute;
148
+ left: 0;
149
+ top: 0;
150
+ height: 100%;
151
+ width: 3px;
152
+ background: linear-gradient(180deg, #3b82f6, #8b5cf6);
153
+ transform: scaleY(0);
154
+ transition: transform 0.3s ease;
155
+ }
156
+
157
+ .nav-link:hover {
158
+ background: rgba(59, 130, 246, 0.1);
159
+ color: #e2e8f0;
160
+ transform: translateX(5px);
161
+ }
162
+
163
+ .nav-link.active {
164
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2));
165
+ color: #fff;
166
+ font-weight: 600;
167
+ }
168
+
169
+ .nav-link.active::before {
170
+ transform: scaleY(1);
171
+ }
172
+
173
+ /* Content area */
174
+ .content-area {
175
+ flex: 1;
176
+ min-width: 0;
177
+ }
178
+
179
+ .assessment-card {
180
+ display: none;
181
+ background: rgba(30, 41, 59, 0.8);
182
+ backdrop-filter: blur(20px);
183
+ border-radius: 20px;
184
+ padding: 3rem;
185
+ border: 1px solid rgba(59, 130, 246, 0.2);
186
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
187
+ animation: slideIn 0.4s ease;
188
+ }
189
+
190
+ .assessment-card.active {
191
+ display: block;
192
+ }
193
+
194
+ @keyframes slideIn {
195
+ from {
196
+ opacity: 0;
197
+ transform: translateY(20px);
198
+ }
199
+ to {
200
+ opacity: 1;
201
+ transform: translateY(0);
202
+ }
203
+ }
204
+
205
+ .card-header {
206
+ margin-bottom: 2rem;
207
+ }
208
+
209
+ .card-header h2 {
210
+ font-size: 2em;
211
+ color: #e2e8f0;
212
+ margin-bottom: 0.5rem;
213
+ }
214
+
215
+ .card-header p {
216
+ color: #94a3b8;
217
+ font-size: 1.05em;
218
+ }
219
+
220
+ /* Form styling */
221
+ .form-grid {
222
+ display: grid;
223
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
224
+ gap: 1.5rem;
225
+ margin-bottom: 2rem;
226
+ }
227
+
228
+ .input-card {
229
+ background: rgba(15, 23, 42, 0.6);
230
+ border: 1px solid rgba(59, 130, 246, 0.2);
231
+ border-radius: 16px;
232
+ padding: 1.5rem;
233
+ transition: all 0.3s ease;
234
+ }
235
+
236
+ .input-card:hover {
237
+ border-color: rgba(59, 130, 246, 0.4);
238
+ background: rgba(15, 23, 42, 0.8);
239
+ transform: translateY(-2px);
240
+ }
241
+
242
+ .input-card label {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 0.5rem;
246
+ margin-bottom: 0.75rem;
247
+ color: #cbd5e1;
248
+ font-weight: 600;
249
+ font-size: 0.95em;
250
+ }
251
+
252
+ .input-card label::before {
253
+ content: '';
254
+ width: 4px;
255
+ height: 16px;
256
+ background: linear-gradient(180deg, #3b82f6, #8b5cf6);
257
+ border-radius: 2px;
258
+ }
259
+
260
+ .input-card input,
261
+ .input-card select {
262
+ width: 100%;
263
+ padding: 0.875rem;
264
+ background: rgba(0, 0, 0, 0.3);
265
+ border: 1px solid rgba(148, 163, 184, 0.2);
266
+ border-radius: 10px;
267
+ color: #e2e8f0;
268
+ font-size: 1em;
269
+ transition: all 0.3s ease;
270
+ }
271
+
272
+ .input-card input:focus,
273
+ .input-card select:focus {
274
+ outline: none;
275
+ border-color: #3b82f6;
276
+ background: rgba(0, 0, 0, 0.5);
277
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
278
+ }
279
+
280
+ .input-card .helper-text {
281
+ margin-top: 0.5rem;
282
+ font-size: 0.8em;
283
+ color: #64748b;
284
+ font-style: italic;
285
+ }
286
+
287
+ /* Height group with predict button */
288
+ .height-group {
289
+ display: flex;
290
+ align-items: stretch;
291
+ gap: 0;
292
+ background: rgba(0, 0, 0, 0.3);
293
+ border: 1px solid rgba(148, 163, 184, 0.2);
294
+ border-radius: 10px;
295
+ overflow: hidden;
296
+ transition: all 0.3s ease;
297
+ }
298
+
299
+ .height-group:focus-within {
300
+ border-color: #3b82f6;
301
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
302
+ }
303
+
304
+ .height-group input {
305
+ flex: 1;
306
+ padding: 0.875rem;
307
+ background: transparent !important;
308
+ border: none !important;
309
+ border-radius: 0 !important;
310
+ color: #e2e8f0;
311
+ font-size: 1em;
312
+ box-shadow: none !important;
313
+ }
314
+
315
+ .height-group input:focus {
316
+ outline: none;
317
+ }
318
+
319
+ .height-group button {
320
+ width: auto !important;
321
+ padding: 0.875rem 1.5rem !important;
322
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6) !important;
323
+ color: white;
324
+ border: none;
325
+ border-left: 1px solid rgba(148, 163, 184, 0.3);
326
+ cursor: pointer;
327
+ font-size: 0.9em;
328
+ font-weight: 600;
329
+ white-space: nowrap;
330
+ transition: opacity 0.2s ease;
331
+ border-radius: 0 !important;
332
+ transform: none !important;
333
+ }
334
+
335
+ .height-group button:hover {
336
+ opacity: 0.9;
337
+ transform: none !important;
338
+ }
339
+
340
+ .height-group button:disabled {
341
+ opacity: 0.5;
342
+ cursor: not-allowed;
343
+ }
344
+
345
+ /* Checkbox styling */
346
+ .checkbox-row {
347
+ display: flex;
348
+ align-items: center;
349
+ gap: 0.75rem;
350
+ padding: 1rem;
351
+ background: rgba(15, 23, 42, 0.6);
352
+ border: 1px solid rgba(59, 130, 246, 0.2);
353
+ border-radius: 12px;
354
+ cursor: pointer;
355
+ transition: all 0.3s ease;
356
+ }
357
+
358
+ .checkbox-row:hover {
359
+ background: rgba(15, 23, 42, 0.8);
360
+ border-color: rgba(59, 130, 246, 0.4);
361
+ }
362
+
363
+ .checkbox-row input[type="checkbox"] {
364
+ width: 20px;
365
+ height: 20px;
366
+ cursor: pointer;
367
+ accent-color: #3b82f6;
368
+ }
369
+
370
+ .checkbox-row label {
371
+ margin: 0 !important;
372
+ color: #cbd5e1;
373
+ font-weight: 500;
374
+ cursor: pointer;
375
+ flex: 1;
376
+ }
377
+
378
+ .checkbox-row label::before {
379
+ display: none;
380
+ }
381
+
382
+ /* Action buttons */
383
+ .action-section {
384
+ margin-top: 2rem;
385
+ padding-top: 2rem;
386
+ border-top: 1px solid rgba(59, 130, 246, 0.2);
387
+ }
388
+
389
+ .primary-button {
390
+ position: relative;
391
+ width: 100%;
392
+ padding: 1.25rem 2rem;
393
+ background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
394
+ color: white;
395
+ border: none;
396
+ border-radius: 12px;
397
+ font-size: 1.1em;
398
+ font-weight: 700;
399
+ cursor: pointer;
400
+ overflow: hidden;
401
+ transition: all 0.3s ease;
402
+ }
403
+
404
+ .primary-button::before {
405
+ content: '';
406
+ position: absolute;
407
+ top: 0;
408
+ left: -100%;
409
+ width: 100%;
410
+ height: 100%;
411
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
412
+ transition: left 0.5s;
413
+ }
414
+
415
+ .primary-button:hover {
416
+ transform: translateY(-3px);
417
+ box-shadow: 0 10px 30px rgba(59, 130, 246, 0.4);
418
+ }
419
+
420
+ .primary-button:hover::before {
421
+ left: 100%;
422
+ }
423
+
424
+ .primary-button:active {
425
+ transform: translateY(-1px);
426
+ }
427
+
428
+ .primary-button:disabled {
429
+ background: #334155;
430
+ cursor: not-allowed;
431
+ transform: none;
432
+ }
433
+
434
+ /* Loading state */
435
+ .loading-state {
436
+ display: none;
437
+ text-align: center;
438
+ padding: 3rem;
439
+ }
440
+
441
+ .loading-spinner {
442
+ width: 60px;
443
+ height: 60px;
444
+ margin: 0 auto 1rem;
445
+ border: 4px solid rgba(59, 130, 246, 0.2);
446
+ border-top-color: #3b82f6;
447
+ border-radius: 50%;
448
+ animation: spin 1s linear infinite;
449
+ }
450
+
451
+ @keyframes spin {
452
+ to { transform: rotate(360deg); }
453
+ }
454
+
455
+ .loading-state p {
456
+ color: #94a3b8;
457
+ font-size: 1.1em;
458
+ }
459
+
460
+ /* Results section */
461
+ .results-section {
462
+ display: none;
463
+ margin-top: 2rem;
464
+ }
465
+
466
+ .results-header {
467
+ text-align: center;
468
+ padding: 2rem;
469
+ margin-bottom: 2rem;
470
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(139, 92, 246, 0.15));
471
+ border-radius: 16px;
472
+ border: 1px solid rgba(59, 130, 246, 0.3);
473
+ }
474
+
475
+ .results-header h2 {
476
+ font-size: 2em;
477
+ margin-bottom: 1rem;
478
+ color: #e2e8f0;
479
+ }
480
+
481
+ .risk-badge {
482
+ display: inline-block;
483
+ padding: 0.75rem 2rem;
484
+ border-radius: 30px;
485
+ font-weight: 700;
486
+ font-size: 1.2em;
487
+ text-transform: uppercase;
488
+ letter-spacing: 1px;
489
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
490
+ }
491
+
492
+ .risk-very-high {
493
+ background: linear-gradient(135deg, #dc2626, #b91c1c);
494
+ box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
495
+ }
496
+ .risk-high {
497
+ background: linear-gradient(135deg, #ea580c, #c2410c);
498
+ box-shadow: 0 4px 15px rgba(234, 88, 12, 0.4);
499
+ }
500
+ .risk-moderate {
501
+ background: linear-gradient(135deg, #ca8a04, #a16207);
502
+ box-shadow: 0 4px 15px rgba(202, 138, 4, 0.4);
503
+ }
504
+ .risk-low {
505
+ background: linear-gradient(135deg, #16a34a, #15803d);
506
+ box-shadow: 0 4px 15px rgba(22, 163, 74, 0.4);
507
+ }
508
+ .risk-very-low {
509
+ background: linear-gradient(135deg, #0891b2, #0e7490);
510
+ box-shadow: 0 4px 15px rgba(8, 145, 178, 0.4);
511
+ }
512
+
513
+ /* Stats grid */
514
+ .stats-grid {
515
+ display: grid;
516
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
517
+ gap: 1.5rem;
518
+ margin-bottom: 2rem;
519
+ }
520
+
521
+ .stat-card {
522
+ background: rgba(15, 23, 42, 0.6);
523
+ border: 1px solid rgba(59, 130, 246, 0.2);
524
+ border-radius: 16px;
525
+ padding: 1.5rem;
526
+ transition: all 0.3s ease;
527
+ }
528
+
529
+ .stat-card:hover {
530
+ border-color: rgba(59, 130, 246, 0.5);
531
+ transform: translateY(-5px);
532
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
533
+ }
534
+
535
+ .stat-label {
536
+ font-size: 0.9em;
537
+ color: #94a3b8;
538
+ text-transform: uppercase;
539
+ letter-spacing: 0.5px;
540
+ margin-bottom: 0.5rem;
541
+ }
542
+
543
+ .stat-value {
544
+ font-size: 2em;
545
+ font-weight: 700;
546
+ background: linear-gradient(135deg, #60a5fa, #a78bfa);
547
+ -webkit-background-clip: text;
548
+ -webkit-text-fill-color: transparent;
549
+ background-clip: text;
550
+ }
551
+
552
+ /* Detail sections */
553
+ .detail-section {
554
+ background: rgba(15, 23, 42, 0.6);
555
+ border: 1px solid rgba(59, 130, 246, 0.2);
556
+ border-radius: 16px;
557
+ padding: 2rem;
558
+ margin-bottom: 1.5rem;
559
+ }
560
+
561
+ .detail-section h3 {
562
+ font-size: 1.5em;
563
+ color: #e2e8f0;
564
+ margin-bottom: 1.5rem;
565
+ padding-bottom: 1rem;
566
+ border-bottom: 1px solid rgba(59, 130, 246, 0.2);
567
+ }
568
+
569
+ .metric-row {
570
+ display: flex;
571
+ justify-content: space-between;
572
+ align-items: center;
573
+ padding: 1rem 0;
574
+ border-bottom: 1px solid rgba(59, 130, 246, 0.1);
575
+ }
576
+
577
+ .metric-row:last-child {
578
+ border-bottom: none;
579
+ }
580
+
581
+ .metric-label {
582
+ color: #94a3b8;
583
+ font-weight: 500;
584
+ }
585
+
586
+ .metric-value {
587
+ color: #e2e8f0;
588
+ font-weight: 700;
589
+ font-size: 1.1em;
590
+ }
591
+
592
+ /* Confidence visualization */
593
+ .confidence-section {
594
+ background: rgba(15, 23, 42, 0.6);
595
+ border: 1px solid rgba(59, 130, 246, 0.2);
596
+ border-radius: 16px;
597
+ padding: 2rem;
598
+ margin-bottom: 1.5rem;
599
+ }
600
+
601
+ .confidence-bar-wrapper {
602
+ display: flex;
603
+ align-items: center;
604
+ gap: 1rem;
605
+ margin: 1.5rem 0;
606
+ }
607
+
608
+ .confidence-bar-wrapper span {
609
+ font-size: 0.85em;
610
+ color: #64748b;
611
+ font-weight: 600;
612
+ }
613
+
614
+ .confidence-bar {
615
+ flex: 1;
616
+ height: 40px;
617
+ background: rgba(0, 0, 0, 0.4);
618
+ border-radius: 20px;
619
+ overflow: hidden;
620
+ position: relative;
621
+ border: 1px solid rgba(59, 130, 246, 0.2);
622
+ }
623
+
624
+ .confidence-fill {
625
+ height: 100%;
626
+ transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
627
+ position: relative;
628
+ }
629
+
630
+ .confidence-high {
631
+ background: linear-gradient(90deg, #16a34a, #22c55e);
632
+ box-shadow: 0 0 20px rgba(34, 197, 94, 0.4);
633
+ }
634
+ .confidence-moderate-fill {
635
+ background: linear-gradient(90deg, #ca8a04, #fbbf24);
636
+ box-shadow: 0 0 20px rgba(251, 191, 36, 0.4);
637
+ }
638
+ .confidence-low-fill {
639
+ background: linear-gradient(90deg, #ea580c, #f97316);
640
+ box-shadow: 0 0 20px rgba(249, 115, 22, 0.4);
641
+ }
642
+
643
+ .confidence-text {
644
+ position: absolute;
645
+ left: 50%;
646
+ top: 50%;
647
+ transform: translate(-50%, -50%);
648
+ font-weight: 800;
649
+ color: white;
650
+ font-size: 1.1em;
651
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
652
+ z-index: 2;
653
+ }
654
+
655
+ /* Hazard breakdown */
656
+ .hazard-grid {
657
+ display: grid;
658
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
659
+ gap: 1.5rem;
660
+ margin: 1.5rem 0;
661
+ }
662
+
663
+ .hazard-card {
664
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(139, 92, 246, 0.1));
665
+ border: 1px solid rgba(59, 130, 246, 0.3);
666
+ border-radius: 16px;
667
+ padding: 2rem;
668
+ text-align: center;
669
+ transition: all 0.3s ease;
670
+ }
671
+
672
+ .hazard-card:hover {
673
+ transform: scale(1.05);
674
+ border-color: rgba(59, 130, 246, 0.5);
675
+ box-shadow: 0 10px 30px rgba(59, 130, 246, 0.3);
676
+ }
677
+
678
+ .hazard-type {
679
+ font-size: 0.9em;
680
+ color: #94a3b8;
681
+ text-transform: uppercase;
682
+ letter-spacing: 0.5px;
683
+ margin-bottom: 1rem;
684
+ }
685
+
686
+ .hazard-value {
687
+ font-size: 3em;
688
+ font-weight: 800;
689
+ background: linear-gradient(135deg, #60a5fa, #a78bfa);
690
+ -webkit-background-clip: text;
691
+ -webkit-text-fill-color: transparent;
692
+ background-clip: text;
693
+ }
694
+
695
+ /* Explanation section */
696
+ .explanation-section {
697
+ background: rgba(15, 23, 42, 0.6);
698
+ border: 1px solid rgba(59, 130, 246, 0.2);
699
+ border-radius: 16px;
700
+ padding: 2rem;
701
+ margin-bottom: 1.5rem;
702
+ }
703
+
704
+ .factor-item {
705
+ display: flex;
706
+ align-items: center;
707
+ gap: 1rem;
708
+ margin: 1rem 0;
709
+ padding: 1rem;
710
+ background: rgba(0, 0, 0, 0.3);
711
+ border-radius: 12px;
712
+ }
713
+
714
+ .factor-name {
715
+ min-width: 200px;
716
+ color: #cbd5e1;
717
+ font-weight: 600;
718
+ }
719
+
720
+ .factor-bar {
721
+ flex: 1;
722
+ height: 28px;
723
+ background: rgba(59, 130, 246, 0.1);
724
+ border-radius: 14px;
725
+ overflow: hidden;
726
+ border: 1px solid rgba(59, 130, 246, 0.2);
727
+ }
728
+
729
+ .factor-fill {
730
+ height: 100%;
731
+ background: linear-gradient(90deg, #3b82f6, #8b5cf6);
732
+ transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
733
+ box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
734
+ }
735
+
736
+ .factor-percentage {
737
+ min-width: 60px;
738
+ text-align: right;
739
+ color: #e2e8f0;
740
+ font-weight: 700;
741
+ font-size: 1.05em;
742
+ }
743
+
744
+ /* Quality warnings */
745
+ .quality-warning {
746
+ background: linear-gradient(135deg, rgba(202, 138, 4, 0.2), rgba(161, 98, 7, 0.2));
747
+ border: 1px solid rgba(202, 138, 4, 0.4);
748
+ border-left: 4px solid #ca8a04;
749
+ border-radius: 12px;
750
+ padding: 1.5rem;
751
+ margin: 1.5rem 0;
752
+ }
753
+
754
+ .quality-warning h4 {
755
+ color: #fbbf24;
756
+ margin-bottom: 1rem;
757
+ display: flex;
758
+ align-items: center;
759
+ gap: 0.5rem;
760
+ }
761
+
762
+ .quality-warning ul {
763
+ list-style: none;
764
+ padding: 0;
765
+ }
766
+
767
+ .quality-warning li {
768
+ padding: 0.5rem 0;
769
+ color: #fde047;
770
+ padding-left: 1.5rem;
771
+ position: relative;
772
+ }
773
+
774
+ .quality-warning li::before {
775
+ content: '⚠';
776
+ position: absolute;
777
+ left: 0;
778
+ }
779
+
780
+ /* Error state */
781
+ .error-message {
782
+ background: linear-gradient(135deg, rgba(220, 38, 38, 0.2), rgba(185, 28, 28, 0.2));
783
+ border: 1px solid rgba(220, 38, 38, 0.4);
784
+ border-left: 4px solid #dc2626;
785
+ color: #fca5a5;
786
+ padding: 1.5rem;
787
+ border-radius: 12px;
788
+ margin: 1.5rem 0;
789
+ display: none;
790
+ }
791
+
792
+ /* Footer */
793
+ footer {
794
+ margin-top: 4rem;
795
+ padding: 2rem;
796
+ text-align: center;
797
+ color: #64748b;
798
+ border-top: 1px solid rgba(59, 130, 246, 0.2);
799
+ background: rgba(15, 23, 42, 0.6);
800
+ }
801
+
802
+ /* File upload styling */
803
+ input[type="file"] {
804
+ cursor: pointer;
805
+ padding: 1rem !important;
806
+ }
807
+
808
+ input[type="file"]::file-selector-button {
809
+ padding: 0.5rem 1rem;
810
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
811
+ color: white;
812
+ border: none;
813
+ border-radius: 8px;
814
+ cursor: pointer;
815
+ margin-right: 1rem;
816
+ transition: all 0.3s;
817
+ }
818
+
819
+ input[type="file"]::file-selector-button:hover {
820
+ transform: translateY(-2px);
821
+ box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
822
+ }
823
+
824
+ /* Pulse animation for predicted height */
825
+ @keyframes height-pulse {
826
+ 0%, 100% { background: rgba(0, 0, 0, 0.3); }
827
+ 50% { background: rgba(59, 130, 246, 0.3); }
828
+ }
829
+
830
+ .height-pulse {
831
+ animation: height-pulse 0.8s ease;
832
+ }
833
+
834
+ /* Responsive design */
835
+ @media (max-width: 1024px) {
836
+ .main-container {
837
+ flex-direction: column;
838
+ }
839
+
840
+ .side-nav {
841
+ width: 100%;
842
+ position: static;
843
+ }
844
+
845
+ .nav-card {
846
+ display: flex;
847
+ overflow-x: auto;
848
+ padding: 1rem;
849
+ }
850
+
851
+ .nav-card h3 {
852
+ display: none;
853
+ }
854
+
855
+ .nav-link {
856
+ white-space: nowrap;
857
+ margin-right: 0.5rem;
858
+ margin-bottom: 0;
859
+ }
860
+ }
861
+
862
+ @media (max-width: 768px) {
863
+ .hero-header h1 {
864
+ font-size: 2em;
865
+ }
866
+
867
+ .form-grid {
868
+ grid-template-columns: 1fr;
869
+ }
870
+
871
+ .stats-grid {
872
+ grid-template-columns: 1fr;
873
+ }
874
+
875
+ .assessment-card {
876
+ padding: 1.5rem;
877
+ }
878
+
879
+ .factor-name {
880
+ min-width: 120px;
881
+ font-size: 0.9em;
882
+ }
883
+ }
884
+ </style>
885
+ </head>
886
+ <body>
887
+ <div class="hero-background"></div>
888
+
889
+ <div class="page-wrapper">
890
+ <header class="hero-header" style="position: relative; padding-top: 40px; text-align: center;">
891
+
892
+ <div style="position: absolute; top: 10; left: 0; font-size: 3rem; padding: 10px;">
893
+ 💦💧🌊
894
+ </div>
895
+
896
+ <div>
897
+ <h1 style="margin: 0;">Flood Vulnerability Assessment</h1>
898
+ <p class="subtitle" style="margin-top: -2px">
899
+ Global building-level assessment
900
+ </p>
901
+ </div>
902
+
903
+ </header>
904
+
905
+ <div class="main-container">
906
+ <!-- Side Navigation -->
907
+ <aside class="side-nav">
908
+ <div class="nav-card">
909
+ <h3>Assessment Tools</h3>
910
+ <button class="nav-link active" onclick="switchTab('basic')">Basic Assessment</button>
911
+ <button class="nav-link" onclick="switchTab('explained')">Detailed Explanation</button>
912
+ <button class="nav-link" onclick="switchTab('multihazard')">Multi-Hazard Analysis</button>
913
+ <button class="nav-link" onclick="switchTab('batch')">Batch Processing</button>
914
+ </div>
915
+ </aside>
916
+
917
+ <!-- Content Area -->
918
+ <main class="content-area">
919
+ <!-- Basic Assessment -->
920
+ <div id="basic-card" class="assessment-card active">
921
+ <div class="card-header">
922
+ <h2>Basic Assessment</h2>
923
+ <p>Quick vulnerability analysis for a single location</p>
924
+ </div>
925
+
926
+ <form onsubmit="assessLocation(event, '/assess', 'basic-results')">
927
+ <div class="form-grid">
928
+ <div class="input-card">
929
+ <label for="latitude">Latitude</label>
930
+ <input type="text" id="latitude" name="latitude" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" required placeholder="40.7128">
931
+ <p class="helper-text">Range: -90 to 90</p>
932
+ </div>
933
+
934
+ <div class="input-card">
935
+ <label for="longitude">Longitude</label>
936
+ <input type="text" id="longitude" name="longitude" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="-74.0060">
937
+ <p class="helper-text">Range: -180 to 180</p>
938
+ </div>
939
+
940
+ <div class="input-card">
941
+ <label for="height">Building Height</label>
942
+ <div class="height-group">
943
+ <input type="number" id="height" step="any" value="0" placeholder="5.0">
944
+ <button type="button" class="predict-height-btn" data-lat-id="latitude" data-lon-id="longitude" data-height-id="height" data-error-id="basic-error">Predict</button>
945
+ </div>
946
+ <p class="helper-text">Meters above ground (0 = ground level)</p>
947
+ </div>
948
+
949
+ <div class="input-card">
950
+ <label for="basement">Basement Depth</label>
951
+ <input type="text" id="basement" name="basement" value="0" max="0" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" placeholder="-2.0">
952
+ <p class="helper-text">Meters below ground (negative values)</p>
953
+ </div>
954
+ </div>
955
+
956
+ <div class="action-section">
957
+ <button type="submit" class="primary-button">Analyze Location</button>
958
+ </div>
959
+ </form>
960
+
961
+ <div class="loading-state" id="basic-loading">
962
+ <div class="loading-spinner"></div>
963
+ <p>Analyzing terrain and water proximity...</p>
964
+ </div>
965
+
966
+ <div class="error-message" id="basic-error"></div>
967
+ <div class="results-section" id="basic-results"></div>
968
+ </div>
969
+
970
+ <!-- Explained Assessment -->
971
+ <div id="explained-card" class="assessment-card">
972
+ <div class="card-header">
973
+ <h2>Detailed Explanation</h2>
974
+ <p>Get comprehensive risk factor analysis with SHAP explanations</p>
975
+ </div>
976
+
977
+ <form onsubmit="assessLocation(event, '/explain', 'explained-results')">
978
+ <div class="form-grid">
979
+ <div class="input-card">
980
+ <label for="latitude2">Latitude</label>
981
+ <input type="text" id="latitude2" name="latitude2" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="40.7128">
982
+ </div>
983
+
984
+ <div class="input-card">
985
+ <label for="longitude2">Longitude</label>
986
+ <input type="text" id="longitude2" name="longitude2" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="-74.0060">
987
+ </div>
988
+
989
+ <div class="input-card">
990
+ <label for="height2">Building Height</label>
991
+ <div class="height-group">
992
+ <input type="number" id="height2" step="any" value="0">
993
+ <button type="button" class="predict-height-btn" data-lat-id="latitude2" data-lon-id="longitude2" data-height-id="height2" data-error-id="explained-error">Predict</button>
994
+ </div>
995
+ <p class="helper-text">Meters above ground</p>
996
+ </div>
997
+
998
+ <div class="input-card">
999
+ <label for="basement2">Basement Depth</label>
1000
+ <input type="text" id="basement2" name="basement2" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" step="0.1" value="0" max="0">
1001
+ <p class="helper-text">Meters below ground (negative)</p>
1002
+ </div>
1003
+ </div>
1004
+
1005
+ <div class="action-section">
1006
+ <button type="submit" class="primary-button">Analyze with Explanation</button>
1007
+ </div>
1008
+ </form>
1009
+
1010
+ <div class="loading-state" id="explained-loading">
1011
+ <div class="loading-spinner"></div>
1012
+ <p>Analyzing and generating explanation...</p>
1013
+ </div>
1014
+
1015
+ <div class="error-message" id="explained-error"></div>
1016
+ <div class="results-section" id="explained-results"></div>
1017
+ </div>
1018
+
1019
+ <!-- Multi-Hazard Assessment -->
1020
+ <div id="multihazard-card" class="assessment-card">
1021
+ <div class="card-header">
1022
+ <h2>Multi-Hazard Analysis</h2>
1023
+ <p>Comprehensive assessment across fluvial, coastal, and pluvial hazards</p>
1024
+ </div>
1025
+
1026
+ <form onsubmit="assessLocation(event, '/assess_multihazard', 'multihazard-results')">
1027
+ <div class="form-grid">
1028
+ <div class="input-card">
1029
+ <label for="latitude3">Latitude</label>
1030
+ <input type="text" id="latitude3" name="latitude3" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="40.7128">
1031
+ </div>
1032
+
1033
+ <div class="input-card">
1034
+ <label for="longitude3">Longitude</label>
1035
+ <input type="text" id="longitude3" name="longitude3" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="-74.0060">
1036
+ </div>
1037
+
1038
+ <div class="input-card">
1039
+ <label for="height3">Building Height</label>
1040
+ <div class="height-group">
1041
+ <input type="number" id="height3" step="any" value="0">
1042
+ <button type="button" class="predict-height-btn" data-lat-id="latitude3" data-lon-id="longitude3" data-height-id="height3" data-error-id="multihazard-error">Predict</button>
1043
+ </div>
1044
+ <p class="helper-text">Meters above ground</p>
1045
+ </div>
1046
+
1047
+ <div class="input-card">
1048
+ <label for="basement3">Basement Depth</label>
1049
+ <input type="text" id="basement3" name="basement3" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" step="0.1" value="0" max="0">
1050
+ <p class="helper-text">Meters below ground (negative)</p>
1051
+ </div>
1052
+ </div>
1053
+
1054
+ <div class="action-section">
1055
+ <button type="submit" class="primary-button">Run Multi-Hazard Analysis</button>
1056
+ </div>
1057
+ </form>
1058
+
1059
+ <div class="loading-state" id="multihazard-loading">
1060
+ <div class="loading-spinner"></div>
1061
+ <p>Analyzing multiple flood hazards...</p>
1062
+ </div>
1063
+
1064
+ <div class="error-message" id="multihazard-error"></div>
1065
+ <div class="results-section" id="multihazard-results"></div>
1066
+ </div>
1067
+
1068
+ <!-- Batch Processing -->
1069
+ <div id="batch-card" class="assessment-card">
1070
+ <div class="card-header">
1071
+ <h2>Batch Processing</h2>
1072
+ <p>Upload a CSV file to assess multiple locations simultaneously</p>
1073
+ </div>
1074
+
1075
+ <div class="form-grid">
1076
+ <div class="input-card">
1077
+ <label for="batchMode">Analysis Mode</label>
1078
+ <select id="batchMode">
1079
+ <option value="standard">Basic Assessment Model</option>
1080
+ <option value="multihazard">Multi-Hazard Analysis</option>
1081
+ </select>
1082
+ <p class="helper-text">Choose the assessment type for batch processing</p>
1083
+ </div>
1084
+
1085
+ <div class="input-card">
1086
+ <label for="csvFile">CSV File</label>
1087
+ <input type="file" id="csvFile" accept=".csv">
1088
+ <p class="helper-text">Required columns: latitude, longitude in decimal degrees (e.g. 29.17, -95.31). Optional: height(meters), basement (should be negative).</p>
1089
+ </div>
1090
+ </div>
1091
+
1092
+ <div class="form-group" style="margin-top: 1.5rem;">
1093
+ <div class="checkbox-row">
1094
+ <input type="checkbox" id="usePredictedHeight">
1095
+ <label for="usePredictedHeight">Use satellite-predicted building height</label>
1096
+ </div>
1097
+ <p class="helper-text" style="margin-top: 0.75rem; color: #64748b;">
1098
+ For each row, estimate height from coordinates and use it in the vulnerability assessment.
1099
+ </p>
1100
+ </div>
1101
+
1102
+ <div class="action-section">
1103
+ <button class="primary-button" onclick="uploadBatch()">Process Batch</button>
1104
+ </div>
1105
+
1106
+ <div class="loading-state" id="batch-loading">
1107
+ <div class="loading-spinner"></div>
1108
+ <p>Processing batch assessments...</p>
1109
+ </div>
1110
+
1111
+ <div class="error-message" id="batch-error"></div>
1112
+ <div class="results-section" id="batch-results"></div>
1113
+ </div>
1114
+ </main>
1115
+ </div>
1116
+
1117
+ <footer>
1118
+ © 2025 Flood Vulnerability Assessment | Powered by Google Earth Engine
1119
+ </footer>
1120
+ </div>
1121
+
1122
+ <script>
1123
+ document.addEventListener('DOMContentLoaded', () => {
1124
+ const buttons = document.querySelectorAll('.predict-height-btn');
1125
+ if (!buttons.length) {
1126
+ return;
1127
+ }
1128
+
1129
+ buttons.forEach(button => {
1130
+ const latId = button.dataset.latId;
1131
+ const lonId = button.dataset.lonId;
1132
+ const heightId = button.dataset.heightId;
1133
+ const errorId = button.dataset.errorId;
1134
+
1135
+ button.addEventListener('click', () => {
1136
+ predictHeight(latId, lonId, heightId, errorId, button);
1137
+ });
1138
+ });
1139
+ });
1140
+
1141
+ async function predictHeight(latId, lonId, heightId, errorId, button) {
1142
+ const latInput = document.getElementById(latId);
1143
+ const lonInput = document.getElementById(lonId);
1144
+ const heightInput = document.getElementById(heightId);
1145
+ const errorBox = document.getElementById(errorId);
1146
+
1147
+ if (!latInput || !lonInput || !heightInput || !errorBox) {
1148
+ return;
1149
+ }
1150
+
1151
+ errorBox.style.display = 'none';
1152
+ errorBox.textContent = '';
1153
+
1154
+ const latitude = parseFloat(latInput.value);
1155
+ const longitude = parseFloat(lonInput.value);
1156
+
1157
+ if (isNaN(latitude) || isNaN(longitude)) {
1158
+ errorBox.textContent = 'Please enter latitude and longitude first.';
1159
+ errorBox.style.display = 'block';
1160
+ return;
1161
+ }
1162
+
1163
+ const originalText = button.textContent;
1164
+ button.disabled = true;
1165
+ button.textContent = 'Predicting...';
1166
+
1167
+ try {
1168
+ const response = await fetch('/predict_height', {
1169
+ method: 'POST',
1170
+ headers: { 'Content-Type': 'application/json' },
1171
+ body: JSON.stringify({
1172
+ latitude,
1173
+ longitude,
1174
+ height: 0,
1175
+ basement: 0
1176
+ })
1177
+ });
1178
+
1179
+ const data = await response.json();
1180
+ if (!response.ok || data.status !== 'success' || data.predicted_height == null) {
1181
+ const message = data.detail || data.error || 'Height prediction failed.';
1182
+ throw new Error(message);
1183
+ }
1184
+
1185
+ const h = Number(data.predicted_height);
1186
+ heightInput.value = h.toFixed(2);
1187
+ heightInput.classList.add('height-pulse');
1188
+ setTimeout(() => {
1189
+ heightInput.classList.remove('height-pulse');
1190
+ }, 800);
1191
+ } catch (err) {
1192
+ errorBox.textContent = err.message || 'Height prediction failed.';
1193
+ errorBox.style.display = 'block';
1194
+ } finally {
1195
+ button.disabled = false;
1196
+ button.textContent = originalText;
1197
+ }
1198
+ }
1199
+
1200
+ function switchTab(tabName) {
1201
+ document.querySelectorAll('.assessment-card').forEach(card => {
1202
+ card.classList.remove('active');
1203
+ });
1204
+ document.querySelectorAll('.nav-link').forEach(link => {
1205
+ link.classList.remove('active');
1206
+ });
1207
+
1208
+ document.getElementById(tabName + '-card').classList.add('active');
1209
+ event.target.classList.add('active');
1210
+ }
1211
+
1212
+ async function assessLocation(event, endpoint, resultsId) {
1213
+ event.preventDefault();
1214
+
1215
+ const tabName = resultsId.split('-')[0];
1216
+ const suffix = endpoint === '/assess' ? '' : (endpoint === '/explain' ? '2' : '3');
1217
+ const latitude = parseFloat(document.getElementById('latitude' + suffix).value);
1218
+ const longitude = parseFloat(document.getElementById('longitude' + suffix).value);
1219
+ const height = parseFloat(document.getElementById('height' + suffix).value) || 0;
1220
+ const basement = parseFloat(document.getElementById('basement' + suffix).value) || 0;
1221
+
1222
+ document.getElementById(tabName + '-loading').style.display = 'block';
1223
+ document.getElementById(resultsId).style.display = 'none';
1224
+ document.getElementById(tabName + '-error').style.display = 'none';
1225
+
1226
+ try {
1227
+ const response = await fetch(endpoint, {
1228
+ method: 'POST',
1229
+ headers: { 'Content-Type': 'application/json' },
1230
+ body: JSON.stringify({ latitude, longitude, height, basement })
1231
+ });
1232
+
1233
+ const data = await response.json();
1234
+
1235
+ if (data.status === 'success') {
1236
+ displayResults(data, resultsId, endpoint);
1237
+ } else {
1238
+ throw new Error(data.detail || 'Assessment failed');
1239
+ }
1240
+ } catch (error) {
1241
+ document.getElementById(tabName + '-error').textContent = error.message;
1242
+ document.getElementById(tabName + '-error').style.display = 'block';
1243
+ } finally {
1244
+ document.getElementById(tabName + '-loading').style.display = 'none';
1245
+ }
1246
+ }
1247
+
1248
+ function formatFlag(flag) {
1249
+ const flagMessages = {
1250
+ 'missing_elevation': 'Elevation data unavailable',
1251
+ 'missing_tpi': 'Topographic position data incomplete',
1252
+ 'missing_slope': 'Slope data incomplete',
1253
+ 'water_distance_unknown': 'Water proximity uncertain',
1254
+ 'far_from_water_search_limited': 'Far from major water bodies (search radius limited)',
1255
+ 'steep_terrain_dem_error_high': 'Steep terrain increases measurement uncertainty',
1256
+ 'coastal_surge_risk_not_modeled': 'Coastal surge dynamics not fully captured'
1257
+ };
1258
+ return flagMessages[flag] || flag.replace(/_/g, ' ');
1259
+ }
1260
+
1261
+ function displayResults(data, resultsId, endpoint) {
1262
+ const resultsDiv = document.getElementById(resultsId);
1263
+ const assessment = data.assessment;
1264
+
1265
+ let html = '<div class="results-header">';
1266
+ html += '<h2>Assessment Complete</h2>';
1267
+
1268
+ const riskClass = 'risk-' + assessment.risk_level.replace(/_/g, '-');
1269
+ html += `<div class="risk-badge ${riskClass}">${assessment.risk_level.replace(/_/g, ' ')}</div>`;
1270
+ html += '</div>';
1271
+
1272
+ html += '<div class="stats-grid">';
1273
+
1274
+ if (assessment.confidence_interval) {
1275
+ const ci = assessment.confidence_interval;
1276
+ html += `
1277
+ <div class="stat-card">
1278
+ <div class="stat-label">Vulnerability Index</div>
1279
+ <div class="stat-value">${ci.point_estimate}</div>
1280
+ <p style="font-size: 0.85em; color: #64748b; margin-top: 0.5rem;">
1281
+ 95% CI: ${ci.lower_bound_95}–${ci.upper_bound_95}
1282
+ </p>
1283
+ </div>
1284
+ `;
1285
+ } else {
1286
+ html += `
1287
+ <div class="stat-card">
1288
+ <div class="stat-label">Vulnerability Index</div>
1289
+ <div class="stat-value">${assessment.vulnerability_index}</div>
1290
+ </div>
1291
+ `;
1292
+ }
1293
+
1294
+ html += `
1295
+ <div class="stat-card">
1296
+ <div class="stat-label">Elevation</div>
1297
+ <div class="stat-value">${assessment.elevation_m}m</div>
1298
+ </div>
1299
+ `;
1300
+
1301
+ if (assessment.distance_to_water_m !== null) {
1302
+ html += `
1303
+ <div class="stat-card">
1304
+ <div class="stat-label">Distance to Water</div>
1305
+ <div class="stat-value">${assessment.distance_to_water_m}m</div>
1306
+ </div>
1307
+ `;
1308
+ }
1309
+
1310
+ html += '</div>';
1311
+
1312
+ if (assessment.uncertainty_analysis) {
1313
+ const ua = assessment.uncertainty_analysis;
1314
+ const confidenceValue = parseFloat(ua.confidence) || 0;
1315
+ const barWidth = Math.round(confidenceValue * 100);
1316
+
1317
+ let confidenceClass = 'confidence-low-fill';
1318
+ if (confidenceValue >= 0.75) confidenceClass = 'confidence-high';
1319
+ else if (confidenceValue >= 0.55) confidenceClass = 'confidence-moderate-fill';
1320
+
1321
+ html += `
1322
+ <div class="confidence-section">
1323
+ <h3>Assessment Confidence</h3>
1324
+ <div class="confidence-bar-wrapper">
1325
+ <span>Low</span>
1326
+ <div class="confidence-bar">
1327
+ <div class="confidence-fill ${confidenceClass}" style="width: ${barWidth}%;"></div>
1328
+ <span class="confidence-text">${barWidth}%</span>
1329
+ </div>
1330
+ <span>High</span>
1331
+ </div>
1332
+ <p style="margin-top: 1rem; color: #94a3b8; font-style: italic;">
1333
+ ${ua.interpretation}
1334
+ </p>
1335
+ </div>
1336
+ `;
1337
+
1338
+ if (ua.data_quality_flags && ua.data_quality_flags.length > 0) {
1339
+ const criticalFlags = ua.data_quality_flags.filter(flag =>
1340
+ flag === 'steep_terrain_dem_error_high' ||
1341
+ flag === 'coastal_surge_risk_not_modeled'
1342
+ );
1343
+
1344
+ if (criticalFlags.length > 0) {
1345
+ html += '<div class="quality-warning"><h4>⚠ Data Quality Notes</h4><ul>';
1346
+ criticalFlags.forEach(flag => {
1347
+ html += `<li>${formatFlag(flag)}</li>`;
1348
+ });
1349
+ html += '</ul></div>';
1350
+ }
1351
+ }
1352
+ }
1353
+
1354
+ html += '<div class="detail-section"><h3>Terrain Analysis</h3>';
1355
+ html += `
1356
+ <div class="metric-row">
1357
+ <span class="metric-label">Elevation</span>
1358
+ <span class="metric-value">${assessment.elevation_m} m</span>
1359
+ </div>
1360
+ <div class="metric-row">
1361
+ <span class="metric-label">Relative Elevation (TPI)</span>
1362
+ <span class="metric-value">${assessment.relative_elevation_m !== null ? assessment.relative_elevation_m + ' m' : 'N/A'}</span>
1363
+ </div>
1364
+ <div class="metric-row">
1365
+ <span class="metric-label">Slope</span>
1366
+ <span class="metric-value">${assessment.slope_degrees !== null ? assessment.slope_degrees + '°' : 'N/A'}</span>
1367
+ </div>
1368
+ <div class="metric-row">
1369
+ <span class="metric-label">Distance to Water</span>
1370
+ <span class="metric-value">${assessment.distance_to_water_m !== null ? assessment.distance_to_water_m + ' m' : 'N/A'}</span>
1371
+ </div>
1372
+ `;
1373
+ html += '</div>';
1374
+
1375
+ if (assessment.hazard_breakdown) {
1376
+ const hb = assessment.hazard_breakdown;
1377
+ html += '<div class="detail-section"><h3>Hazard Breakdown</h3>';
1378
+ html += '<div class="hazard-grid">';
1379
+ html += `
1380
+ <div class="hazard-card">
1381
+ <div class="hazard-type">Fluvial/Riverine</div>
1382
+ <div class="hazard-value">${hb.fluvial_riverine}</div>
1383
+ </div>
1384
+ <div class="hazard-card">
1385
+ <div class="hazard-type">Coastal Surge</div>
1386
+ <div class="hazard-value">${hb.coastal_surge}</div>
1387
+ </div>
1388
+ <div class="hazard-card">
1389
+ <div class="hazard-type">Pluvial/Drainage</div>
1390
+ <div class="hazard-value">${hb.pluvial_drainage}</div>
1391
+ </div>
1392
+ `;
1393
+ html += '</div>';
1394
+ html += `<p style="margin-top: 1.5rem;"><strong>Dominant Hazard:</strong> ${assessment.dominant_hazard.replace(/_/g, ' ').toUpperCase()}</p>`;
1395
+ html += '</div>';
1396
+ }
1397
+
1398
+ if (data.explanation) {
1399
+ const exp = data.explanation;
1400
+ html += '<div class="explanation-section">';
1401
+ html += '<h3>Risk Factor Analysis</h3>';
1402
+ html += `<p style="margin-bottom: 1.5rem; color: #cbd5e1;"><strong>Top Risk Driver:</strong> ${exp.top_risk_driver}</p>`;
1403
+
1404
+ exp.explanations.forEach(factor => {
1405
+ html += `
1406
+ <div class="factor-item">
1407
+ <span class="factor-name">${factor.factor}</span>
1408
+ <div class="factor-bar">
1409
+ <div class="factor-fill" style="width: ${factor.contribution_pct}%"></div>
1410
+ </div>
1411
+ <span class="factor-percentage">${factor.contribution_pct}%</span>
1412
+ </div>
1413
+ `;
1414
+ });
1415
+
1416
+ html += '</div>';
1417
+ }
1418
+
1419
+ resultsDiv.innerHTML = html;
1420
+ resultsDiv.style.display = 'block';
1421
+ }
1422
+
1423
+ async function uploadBatch() {
1424
+ const fileInput = document.getElementById('csvFile');
1425
+ const file = fileInput.files[0];
1426
+
1427
+ if (!file) {
1428
+ alert('Please select a CSV file');
1429
+ return;
1430
+ }
1431
+
1432
+ document.getElementById('batch-loading').style.display = 'block';
1433
+ document.getElementById('batch-results').style.display = 'none';
1434
+ document.getElementById('batch-error').style.display = 'none';
1435
+
1436
+ const formData = new FormData();
1437
+ formData.append('file', file);
1438
+
1439
+ try {
1440
+ const mode = document.getElementById('batchMode').value;
1441
+ const usePredicted = document.getElementById('usePredictedHeight').checked;
1442
+ let endpoint = mode === 'multihazard' ? '/assess_batch_multihazard' : '/assess_batch';
1443
+
1444
+ if (usePredicted) {
1445
+ const sep = endpoint.includes('?') ? '&' : '?';
1446
+ endpoint = endpoint + sep + 'use_predicted_height=true';
1447
+ }
1448
+
1449
+ const response = await fetch(endpoint, {
1450
+ method: 'POST',
1451
+ body: formData
1452
+ });
1453
+
1454
+ if (response.ok) {
1455
+ const blob = await response.blob();
1456
+ const url = window.URL.createObjectURL(blob);
1457
+ const a = document.createElement('a');
1458
+ a.href = url;
1459
+ const filename = mode === 'multihazard'
1460
+ ? 'multihazard_results.csv'
1461
+ : 'vulnerability_results.csv';
1462
+ a.download = filename;
1463
+ document.body.appendChild(a);
1464
+ a.click();
1465
+ window.URL.revokeObjectURL(url);
1466
+
1467
+ document.getElementById('batch-results').innerHTML = `
1468
+ <div class="results-header">
1469
+ <h2>✓ Processing Complete</h2>
1470
+ <p style="color: #94a3b8; margin-top: 1rem;">Results downloaded as ${filename}</p>
1471
+ </div>
1472
+ `;
1473
+ document.getElementById('batch-results').style.display = 'block';
1474
+ } else {
1475
+ throw new Error('Batch processing failed');
1476
+ }
1477
+ } catch (error) {
1478
+ document.getElementById('batch-error').textContent = error.message;
1479
+ document.getElementById('batch-error').style.display = 'block';
1480
+ } finally {
1481
+ document.getElementById('batch-loading').style.display = 'none';
1482
+ }
1483
+ }
1484
+ </script>
1485
+ </body>
1486
  </html>