adema5051 commited on
Commit
a5d7fb6
·
verified ·
1 Parent(s): d5efbd9

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +999 -862
templates/index.html CHANGED
@@ -1,862 +1,999 @@
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
- html, body {
9
- height: 100%;
10
- margin: 0;
11
- padding: 0;
12
- }
13
-
14
-
15
- body {
16
- display: flex;
17
- flex-direction: column;
18
- min-height: 100vh;
19
-
20
-
21
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
22
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
23
- padding: 20px;
24
- }
25
-
26
-
27
- .container {
28
- flex: 1 0 auto;
29
- width: 100%;
30
- max-width: 950px;
31
- margin: 0 auto;
32
- background: white;
33
- border-radius: 15px;
34
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
35
- overflow: hidden;
36
- }
37
-
38
-
39
- footer {
40
- flex-shrink: 0;
41
- }
42
-
43
- .header {
44
- background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
45
- color: white;
46
- padding: 30px;
47
- text-align: center;
48
- }
49
- h1 { font-size: 2em; margin-bottom: 10px; }
50
- .subtitle { opacity: 0.9; font-size: 0.95em; }
51
-
52
- .tabs {
53
- display: flex;
54
- justify-content: space-between;
55
- background: #e6e8ef;
56
- border-radius: 10px;
57
- border: 1px solid #d0d3da;
58
- margin: 20px;
59
- overflow: hidden;
60
- box-shadow: 0 1px 4px rgba(0,0,0,0.05);
61
- }
62
-
63
- .tab {
64
- flex: 1;
65
- padding: 14px 0;
66
- text-align: center;
67
- cursor: pointer;
68
- background: #d9dbe3;
69
- font-size: 1em;
70
- font-weight: 500;
71
- color: #2f2f2f;
72
- border-right: 1px solid #c4c6ce;
73
- transition: all 0.25s ease-in-out;
74
- }
75
-
76
- .tab:last-child { border-right: none; }
77
- .tab:hover { background: #cfd1da; }
78
- .tab.active {
79
- background: linear-gradient(135deg, #667eea 0%, #5a67d8 100%);
80
- color: white;
81
- font-weight: 600;
82
- box-shadow: inset 0 -2px 0 rgba(0,0,0,0.1);
83
- z-index: 1;
84
- }
85
-
86
- .tab-content {
87
- display: none;
88
- padding: 30px;
89
- }
90
- .tab-content.active { display: block; }
91
-
92
- .form-group { margin-bottom: 20px; }
93
- label {
94
- display: block;
95
- margin-bottom: 8px;
96
- font-weight: 600;
97
- color: #2c3e50;
98
- }
99
- input, select {
100
- width: 100%;
101
- padding: 12px;
102
- border: 2px solid #e0e0e0;
103
- border-radius: 8px;
104
- font-size: 1em;
105
- transition: border 0.3s;
106
- }
107
- input:focus, select:focus {
108
- outline: none;
109
- border-color: #667eea;
110
- }
111
- .helper-text {
112
- font-size: 0.85em;
113
- color: #666;
114
- margin-top: 5px;
115
- }
116
-
117
- .height-group {
118
- display: flex;
119
- align-items: center;
120
- width: 100%;
121
- border: 2px solid #d0d3da;
122
- border-radius: 8px;
123
- overflow: hidden;
124
- background: #ffffff;
125
- }
126
-
127
- .height-group:focus-within {
128
- border-color: #667eea;
129
- }
130
-
131
- .height-group input {
132
- flex: 1;
133
- padding: 14px 18px;
134
- font-size: 1rem;
135
- border: none !important;
136
- outline: none !important;
137
- border-radius: 0 !important;
138
- background: transparent;
139
- color: #333;
140
- }
141
-
142
- .height-group input::-webkit-inner-spin-button,
143
- .height-group input::-webkit-outer-spin-button {
144
- margin: 0;
145
- }
146
-
147
- .height-group button {
148
- width: auto !important;
149
- padding: 14px 24px;
150
- font-size: 0.95rem;
151
- font-weight: 600;
152
- border: none;
153
- border-left: 2px solid #d0d3da;
154
- cursor: pointer;
155
- color: white;
156
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
157
- border-radius: 0 !important;
158
- height: 100%;
159
- white-space: nowrap;
160
- transition: opacity 0.2s ease;
161
- }
162
-
163
- .height-group button:hover {
164
- opacity: 0.9;
165
- transform: none !important;
166
- }
167
-
168
- .height-group button:active {
169
- transform: none !important;
170
- }
171
-
172
- button {
173
- width: 100%;
174
- padding: 15px;
175
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
176
- color: white;
177
- border: none;
178
- border-radius: 8px;
179
- font-size: 1.1em;
180
- font-weight: 600;
181
- cursor: pointer;
182
- transition: transform 0.2s;
183
- }
184
- button:hover { transform: translateY(-2px); }
185
- button:active { transform: translateY(0); }
186
- button:disabled {
187
- background: #ccc;
188
- cursor: not-allowed;
189
- transform: none;
190
- }
191
-
192
- .loading {
193
- display: none;
194
- text-align: center;
195
- padding: 20px;
196
- color: #667eea;
197
- }
198
-
199
- .results {
200
- margin-top: 30px;
201
- padding: 25px;
202
- background: #f8f9fa;
203
- border-radius: 10px;
204
- display: none;
205
- }
206
-
207
- .risk-badge {
208
- display: inline-block;
209
- padding: 8px 16px;
210
- border-radius: 20px;
211
- font-weight: 600;
212
- margin: 10px 0;
213
- }
214
- .risk-very-high { background: #dc3545; color: white; }
215
- .risk-high { background: #fd7e14; color: white; }
216
- .risk-moderate { background: #ffc107; color: #000; }
217
- .risk-low { background: #28a745; color: white; }
218
- .risk-very-low { background: #17a2b8; color: white; }
219
-
220
- .metric {
221
- display: flex;
222
- justify-content: space-between;
223
- padding: 12px 0;
224
- border-bottom: 1px solid #dee2e6;
225
- }
226
- .metric:last-child { border-bottom: none; }
227
- .metric-label { font-weight: 600; color: #495057; }
228
- .metric-value { color: #212529; }
229
-
230
- .confidence-badge {
231
- display: inline-block;
232
- padding: 6px 14px;
233
- border-radius: 15px;
234
- font-size: 0.85em;
235
- font-weight: 600;
236
- margin-left: 10px;
237
- vertical-align: middle;
238
- }
239
- .confidence-good { background: #10b981; color: white; }
240
- .confidence-moderate { background: #f59e0b; color: white; }
241
- .confidence-low { background: #ef4444; color: white; }
242
-
243
- .quality-flags {
244
- background: #fff3cd;
245
- border-left: 4px solid #ffc107;
246
- padding: 15px;
247
- margin: 20px 0;
248
- border-radius: 5px;
249
- }
250
- .quality-flags h4 {
251
- margin-bottom: 10px;
252
- color: #856404;
253
- }
254
- .quality-flags ul {
255
- list-style: none;
256
- padding: 0;
257
- }
258
- .quality-flags li {
259
- padding: 5px 0;
260
- color: #856404;
261
- }
262
-
263
- .explanation-card {
264
- background: white;
265
- padding: 15px;
266
- margin: 10px 0;
267
- border-radius: 8px;
268
- border-left: 4px solid #667eea;
269
- }
270
- .factor-contribution {
271
- display: flex;
272
- align-items: center;
273
- margin: 8px 0;
274
- }
275
- .contribution-bar {
276
- flex: 1;
277
- height: 20px;
278
- background: #e9ecef;
279
- border-radius: 4px;
280
- overflow: hidden;
281
- margin: 0 10px;
282
- }
283
- .contribution-fill {
284
- height: 100%;
285
- background: linear-gradient(90deg, #667eea, #764ba2);
286
- transition: width 0.5s;
287
- }
288
-
289
- .hazard-breakdown {
290
- display: grid;
291
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
292
- gap: 15px;
293
- margin: 20px 0;
294
- }
295
- .hazard-card {
296
- background: white;
297
- padding: 15px;
298
- border-radius: 8px;
299
- text-align: center;
300
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
301
- }
302
- .hazard-value {
303
- font-size: 2em;
304
- font-weight: 700;
305
- color: #667eea;
306
- margin: 10px 0;
307
- }
308
-
309
- .confidence-fill {
310
- height: 100%;
311
- border-radius: 5px;
312
- transition: width 0.5s;
313
- }
314
- .confidence-high { background: #28a745; }
315
- .confidence-moderate-fill { background: #ffc107; }
316
- .confidence-low-fill { background: #fd7e14; }
317
-
318
- .error {
319
- background: #f8d7da;
320
- color: #721c24;
321
- padding: 15px;
322
- border-radius: 8px;
323
- margin: 20px 0;
324
- display: none;
325
- }
326
- .checkbox-row {
327
- display: flex;
328
- align-items: center;
329
- gap: 10px;
330
- }
331
-
332
- .checkbox-row input[type="checkbox"] {
333
- width: auto;
334
- height: auto;
335
- margin: 0;
336
- }
337
-
338
- .checkbox-row label {
339
- margin: 0;
340
- font-weight: 600;
341
- color: #2c3e50;
342
- display: inline-block;
343
- }
344
-
345
- </style>
346
- </head>
347
- <body>
348
- <div class="container">
349
- <div class="header">
350
- <h1>🌊 Flood Vulnerability Assessment</h1>
351
- <p class="subtitle">Advanced multi-hazard flood risk analysis powered by GEE</p>
352
- </div>
353
-
354
- <div class="tabs">
355
- <button class="tab active" onclick="switchTab('basic')">Basic Assessment</button>
356
- <button class="tab" onclick="switchTab('explained')">With Explanation</button>
357
- <button class="tab" onclick="switchTab('multihazard')">Multi-Hazard</button>
358
- <button class="tab" onclick="switchTab('batch')">Batch Upload</button>
359
- </div>
360
-
361
- <!-- Basic Assessment Tab -->
362
- <div id="basic-tab" class="tab-content active">
363
- <form id="assessForm" onsubmit="assessLocation(event, '/assess', 'basic-results')">
364
- <div class="form-group">
365
- <label for="latitude">Latitude</label>
366
- <input type="text" id="latitude" name="latitude" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" required placeholder="e.g., 40.7128">
367
- <p class="helper-text">Range: -90 to 90</p>
368
- </div>
369
-
370
- <div class="form-group">
371
- <label for="longitude">Longitude</label>
372
- <input type="text" id="longitude" name="longitude" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="e.g., -74.0060">
373
- <p class="helper-text">Range: -180 to 180</p>
374
- </div>
375
-
376
- <div class="form-group">
377
- <label for="height">Building Height (meters above ground)</label>
378
- <div class="height-group">
379
- <input type="number" id="height" step="any" value="0" placeholder="e.g., 5.0">
380
- <button type="button" id="predict-height-btn">Predict</button>
381
- </div>
382
- <p class="helper-text">0 = ground level, 5 = typical 2-story building</p>
383
- </div>
384
-
385
- <div class="form-group">
386
- <label for="basement">Basement Depth (meters below ground)</label>
387
- <input type="text" id="basement" name="basement" value="0" max="0" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" placeholder="e.g., -2.0">
388
- <p class="helper-text">0 = no basement, -2 = 2 meters below ground (increases risk)</p>
389
- </div>
390
-
391
- <button type="submit">Assess Vulnerability</button>
392
- </form>
393
-
394
- <div class="loading" id="basic-loading">
395
- <p>⏳ Analyzing terrain and water proximity...</p>
396
- </div>
397
-
398
- <div class="error" id="basic-error"></div>
399
- <div class="results" id="basic-results"></div>
400
- </div>
401
-
402
- <!-- Explained Assessment Tab -->
403
- <div id="explained-tab" class="tab-content">
404
- <form onsubmit="assessLocation(event, '/explain', 'explained-results')">
405
- <div class="form-group">
406
- <label for="latitude2">Latitude</label>
407
- <input type="text" id="latitude2" name="latitude2" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="e.g., 40.7128">
408
- </div>
409
-
410
- <div class="form-group">
411
- <label for="longitude2">Longitude</label>
412
- <input type="text" id="longitude2" name="longitude2" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="e.g., -74.0060">
413
- </div>
414
-
415
- <div class="form-group">
416
- <label for="height2">Building Height (meters)</label>
417
- <input type="number" id="height2" step="0.1" value="0">
418
- </div>
419
-
420
- <div class="form-group">
421
- <label for="basement2">Basement Depth (meters, negative)</label>
422
- <input type="text" id="basement2" name="basement2" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" step="0.1" value="0" max="0">
423
- </div>
424
-
425
- <button type="submit">Assess with Explanation</button>
426
- </form>
427
-
428
- <div class="loading" id="explained-loading">
429
- <p>⏳ Analyzing and generating explanation...</p>
430
- </div>
431
-
432
- <div class="error" id="explained-error"></div>
433
- <div class="results" id="explained-results"></div>
434
- </div>
435
-
436
- <!-- Multi-Hazard Tab -->
437
- <div id="multihazard-tab" class="tab-content">
438
- <form onsubmit="assessLocation(event, '/assess_multihazard', 'multihazard-results')">
439
- <div class="form-group">
440
- <label for="latitude3">Latitude</label>
441
- <input type="text" id="latitude3" name="latitude3" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="e.g., 40.7128">
442
- </div>
443
-
444
- <div class="form-group">
445
- <label for="longitude3">Longitude</label>
446
- <input type="text" id="longitude3" name="longitude3" pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="e.g., -74.0060">
447
- </div>
448
-
449
- <div class="form-group">
450
- <label for="height3">Building Height (meters)</label>
451
- <input type="number" id="height3" step="0.1" value="0">
452
- </div>
453
-
454
- <div class="form-group">
455
- <label for="basement3">Basement Depth (meters, negative)</label>
456
- <input type="text" id="basement3" name="basement3" inputmode="text" pattern="-?[0-9]*[.,]?[0-9]*" step="0.1" value="0" max="0">
457
- </div>
458
-
459
- <button type="submit">Multi-Hazard Assessment</button>
460
- </form>
461
-
462
- <div class="loading" id="multihazard-loading">
463
- <p>⏳ Analyzing multiple flood hazards...</p>
464
- </div>
465
-
466
- <div class="error" id="multihazard-error"></div>
467
- <div class="results" id="multihazard-results"></div>
468
- </div>
469
-
470
- <div id="batch-tab" class="tab-content">
471
-
472
- <div class="form-group">
473
- <label for="batchMode">Batch Mode</label>
474
- <select id="batchMode">
475
- <option value="standard">Basic Assessment Model</option>
476
- <option value="multihazard">Multi-Hazard (Fluvial / Coastal / Pluvial)</option>
477
- </select>
478
- </div>
479
-
480
- <div class="form-group">
481
- <label for="csvFile">Upload CSV File</label>
482
- <input type="file" id="csvFile" accept=".csv">
483
- <p class="helper-text"> CSV must contain: latitude, longitude in decimal degrees (WGS84), e.g. 29.1703, -95.3128.<br>
484
- Optional columns: height, basement.</p>
485
-
486
- </div>
487
-
488
- <div class="form-group">
489
- <div class="checkbox-row">
490
- <input type="checkbox" id="usePredictedHeight">
491
- <label for="usePredictedHeight">Use satellite-predicted building height</label>
492
- </div>
493
- <p class="helper-text">
494
- For each row, estimate height from coordinates and use it in the vulnerability assessment.
495
- </p>
496
- </div>
497
-
498
- <button onclick="uploadBatch()">Process Batch</button>
499
-
500
- <div class="loading" id="batch-loading">
501
- <p>⏳ Processing batch assessments...</p>
502
- </div>
503
-
504
- <div class="error" id="batch-error"></div>
505
- <div class="results" id="batch-results"></div>
506
-
507
- </div>
508
-
509
-
510
- </div>
511
-
512
- <script>
513
-
514
- document.addEventListener('DOMContentLoaded', () => {
515
- const predictBtn = document.getElementById('predict-height-btn');
516
- if (!predictBtn) {
517
- return;
518
- }
519
- predictBtn.addEventListener('click', async () => {
520
- const latInput = document.getElementById('latitude');
521
- const lonInput = document.getElementById('longitude');
522
- const heightInput = document.getElementById('height');
523
- const basicError = document.getElementById('basic-error');
524
-
525
- basicError.style.display = 'none';
526
- basicError.textContent = '';
527
-
528
- const latitude = parseFloat(latInput.value);
529
- const longitude = parseFloat(lonInput.value);
530
-
531
- if (isNaN(latitude) || isNaN(longitude)) {
532
- basicError.textContent = 'Please enter latitude and longitude first.';
533
- basicError.style.display = 'block';
534
- return;
535
- }
536
-
537
- const originalText = predictBtn.textContent;
538
- predictBtn.disabled = true;
539
- predictBtn.textContent = 'Predicting...';
540
-
541
- try {
542
- const response = await fetch('/predict_height', {
543
- method: 'POST',
544
- headers: {'Content-Type': 'application/json'},
545
- body: JSON.stringify({
546
- latitude,
547
- longitude,
548
- height: 0,
549
- basement: 0
550
- })
551
- });
552
-
553
- const data = await response.json();
554
- if (!response.ok || data.status !== 'success' || data.predicted_height == null) {
555
- const message = data.detail || data.error || 'Height prediction failed.';
556
- throw new Error(message);
557
- }
558
- const h = Number(data.predicted_height);
559
- heightInput.value = h.toFixed(2);
560
- heightInput.classList.add('height-pulse');
561
- setTimeout(() => {
562
- heightInput.classList.remove('height-pulse');
563
- }, 800);
564
- } catch (err) {
565
- basicError.textContent = err.message || 'Height prediction failed.';
566
- basicError.style.display = 'block';
567
- } finally {
568
- predictBtn.disabled = false;
569
- predictBtn.textContent = originalText;
570
- }
571
- });
572
- });
573
-
574
- function switchTab(tabName) {
575
- document.querySelectorAll('.tab-content').forEach(tab => {
576
- tab.classList.remove('active');
577
- });
578
- document.querySelectorAll('.tab').forEach(tab => {
579
- tab.classList.remove('active');
580
- });
581
-
582
- document.getElementById(tabName + '-tab').classList.add('active');
583
- event.target.classList.add('active');
584
- }
585
-
586
- async function assessLocation(event, endpoint, resultsId) {
587
- event.preventDefault();
588
-
589
- const tabName = resultsId.split('-')[0];
590
- const suffix = endpoint === '/assess' ? '' : (endpoint === '/explain' ? '2' : '3');
591
- const latitude = parseFloat(document.getElementById('latitude' + suffix).value);
592
- const longitude = parseFloat(document.getElementById('longitude' + suffix).value);
593
- const height = parseFloat(document.getElementById('height' + suffix).value) || 0;
594
- const basement = parseFloat(document.getElementById('basement' + suffix).value) || 0;
595
-
596
- document.getElementById(tabName + '-loading').style.display = 'block';
597
- document.getElementById(resultsId).style.display = 'none';
598
- document.getElementById(tabName + '-error').style.display = 'none';
599
-
600
- try {
601
- const response = await fetch(endpoint, {
602
- method: 'POST',
603
- headers: { 'Content-Type': 'application/json' },
604
- body: JSON.stringify({ latitude, longitude, height, basement })
605
- });
606
-
607
- const data = await response.json();
608
-
609
- if (data.status === 'success') {
610
- displayResults(data, resultsId, endpoint);
611
- } else {
612
- throw new Error(data.detail || 'Assessment failed');
613
- }
614
- } catch (error) {
615
- document.getElementById(tabName + '-error').textContent = error.message;
616
- document.getElementById(tabName + '-error').style.display = 'block';
617
- } finally {
618
- document.getElementById(tabName + '-loading').style.display = 'none';
619
- }
620
- }
621
-
622
- function formatFlag(flag) {
623
- const flagMessages = {
624
- 'missing_elevation': 'Elevation data unavailable',
625
- 'missing_tpi': 'Topographic position data incomplete',
626
- 'missing_slope': 'Slope data incomplete',
627
- 'water_distance_unknown': 'Water proximity uncertain',
628
- 'far_from_water_search_limited': 'Far from major water bodies (search radius limited)',
629
- 'steep_terrain_dem_error_high': ' Steep terrain increases measurement uncertainty',
630
- 'coastal_surge_risk_not_modeled': ' Coastal surge dynamics not fully captured'
631
- };
632
- return flagMessages[flag] || flag.replace(/_/g, ' ');
633
- }
634
- function displayResults(data, resultsId, endpoint) {
635
- const resultsDiv = document.getElementById(resultsId);
636
- const assessment = data.assessment;
637
-
638
- let html = '<h2>Assessment Results</h2>';
639
-
640
- // Risk badge
641
- const riskClass = 'risk-' + assessment.risk_level.replace(/_/g, '-');
642
- html += `<div style="margin: 15px 0;">`;
643
- html += `<div class="risk-badge ${riskClass}">${assessment.risk_level.toUpperCase().replace(/_/g, ' ')}</div>`;
644
- html += `</div>`;
645
-
646
-
647
-
648
- // Vulnerability score
649
- if (assessment.confidence_interval) {
650
- const ci = assessment.confidence_interval;
651
- html += `
652
- <div class="metric">
653
- <span class="metric-label">Vulnerability Index</span>
654
- <span class="metric-value">
655
- ${ci.point_estimate} (95% CI: ${ci.lower_bound_95}–${ci.upper_bound_95})
656
- </span>
657
- </div>
658
- `;
659
- } else {
660
- html += `
661
- <div class="metric">
662
- <span class="metric-label">Vulnerability Index</span>
663
- <span class="metric-value">${assessment.vulnerability_index}</span>
664
- </div>
665
- `;
666
- }
667
-
668
- // Confidence visualization
669
- if (assessment.uncertainty_analysis) {
670
- const ua = assessment.uncertainty_analysis;
671
- const confidenceValue = parseFloat(ua.confidence) || 0;
672
- const barWidth = Math.round(confidenceValue * 100);
673
-
674
- let confidenceClass = 'confidence-low-fill';
675
- if (confidenceValue >= 0.75) confidenceClass = 'confidence-high';
676
- else if (confidenceValue >= 0.55) confidenceClass = 'confidence-moderate-fill';
677
-
678
- html += `
679
- <div style="margin: 25px 0; padding: 20px; background: white; border-radius: 8px;">
680
- <h3 style="margin-bottom: 10px; color: #333; font-size: 1.1em;">Assessment Confidence</h3>
681
- <div style="display: flex; align-items: center; font-size: 0.85em; color: #555;">
682
- <span style="min-width: 40px;">Low</span>
683
- <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);">
684
- <div class="confidence-fill ${confidenceClass}"
685
- style="width: ${barWidth}%; height: 100%; transition: width 0.4s ease;"></div>
686
- <span style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
687
- font-weight: 700; color: #000; font-size: 0.9em; text-shadow: 0 0 4px rgba(255,255,255,0.9);">
688
- ${barWidth}%
689
- </span>
690
- </div>
691
- <span style="min-width: 40px; text-align: right;">High</span>
692
- </div>
693
- <p style="margin: 12px 0 0; font-size: 0.9em; color: #555; font-style: italic;">
694
- ${ua.interpretation}
695
- </p>
696
- </div>
697
- `;
698
-
699
- // Quality flags
700
- if (ua.data_quality_flags && ua.data_quality_flags.length > 0) {
701
- const criticalFlags = ua.data_quality_flags.filter(flag =>
702
- flag === 'steep_terrain_dem_error_high' ||
703
- flag === 'coastal_surge_risk_not_modeled'
704
- );
705
-
706
- if (criticalFlags.length > 0) {
707
- html += `
708
- <div class="quality-flags">
709
- <h4>Data Quality Notes</h4>
710
- <ul>
711
- `;
712
- criticalFlags.forEach(flag => {
713
- html += `<li>${formatFlag(flag)}</li>`;
714
- });
715
- html += `
716
- </ul>
717
- </div>
718
- `;
719
- }
720
- }
721
- }
722
-
723
- // Terrain metrics
724
- html += '<h3 style="margin-top: 25px;">Terrain Analysis</h3>';
725
- html += `
726
- <div class="metric">
727
- <span class="metric-label">Elevation</span>
728
- <span class="metric-value">${assessment.elevation_m} m</span>
729
- </div>
730
- <div class="metric">
731
- <span class="metric-label">Relative Elevation (TPI)</span>
732
- <span class="metric-value">${assessment.relative_elevation_m !== null ? assessment.relative_elevation_m + ' m' : 'N/A'}</span>
733
- </div>
734
- <div class="metric">
735
- <span class="metric-label">Slope</span>
736
- <span class="metric-value">${assessment.slope_degrees !== null ? assessment.slope_degrees + '°' : 'N/A'}</span>
737
- </div>
738
- <div class="metric">
739
- <span class="metric-label">Distance to Water</span>
740
- <span class="metric-value">
741
- ${assessment.distance_to_water_m !== null
742
- ? assessment.distance_to_water_m + ' m'
743
- : 'N/A'}
744
- </span>
745
- </div>
746
- `;
747
-
748
- // Multi-hazard breakdown
749
- if (assessment.hazard_breakdown) {
750
- const hb = assessment.hazard_breakdown;
751
- html += '<h3 style="margin-top: 25px;">Hazard Breakdown</h3>';
752
- html += '<div class="hazard-breakdown">';
753
- html += `
754
- <div class="hazard-card">
755
- <div>Fluvial/Riverine</div>
756
- <div class="hazard-value">${hb.fluvial_riverine}</div>
757
- </div>
758
- <div class="hazard-card">
759
- <div>Coastal Surge</div>
760
- <div class="hazard-value">${hb.coastal_surge}</div>
761
- </div>
762
- <div class="hazard-card">
763
- <div>Pluvial/Drainage</div>
764
- <div class="hazard-value">${hb.pluvial_drainage}</div>
765
- </div>
766
- `;
767
- html += '</div>';
768
- html += `<p><strong>Dominant Hazard:</strong> ${assessment.dominant_hazard.replace(/_/g, ' ').toUpperCase()}</p>`;
769
- }
770
-
771
- // SHAP explanation
772
- if (data.explanation) {
773
- const exp = data.explanation;
774
- html += '<h3 style="margin-top: 25px;">Risk Factor Explanation</h3>';
775
- html += `<p><strong>Top Risk Driver:</strong> ${exp.top_risk_driver}</p>`;
776
- html += '<div class="explanation-card">';
777
-
778
- exp.explanations.forEach(factor => {
779
- html += `
780
- <div class="factor-contribution">
781
- <span style="min-width: 150px;">${factor.factor}</span>
782
- <div class="contribution-bar">
783
- <div class="contribution-fill" style="width: ${factor.contribution_pct}%"></div>
784
- </div>
785
- <span style="min-width: 60px; text-align: right;">${factor.contribution_pct}%</span>
786
- </div>
787
- `;
788
- });
789
-
790
- html += '</div>';
791
- }
792
-
793
- resultsDiv.innerHTML = html;
794
- resultsDiv.style.display = 'block';
795
- }
796
-
797
- async function uploadBatch() {
798
- const fileInput = document.getElementById('csvFile');
799
- const file = fileInput.files[0];
800
-
801
- if (!file) {
802
- alert('Please select a CSV file');
803
- return;
804
- }
805
-
806
- document.getElementById('batch-loading').style.display = 'block';
807
- document.getElementById('batch-results').style.display = 'none';
808
- document.getElementById('batch-error').style.display = 'none';
809
-
810
- const formData = new FormData();
811
- formData.append('file', file);
812
-
813
- try {
814
- const mode = document.getElementById('batchMode').value;
815
- const usePredicted = document.getElementById('usePredictedHeight').checked;
816
- let endpoint = mode === 'multihazard' ? '/assess_batch_multihazard' : '/assess_batch';
817
-
818
- if (usePredicted) {
819
- const sep = endpoint.includes('?') ? '&' : '?';
820
- endpoint = endpoint + sep + 'use_predicted_height=true';
821
- }
822
-
823
- const response = await fetch(endpoint, {
824
- method: 'POST',
825
- body: formData
826
- });
827
-
828
- if (response.ok) {
829
- const blob = await response.blob();
830
- const url = window.URL.createObjectURL(blob);
831
- const a = document.createElement('a');
832
- a.href = url;
833
- const mode = document.getElementById('batchMode').value;
834
- const filename = mode === 'multihazard'
835
- ? 'multihazard_results.csv'
836
- : 'vulnerability_results.csv';
837
-
838
- a.download = filename;
839
- document.body.appendChild(a);
840
- a.click();
841
- window.URL.revokeObjectURL(url);
842
-
843
- document.getElementById('batch-results').innerHTML = '<p>✅ Batch processing complete! Results downloaded.</p>';
844
- document.getElementById('batch-results').style.display = 'block';
845
- } else {
846
- throw new Error('Batch processing failed');
847
- }
848
- } catch (error) {
849
- document.getElementById('batch-error').textContent = error.message;
850
- document.getElementById('batch-error').style.display = 'block';
851
- } finally {
852
- document.getElementById('batch-loading').style.display = 'none';
853
- }
854
- }
855
- </script>
856
-
857
- <footer style="text-align: center; padding: 15px; margin-top: 20px; background: #2c3e50; color: white; border-top: 2px solid #667eea; font-size: 0.9em;">
858
- © 2025 Flood Vulnerability Assessment | Made by ...
859
- </footer>
860
- </body>
861
- </html>
862
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>