Spaces:
Sleeping
Sleeping
Update templates/index.html
Browse files- templates/index.html +214 -74
templates/index.html
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 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">
|
|
@@ -11,7 +12,8 @@
|
|
| 11 |
box-sizing: border-box;
|
| 12 |
}
|
| 13 |
|
| 14 |
-
html,
|
|
|
|
| 15 |
height: 100%;
|
| 16 |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 17 |
}
|
|
@@ -41,15 +43,22 @@
|
|
| 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 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
.page-wrapper {
|
|
@@ -80,7 +89,9 @@
|
|
| 80 |
}
|
| 81 |
|
| 82 |
@keyframes shine {
|
| 83 |
-
to {
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
|
| 86 |
.hero-header .subtitle {
|
|
@@ -196,6 +207,7 @@
|
|
| 196 |
opacity: 0;
|
| 197 |
transform: translateY(20px);
|
| 198 |
}
|
|
|
|
| 199 |
to {
|
| 200 |
opacity: 1;
|
| 201 |
transform: translateY(0);
|
|
@@ -342,6 +354,36 @@
|
|
| 342 |
cursor: not-allowed;
|
| 343 |
}
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
/* Checkbox styling */
|
| 346 |
.checkbox-row {
|
| 347 |
display: flex;
|
|
@@ -408,7 +450,7 @@
|
|
| 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 |
|
|
@@ -449,7 +491,9 @@
|
|
| 449 |
}
|
| 450 |
|
| 451 |
@keyframes spin {
|
| 452 |
-
to {
|
|
|
|
|
|
|
| 453 |
}
|
| 454 |
|
| 455 |
.loading-state p {
|
|
@@ -489,23 +533,27 @@
|
|
| 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 |
-
|
|
|
|
| 497 |
background: linear-gradient(135deg, #ea580c, #c2410c);
|
| 498 |
box-shadow: 0 4px 15px rgba(234, 88, 12, 0.4);
|
| 499 |
}
|
| 500 |
-
|
|
|
|
| 501 |
background: linear-gradient(135deg, #ca8a04, #a16207);
|
| 502 |
box-shadow: 0 4px 15px rgba(202, 138, 4, 0.4);
|
| 503 |
}
|
| 504 |
-
|
|
|
|
| 505 |
background: linear-gradient(135deg, #16a34a, #15803d);
|
| 506 |
box-shadow: 0 4px 15px rgba(22, 163, 74, 0.4);
|
| 507 |
}
|
| 508 |
-
|
|
|
|
| 509 |
background: linear-gradient(135deg, #0891b2, #0e7490);
|
| 510 |
box-shadow: 0 4px 15px rgba(8, 145, 178, 0.4);
|
| 511 |
}
|
|
@@ -627,15 +675,17 @@
|
|
| 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 |
-
|
|
|
|
| 635 |
background: linear-gradient(90deg, #ca8a04, #fbbf24);
|
| 636 |
box-shadow: 0 0 20px rgba(251, 191, 36, 0.4);
|
| 637 |
}
|
| 638 |
-
|
|
|
|
| 639 |
background: linear-gradient(90deg, #ea580c, #f97316);
|
| 640 |
box-shadow: 0 0 20px rgba(249, 115, 22, 0.4);
|
| 641 |
}
|
|
@@ -823,8 +873,15 @@
|
|
| 823 |
|
| 824 |
/* Pulse animation for predicted height */
|
| 825 |
@keyframes height-pulse {
|
| 826 |
-
|
| 827 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
}
|
| 829 |
|
| 830 |
.height-pulse {
|
|
@@ -883,17 +940,22 @@
|
|
| 883 |
}
|
| 884 |
</style>
|
| 885 |
</head>
|
|
|
|
| 886 |
<body>
|
| 887 |
<div class="hero-background"></div>
|
| 888 |
-
|
| 889 |
<div class="page-wrapper">
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
<div>
|
| 893 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
Flood Vulnerability Assessment
|
| 895 |
</h1>
|
| 896 |
-
<p class="subtitle">
|
| 897 |
Global building-level assessment
|
| 898 |
</p>
|
| 899 |
</div>
|
|
@@ -925,13 +987,15 @@
|
|
| 925 |
<div class="form-grid">
|
| 926 |
<div class="input-card">
|
| 927 |
<label for="latitude">Latitude</label>
|
| 928 |
-
<input type="text" id="latitude" name="latitude" inputmode="text"
|
|
|
|
| 929 |
<p class="helper-text">Range: -90 to 90</p>
|
| 930 |
</div>
|
| 931 |
|
| 932 |
<div class="input-card">
|
| 933 |
<label for="longitude">Longitude</label>
|
| 934 |
-
<input type="text" id="longitude" name="longitude" inputmode="text"
|
|
|
|
| 935 |
<p class="helper-text">Range: -180 to 180</p>
|
| 936 |
</div>
|
| 937 |
|
|
@@ -939,14 +1003,23 @@
|
|
| 939 |
<label for="height">Building Height</label>
|
| 940 |
<div class="height-group">
|
| 941 |
<input type="number" id="height" step="any" value="0" placeholder="5.0">
|
| 942 |
-
<button type="button" class="predict-height-btn" data-lat-id="latitude"
|
|
|
|
|
|
|
| 943 |
</div>
|
| 944 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 945 |
</div>
|
| 946 |
|
| 947 |
<div class="input-card">
|
| 948 |
<label for="basement">Basement Depth</label>
|
| 949 |
-
<input type="text" id="basement" name="basement" value="0" max="0" inputmode="text"
|
|
|
|
| 950 |
<p class="helper-text">Meters below ground (negative values)</p>
|
| 951 |
</div>
|
| 952 |
</div>
|
|
@@ -976,26 +1049,31 @@
|
|
| 976 |
<div class="form-grid">
|
| 977 |
<div class="input-card">
|
| 978 |
<label for="latitude2">Latitude</label>
|
| 979 |
-
<input type="text" id="latitude2" name="latitude2" pattern="-?[0-9]*[.,]?[0-9]*"
|
|
|
|
| 980 |
</div>
|
| 981 |
|
| 982 |
<div class="input-card">
|
| 983 |
<label for="longitude2">Longitude</label>
|
| 984 |
-
<input type="text" id="longitude2" name="longitude2" pattern="-?[0-9]*[.,]?[0-9]*"
|
|
|
|
| 985 |
</div>
|
| 986 |
|
| 987 |
<div class="input-card">
|
| 988 |
<label for="height2">Building Height</label>
|
| 989 |
<div class="height-group">
|
| 990 |
<input type="number" id="height2" step="any" value="0">
|
| 991 |
-
<button type="button" class="predict-height-btn" data-lat-id="latitude2"
|
|
|
|
|
|
|
| 992 |
</div>
|
| 993 |
<p class="helper-text">Meters above ground</p>
|
| 994 |
</div>
|
| 995 |
|
| 996 |
<div class="input-card">
|
| 997 |
<label for="basement2">Basement Depth</label>
|
| 998 |
-
<input type="text" id="basement2" name="basement2" inputmode="text"
|
|
|
|
| 999 |
<p class="helper-text">Meters below ground (negative)</p>
|
| 1000 |
</div>
|
| 1001 |
</div>
|
|
@@ -1025,26 +1103,31 @@
|
|
| 1025 |
<div class="form-grid">
|
| 1026 |
<div class="input-card">
|
| 1027 |
<label for="latitude3">Latitude</label>
|
| 1028 |
-
<input type="text" id="latitude3" name="latitude3" pattern="-?[0-9]*[.,]?[0-9]*"
|
|
|
|
| 1029 |
</div>
|
| 1030 |
|
| 1031 |
<div class="input-card">
|
| 1032 |
<label for="longitude3">Longitude</label>
|
| 1033 |
-
<input type="text" id="longitude3" name="longitude3" pattern="-?[0-9]*[.,]?[0-9]*"
|
|
|
|
| 1034 |
</div>
|
| 1035 |
|
| 1036 |
<div class="input-card">
|
| 1037 |
<label for="height3">Building Height</label>
|
| 1038 |
<div class="height-group">
|
| 1039 |
<input type="number" id="height3" step="any" value="0">
|
| 1040 |
-
<button type="button" class="predict-height-btn" data-lat-id="latitude3"
|
|
|
|
|
|
|
| 1041 |
</div>
|
| 1042 |
<p class="helper-text">Meters above ground</p>
|
| 1043 |
</div>
|
| 1044 |
|
| 1045 |
<div class="input-card">
|
| 1046 |
<label for="basement3">Basement Depth</label>
|
| 1047 |
-
<input type="text" id="basement3" name="basement3" inputmode="text"
|
|
|
|
| 1048 |
<p class="helper-text">Meters below ground (negative)</p>
|
| 1049 |
</div>
|
| 1050 |
</div>
|
|
@@ -1083,7 +1166,8 @@
|
|
| 1083 |
<div class="input-card">
|
| 1084 |
<label for="csvFile">CSV File</label>
|
| 1085 |
<input type="file" id="csvFile" accept=".csv">
|
| 1086 |
-
<p class="helper-text">Required columns: latitude, longitude in decimal degrees (e.g. 29.17,
|
|
|
|
| 1087 |
</div>
|
| 1088 |
</div>
|
| 1089 |
|
|
@@ -1202,34 +1286,34 @@
|
|
| 1202 |
document.querySelectorAll('.nav-link').forEach(link => {
|
| 1203 |
link.classList.remove('active');
|
| 1204 |
});
|
| 1205 |
-
|
| 1206 |
document.getElementById(tabName + '-card').classList.add('active');
|
| 1207 |
event.target.classList.add('active');
|
| 1208 |
}
|
| 1209 |
-
|
| 1210 |
async function assessLocation(event, endpoint, resultsId) {
|
| 1211 |
event.preventDefault();
|
| 1212 |
-
|
| 1213 |
const tabName = resultsId.split('-')[0];
|
| 1214 |
const suffix = endpoint === '/assess' ? '' : (endpoint === '/explain' ? '2' : '3');
|
| 1215 |
const latitude = parseFloat(document.getElementById('latitude' + suffix).value);
|
| 1216 |
const longitude = parseFloat(document.getElementById('longitude' + suffix).value);
|
| 1217 |
const height = parseFloat(document.getElementById('height' + suffix).value) || 0;
|
| 1218 |
const basement = parseFloat(document.getElementById('basement' + suffix).value) || 0;
|
| 1219 |
-
|
| 1220 |
document.getElementById(tabName + '-loading').style.display = 'block';
|
| 1221 |
document.getElementById(resultsId).style.display = 'none';
|
| 1222 |
document.getElementById(tabName + '-error').style.display = 'none';
|
| 1223 |
-
|
| 1224 |
try {
|
| 1225 |
const response = await fetch(endpoint, {
|
| 1226 |
method: 'POST',
|
| 1227 |
headers: { 'Content-Type': 'application/json' },
|
| 1228 |
body: JSON.stringify({ latitude, longitude, height, basement })
|
| 1229 |
});
|
| 1230 |
-
|
| 1231 |
const data = await response.json();
|
| 1232 |
-
|
| 1233 |
if (data.status === 'success') {
|
| 1234 |
displayResults(data, resultsId, endpoint);
|
| 1235 |
} else {
|
|
@@ -1242,7 +1326,7 @@
|
|
| 1242 |
document.getElementById(tabName + '-loading').style.display = 'none';
|
| 1243 |
}
|
| 1244 |
}
|
| 1245 |
-
|
| 1246 |
function formatFlag(flag) {
|
| 1247 |
const flagMessages = {
|
| 1248 |
'missing_elevation': 'Elevation data unavailable',
|
|
@@ -1255,20 +1339,20 @@
|
|
| 1255 |
};
|
| 1256 |
return flagMessages[flag] || flag.replace(/_/g, ' ');
|
| 1257 |
}
|
| 1258 |
-
|
| 1259 |
function displayResults(data, resultsId, endpoint) {
|
| 1260 |
const resultsDiv = document.getElementById(resultsId);
|
| 1261 |
const assessment = data.assessment;
|
| 1262 |
-
|
| 1263 |
let html = '<div class="results-header">';
|
| 1264 |
html += '<h2>Assessment Complete</h2>';
|
| 1265 |
-
|
| 1266 |
const riskClass = 'risk-' + assessment.risk_level.replace(/_/g, '-');
|
| 1267 |
html += `<div class="risk-badge ${riskClass}">${assessment.risk_level.replace(/_/g, ' ')}</div>`;
|
| 1268 |
html += '</div>';
|
| 1269 |
-
|
| 1270 |
html += '<div class="stats-grid">';
|
| 1271 |
-
|
| 1272 |
if (assessment.confidence_interval) {
|
| 1273 |
const ci = assessment.confidence_interval;
|
| 1274 |
html += `
|
|
@@ -1288,14 +1372,14 @@
|
|
| 1288 |
</div>
|
| 1289 |
`;
|
| 1290 |
}
|
| 1291 |
-
|
| 1292 |
html += `
|
| 1293 |
<div class="stat-card">
|
| 1294 |
<div class="stat-label">Elevation</div>
|
| 1295 |
<div class="stat-value">${assessment.elevation_m}m</div>
|
| 1296 |
</div>
|
| 1297 |
`;
|
| 1298 |
-
|
| 1299 |
if (assessment.distance_to_water_m !== null) {
|
| 1300 |
html += `
|
| 1301 |
<div class="stat-card">
|
|
@@ -1304,9 +1388,9 @@
|
|
| 1304 |
</div>
|
| 1305 |
`;
|
| 1306 |
}
|
| 1307 |
-
|
| 1308 |
html += '</div>';
|
| 1309 |
-
|
| 1310 |
if (assessment.uncertainty_analysis) {
|
| 1311 |
const ua = assessment.uncertainty_analysis;
|
| 1312 |
const confidenceValue = parseFloat(ua.confidence) || 0;
|
|
@@ -1332,13 +1416,13 @@
|
|
| 1332 |
</p>
|
| 1333 |
</div>
|
| 1334 |
`;
|
| 1335 |
-
|
| 1336 |
if (ua.data_quality_flags && ua.data_quality_flags.length > 0) {
|
| 1337 |
-
const criticalFlags = ua.data_quality_flags.filter(flag =>
|
| 1338 |
-
flag === 'steep_terrain_dem_error_high' ||
|
| 1339 |
flag === 'coastal_surge_risk_not_modeled'
|
| 1340 |
);
|
| 1341 |
-
|
| 1342 |
if (criticalFlags.length > 0) {
|
| 1343 |
html += '<div class="quality-warning"><h4>⚠ Data Quality Notes</h4><ul>';
|
| 1344 |
criticalFlags.forEach(flag => {
|
|
@@ -1348,7 +1432,7 @@
|
|
| 1348 |
}
|
| 1349 |
}
|
| 1350 |
}
|
| 1351 |
-
|
| 1352 |
html += '<div class="detail-section"><h3>Terrain Analysis</h3>';
|
| 1353 |
html += `
|
| 1354 |
<div class="metric-row">
|
|
@@ -1369,7 +1453,7 @@
|
|
| 1369 |
</div>
|
| 1370 |
`;
|
| 1371 |
html += '</div>';
|
| 1372 |
-
|
| 1373 |
if (assessment.hazard_breakdown) {
|
| 1374 |
const hb = assessment.hazard_breakdown;
|
| 1375 |
html += '<div class="detail-section"><h3>Hazard Breakdown</h3>';
|
|
@@ -1392,13 +1476,13 @@
|
|
| 1392 |
html += `<p style="margin-top: 1.5rem;"><strong>Dominant Hazard:</strong> ${assessment.dominant_hazard.replace(/_/g, ' ').toUpperCase()}</p>`;
|
| 1393 |
html += '</div>';
|
| 1394 |
}
|
| 1395 |
-
|
| 1396 |
if (data.explanation) {
|
| 1397 |
const exp = data.explanation;
|
| 1398 |
html += '<div class="explanation-section">';
|
| 1399 |
html += '<h3>Risk Factor Analysis</h3>';
|
| 1400 |
html += `<p style="margin-bottom: 1.5rem; color: #cbd5e1;"><strong>Top Risk Driver:</strong> ${exp.top_risk_driver}</p>`;
|
| 1401 |
-
|
| 1402 |
exp.explanations.forEach(factor => {
|
| 1403 |
html += `
|
| 1404 |
<div class="factor-item">
|
|
@@ -1410,30 +1494,85 @@
|
|
| 1410 |
</div>
|
| 1411 |
`;
|
| 1412 |
});
|
| 1413 |
-
|
| 1414 |
html += '</div>';
|
| 1415 |
}
|
| 1416 |
-
|
| 1417 |
resultsDiv.innerHTML = html;
|
| 1418 |
resultsDiv.style.display = 'block';
|
| 1419 |
}
|
| 1420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1421 |
async function uploadBatch() {
|
| 1422 |
const fileInput = document.getElementById('csvFile');
|
| 1423 |
const file = fileInput.files[0];
|
| 1424 |
-
|
| 1425 |
if (!file) {
|
| 1426 |
alert('Please select a CSV file');
|
| 1427 |
return;
|
| 1428 |
}
|
| 1429 |
-
|
| 1430 |
document.getElementById('batch-loading').style.display = 'block';
|
| 1431 |
document.getElementById('batch-results').style.display = 'none';
|
| 1432 |
document.getElementById('batch-error').style.display = 'none';
|
| 1433 |
-
|
| 1434 |
const formData = new FormData();
|
| 1435 |
formData.append('file', file);
|
| 1436 |
-
|
| 1437 |
try {
|
| 1438 |
const mode = document.getElementById('batchMode').value;
|
| 1439 |
const usePredicted = document.getElementById('usePredictedHeight').checked;
|
|
@@ -1448,20 +1587,20 @@
|
|
| 1448 |
method: 'POST',
|
| 1449 |
body: formData
|
| 1450 |
});
|
| 1451 |
-
|
| 1452 |
if (response.ok) {
|
| 1453 |
const blob = await response.blob();
|
| 1454 |
const url = window.URL.createObjectURL(blob);
|
| 1455 |
const a = document.createElement('a');
|
| 1456 |
a.href = url;
|
| 1457 |
-
const filename = mode === 'multihazard'
|
| 1458 |
-
? 'multihazard_results.csv'
|
| 1459 |
: 'vulnerability_results.csv';
|
| 1460 |
a.download = filename;
|
| 1461 |
document.body.appendChild(a);
|
| 1462 |
a.click();
|
| 1463 |
window.URL.revokeObjectURL(url);
|
| 1464 |
-
|
| 1465 |
document.getElementById('batch-results').innerHTML = `
|
| 1466 |
<div class="results-header">
|
| 1467 |
<h2>✓ Processing Complete</h2>
|
|
@@ -1481,4 +1620,5 @@
|
|
| 1481 |
}
|
| 1482 |
</script>
|
| 1483 |
</body>
|
| 1484 |
-
|
|
|
|
|
|
| 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">
|
|
|
|
| 12 |
box-sizing: border-box;
|
| 13 |
}
|
| 14 |
|
| 15 |
+
html,
|
| 16 |
+
body {
|
| 17 |
height: 100%;
|
| 18 |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 19 |
}
|
|
|
|
| 43 |
left: 0;
|
| 44 |
width: 100%;
|
| 45 |
height: 100%;
|
| 46 |
+
background:
|
| 47 |
radial-gradient(circle at 20% 50%, rgba(59, 130, 246, 0.15) 0%, transparent 50%),
|
| 48 |
radial-gradient(circle at 80% 50%, rgba(139, 92, 246, 0.15) 0%, transparent 50%);
|
| 49 |
animation: pulse 8s ease-in-out infinite;
|
| 50 |
}
|
| 51 |
|
| 52 |
@keyframes pulse {
|
| 53 |
+
|
| 54 |
+
0%,
|
| 55 |
+
100% {
|
| 56 |
+
opacity: 1;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
50% {
|
| 60 |
+
opacity: 0.5;
|
| 61 |
+
}
|
| 62 |
}
|
| 63 |
|
| 64 |
.page-wrapper {
|
|
|
|
| 89 |
}
|
| 90 |
|
| 91 |
@keyframes shine {
|
| 92 |
+
to {
|
| 93 |
+
background-position: 200% center;
|
| 94 |
+
}
|
| 95 |
}
|
| 96 |
|
| 97 |
.hero-header .subtitle {
|
|
|
|
| 207 |
opacity: 0;
|
| 208 |
transform: translateY(20px);
|
| 209 |
}
|
| 210 |
+
|
| 211 |
to {
|
| 212 |
opacity: 1;
|
| 213 |
transform: translateY(0);
|
|
|
|
| 354 |
cursor: not-allowed;
|
| 355 |
}
|
| 356 |
|
| 357 |
+
.gba-height-btn {
|
| 358 |
+
margin-top: 0.4rem;
|
| 359 |
+
align-self: flex-start;
|
| 360 |
+
|
| 361 |
+
background: rgba(96, 165, 250, 0.08);
|
| 362 |
+
border: 1px solid rgba(96, 165, 250, 0.25);
|
| 363 |
+
border-radius: 8px;
|
| 364 |
+
|
| 365 |
+
padding: 0.25rem 0.6rem;
|
| 366 |
+
font-size: 0.7em;
|
| 367 |
+
font-weight: 500;
|
| 368 |
+
|
| 369 |
+
color: #93c5fd;
|
| 370 |
+
cursor: pointer;
|
| 371 |
+
|
| 372 |
+
transition: background 0.2s ease, border 0.2s ease, color 0.2s ease;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.gba-height-btn:hover {
|
| 376 |
+
background: rgba(96, 165, 250, 0.18);
|
| 377 |
+
border-color: rgba(96, 165, 250, 0.5);
|
| 378 |
+
color: #bfdbfe;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
.gba-height-btn:disabled {
|
| 383 |
+
opacity: 0.4;
|
| 384 |
+
cursor: not-allowed;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
/* Checkbox styling */
|
| 388 |
.checkbox-row {
|
| 389 |
display: flex;
|
|
|
|
| 450 |
left: -100%;
|
| 451 |
width: 100%;
|
| 452 |
height: 100%;
|
| 453 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
| 454 |
transition: left 0.5s;
|
| 455 |
}
|
| 456 |
|
|
|
|
| 491 |
}
|
| 492 |
|
| 493 |
@keyframes spin {
|
| 494 |
+
to {
|
| 495 |
+
transform: rotate(360deg);
|
| 496 |
+
}
|
| 497 |
}
|
| 498 |
|
| 499 |
.loading-state p {
|
|
|
|
| 533 |
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
| 534 |
}
|
| 535 |
|
| 536 |
+
.risk-very-high {
|
| 537 |
background: linear-gradient(135deg, #dc2626, #b91c1c);
|
| 538 |
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
|
| 539 |
}
|
| 540 |
+
|
| 541 |
+
.risk-high {
|
| 542 |
background: linear-gradient(135deg, #ea580c, #c2410c);
|
| 543 |
box-shadow: 0 4px 15px rgba(234, 88, 12, 0.4);
|
| 544 |
}
|
| 545 |
+
|
| 546 |
+
.risk-moderate {
|
| 547 |
background: linear-gradient(135deg, #ca8a04, #a16207);
|
| 548 |
box-shadow: 0 4px 15px rgba(202, 138, 4, 0.4);
|
| 549 |
}
|
| 550 |
+
|
| 551 |
+
.risk-low {
|
| 552 |
background: linear-gradient(135deg, #16a34a, #15803d);
|
| 553 |
box-shadow: 0 4px 15px rgba(22, 163, 74, 0.4);
|
| 554 |
}
|
| 555 |
+
|
| 556 |
+
.risk-very-low {
|
| 557 |
background: linear-gradient(135deg, #0891b2, #0e7490);
|
| 558 |
box-shadow: 0 4px 15px rgba(8, 145, 178, 0.4);
|
| 559 |
}
|
|
|
|
| 675 |
position: relative;
|
| 676 |
}
|
| 677 |
|
| 678 |
+
.confidence-high {
|
| 679 |
background: linear-gradient(90deg, #16a34a, #22c55e);
|
| 680 |
box-shadow: 0 0 20px rgba(34, 197, 94, 0.4);
|
| 681 |
}
|
| 682 |
+
|
| 683 |
+
.confidence-moderate-fill {
|
| 684 |
background: linear-gradient(90deg, #ca8a04, #fbbf24);
|
| 685 |
box-shadow: 0 0 20px rgba(251, 191, 36, 0.4);
|
| 686 |
}
|
| 687 |
+
|
| 688 |
+
.confidence-low-fill {
|
| 689 |
background: linear-gradient(90deg, #ea580c, #f97316);
|
| 690 |
box-shadow: 0 0 20px rgba(249, 115, 22, 0.4);
|
| 691 |
}
|
|
|
|
| 873 |
|
| 874 |
/* Pulse animation for predicted height */
|
| 875 |
@keyframes height-pulse {
|
| 876 |
+
|
| 877 |
+
0%,
|
| 878 |
+
100% {
|
| 879 |
+
background: rgba(0, 0, 0, 0.3);
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
50% {
|
| 883 |
+
background: rgba(59, 130, 246, 0.3);
|
| 884 |
+
}
|
| 885 |
}
|
| 886 |
|
| 887 |
.height-pulse {
|
|
|
|
| 940 |
}
|
| 941 |
</style>
|
| 942 |
</head>
|
| 943 |
+
|
| 944 |
<body>
|
| 945 |
<div class="hero-background"></div>
|
| 946 |
+
|
| 947 |
<div class="page-wrapper">
|
| 948 |
+
<header class="hero-header" style="position: relative; padding-top: 40px; text-align: center;">
|
| 949 |
+
|
| 950 |
+
<div style="position: absolute; top: 7%; left: 1%; font-size: 3.2vw; line-height: 0.8;">
|
| 951 |
+
💦💧🌊
|
| 952 |
+
</div>
|
| 953 |
+
|
| 954 |
+
<div style="display: inline-block; text-align: center; max-width: 90%; margin: 0 auto;">
|
| 955 |
+
<h1 style="margin: 0; font-size: 3rem; line-height: 1.2;">
|
| 956 |
Flood Vulnerability Assessment
|
| 957 |
</h1>
|
| 958 |
+
<p class="subtitle" style="margin: 0.2rem 0 0 0; font-size: 1rem; line-height: 1.2;">
|
| 959 |
Global building-level assessment
|
| 960 |
</p>
|
| 961 |
</div>
|
|
|
|
| 987 |
<div class="form-grid">
|
| 988 |
<div class="input-card">
|
| 989 |
<label for="latitude">Latitude</label>
|
| 990 |
+
<input type="text" id="latitude" name="latitude" inputmode="text"
|
| 991 |
+
pattern="-?[0-9]*[.,]?[0-9]*" required placeholder="40.7128">
|
| 992 |
<p class="helper-text">Range: -90 to 90</p>
|
| 993 |
</div>
|
| 994 |
|
| 995 |
<div class="input-card">
|
| 996 |
<label for="longitude">Longitude</label>
|
| 997 |
+
<input type="text" id="longitude" name="longitude" inputmode="text"
|
| 998 |
+
pattern="-?[0-9]*[.,]?[0-9]*" autocomplete="off" required placeholder="-74.0060">
|
| 999 |
<p class="helper-text">Range: -180 to 180</p>
|
| 1000 |
</div>
|
| 1001 |
|
|
|
|
| 1003 |
<label for="height">Building Height</label>
|
| 1004 |
<div class="height-group">
|
| 1005 |
<input type="number" id="height" step="any" value="0" placeholder="5.0">
|
| 1006 |
+
<button type="button" class="predict-height-btn" data-lat-id="latitude"
|
| 1007 |
+
data-lon-id="longitude" data-height-id="height"
|
| 1008 |
+
data-error-id="basic-error">Predict</button>
|
| 1009 |
</div>
|
| 1010 |
+
<button type="button" class="gba-height-btn" data-lat-id="latitude"
|
| 1011 |
+
data-lon-id="longitude" data-height-id="height" data-error-id="basic-error">
|
| 1012 |
+
Get height from Global Building Atlas
|
| 1013 |
+
</button>
|
| 1014 |
+
<p class="helper-text">
|
| 1015 |
+
Meters above ground (0 = ground level)
|
| 1016 |
+
</p>
|
| 1017 |
</div>
|
| 1018 |
|
| 1019 |
<div class="input-card">
|
| 1020 |
<label for="basement">Basement Depth</label>
|
| 1021 |
+
<input type="text" id="basement" name="basement" value="0" max="0" inputmode="text"
|
| 1022 |
+
pattern="-?[0-9]*[.,]?[0-9]*" placeholder="-2.0">
|
| 1023 |
<p class="helper-text">Meters below ground (negative values)</p>
|
| 1024 |
</div>
|
| 1025 |
</div>
|
|
|
|
| 1049 |
<div class="form-grid">
|
| 1050 |
<div class="input-card">
|
| 1051 |
<label for="latitude2">Latitude</label>
|
| 1052 |
+
<input type="text" id="latitude2" name="latitude2" pattern="-?[0-9]*[.,]?[0-9]*"
|
| 1053 |
+
autocomplete="off" required placeholder="40.7128">
|
| 1054 |
</div>
|
| 1055 |
|
| 1056 |
<div class="input-card">
|
| 1057 |
<label for="longitude2">Longitude</label>
|
| 1058 |
+
<input type="text" id="longitude2" name="longitude2" pattern="-?[0-9]*[.,]?[0-9]*"
|
| 1059 |
+
autocomplete="off" required placeholder="-74.0060">
|
| 1060 |
</div>
|
| 1061 |
|
| 1062 |
<div class="input-card">
|
| 1063 |
<label for="height2">Building Height</label>
|
| 1064 |
<div class="height-group">
|
| 1065 |
<input type="number" id="height2" step="any" value="0">
|
| 1066 |
+
<button type="button" class="predict-height-btn" data-lat-id="latitude2"
|
| 1067 |
+
data-lon-id="longitude2" data-height-id="height2"
|
| 1068 |
+
data-error-id="explained-error">Predict</button>
|
| 1069 |
</div>
|
| 1070 |
<p class="helper-text">Meters above ground</p>
|
| 1071 |
</div>
|
| 1072 |
|
| 1073 |
<div class="input-card">
|
| 1074 |
<label for="basement2">Basement Depth</label>
|
| 1075 |
+
<input type="text" id="basement2" name="basement2" inputmode="text"
|
| 1076 |
+
pattern="-?[0-9]*[.,]?[0-9]*" step="0.1" value="0" max="0">
|
| 1077 |
<p class="helper-text">Meters below ground (negative)</p>
|
| 1078 |
</div>
|
| 1079 |
</div>
|
|
|
|
| 1103 |
<div class="form-grid">
|
| 1104 |
<div class="input-card">
|
| 1105 |
<label for="latitude3">Latitude</label>
|
| 1106 |
+
<input type="text" id="latitude3" name="latitude3" pattern="-?[0-9]*[.,]?[0-9]*"
|
| 1107 |
+
autocomplete="off" required placeholder="40.7128">
|
| 1108 |
</div>
|
| 1109 |
|
| 1110 |
<div class="input-card">
|
| 1111 |
<label for="longitude3">Longitude</label>
|
| 1112 |
+
<input type="text" id="longitude3" name="longitude3" pattern="-?[0-9]*[.,]?[0-9]*"
|
| 1113 |
+
autocomplete="off" required placeholder="-74.0060">
|
| 1114 |
</div>
|
| 1115 |
|
| 1116 |
<div class="input-card">
|
| 1117 |
<label for="height3">Building Height</label>
|
| 1118 |
<div class="height-group">
|
| 1119 |
<input type="number" id="height3" step="any" value="0">
|
| 1120 |
+
<button type="button" class="predict-height-btn" data-lat-id="latitude3"
|
| 1121 |
+
data-lon-id="longitude3" data-height-id="height3"
|
| 1122 |
+
data-error-id="multihazard-error">Predict</button>
|
| 1123 |
</div>
|
| 1124 |
<p class="helper-text">Meters above ground</p>
|
| 1125 |
</div>
|
| 1126 |
|
| 1127 |
<div class="input-card">
|
| 1128 |
<label for="basement3">Basement Depth</label>
|
| 1129 |
+
<input type="text" id="basement3" name="basement3" inputmode="text"
|
| 1130 |
+
pattern="-?[0-9]*[.,]?[0-9]*" step="0.1" value="0" max="0">
|
| 1131 |
<p class="helper-text">Meters below ground (negative)</p>
|
| 1132 |
</div>
|
| 1133 |
</div>
|
|
|
|
| 1166 |
<div class="input-card">
|
| 1167 |
<label for="csvFile">CSV File</label>
|
| 1168 |
<input type="file" id="csvFile" accept=".csv">
|
| 1169 |
+
<p class="helper-text">Required columns: latitude, longitude in decimal degrees (e.g. 29.17,
|
| 1170 |
+
-95.31). Optional: height(meters), basement (should be negative).</p>
|
| 1171 |
</div>
|
| 1172 |
</div>
|
| 1173 |
|
|
|
|
| 1286 |
document.querySelectorAll('.nav-link').forEach(link => {
|
| 1287 |
link.classList.remove('active');
|
| 1288 |
});
|
| 1289 |
+
|
| 1290 |
document.getElementById(tabName + '-card').classList.add('active');
|
| 1291 |
event.target.classList.add('active');
|
| 1292 |
}
|
| 1293 |
+
|
| 1294 |
async function assessLocation(event, endpoint, resultsId) {
|
| 1295 |
event.preventDefault();
|
| 1296 |
+
|
| 1297 |
const tabName = resultsId.split('-')[0];
|
| 1298 |
const suffix = endpoint === '/assess' ? '' : (endpoint === '/explain' ? '2' : '3');
|
| 1299 |
const latitude = parseFloat(document.getElementById('latitude' + suffix).value);
|
| 1300 |
const longitude = parseFloat(document.getElementById('longitude' + suffix).value);
|
| 1301 |
const height = parseFloat(document.getElementById('height' + suffix).value) || 0;
|
| 1302 |
const basement = parseFloat(document.getElementById('basement' + suffix).value) || 0;
|
| 1303 |
+
|
| 1304 |
document.getElementById(tabName + '-loading').style.display = 'block';
|
| 1305 |
document.getElementById(resultsId).style.display = 'none';
|
| 1306 |
document.getElementById(tabName + '-error').style.display = 'none';
|
| 1307 |
+
|
| 1308 |
try {
|
| 1309 |
const response = await fetch(endpoint, {
|
| 1310 |
method: 'POST',
|
| 1311 |
headers: { 'Content-Type': 'application/json' },
|
| 1312 |
body: JSON.stringify({ latitude, longitude, height, basement })
|
| 1313 |
});
|
| 1314 |
+
|
| 1315 |
const data = await response.json();
|
| 1316 |
+
|
| 1317 |
if (data.status === 'success') {
|
| 1318 |
displayResults(data, resultsId, endpoint);
|
| 1319 |
} else {
|
|
|
|
| 1326 |
document.getElementById(tabName + '-loading').style.display = 'none';
|
| 1327 |
}
|
| 1328 |
}
|
| 1329 |
+
|
| 1330 |
function formatFlag(flag) {
|
| 1331 |
const flagMessages = {
|
| 1332 |
'missing_elevation': 'Elevation data unavailable',
|
|
|
|
| 1339 |
};
|
| 1340 |
return flagMessages[flag] || flag.replace(/_/g, ' ');
|
| 1341 |
}
|
| 1342 |
+
|
| 1343 |
function displayResults(data, resultsId, endpoint) {
|
| 1344 |
const resultsDiv = document.getElementById(resultsId);
|
| 1345 |
const assessment = data.assessment;
|
| 1346 |
+
|
| 1347 |
let html = '<div class="results-header">';
|
| 1348 |
html += '<h2>Assessment Complete</h2>';
|
| 1349 |
+
|
| 1350 |
const riskClass = 'risk-' + assessment.risk_level.replace(/_/g, '-');
|
| 1351 |
html += `<div class="risk-badge ${riskClass}">${assessment.risk_level.replace(/_/g, ' ')}</div>`;
|
| 1352 |
html += '</div>';
|
| 1353 |
+
|
| 1354 |
html += '<div class="stats-grid">';
|
| 1355 |
+
|
| 1356 |
if (assessment.confidence_interval) {
|
| 1357 |
const ci = assessment.confidence_interval;
|
| 1358 |
html += `
|
|
|
|
| 1372 |
</div>
|
| 1373 |
`;
|
| 1374 |
}
|
| 1375 |
+
|
| 1376 |
html += `
|
| 1377 |
<div class="stat-card">
|
| 1378 |
<div class="stat-label">Elevation</div>
|
| 1379 |
<div class="stat-value">${assessment.elevation_m}m</div>
|
| 1380 |
</div>
|
| 1381 |
`;
|
| 1382 |
+
|
| 1383 |
if (assessment.distance_to_water_m !== null) {
|
| 1384 |
html += `
|
| 1385 |
<div class="stat-card">
|
|
|
|
| 1388 |
</div>
|
| 1389 |
`;
|
| 1390 |
}
|
| 1391 |
+
|
| 1392 |
html += '</div>';
|
| 1393 |
+
|
| 1394 |
if (assessment.uncertainty_analysis) {
|
| 1395 |
const ua = assessment.uncertainty_analysis;
|
| 1396 |
const confidenceValue = parseFloat(ua.confidence) || 0;
|
|
|
|
| 1416 |
</p>
|
| 1417 |
</div>
|
| 1418 |
`;
|
| 1419 |
+
|
| 1420 |
if (ua.data_quality_flags && ua.data_quality_flags.length > 0) {
|
| 1421 |
+
const criticalFlags = ua.data_quality_flags.filter(flag =>
|
| 1422 |
+
flag === 'steep_terrain_dem_error_high' ||
|
| 1423 |
flag === 'coastal_surge_risk_not_modeled'
|
| 1424 |
);
|
| 1425 |
+
|
| 1426 |
if (criticalFlags.length > 0) {
|
| 1427 |
html += '<div class="quality-warning"><h4>⚠ Data Quality Notes</h4><ul>';
|
| 1428 |
criticalFlags.forEach(flag => {
|
|
|
|
| 1432 |
}
|
| 1433 |
}
|
| 1434 |
}
|
| 1435 |
+
|
| 1436 |
html += '<div class="detail-section"><h3>Terrain Analysis</h3>';
|
| 1437 |
html += `
|
| 1438 |
<div class="metric-row">
|
|
|
|
| 1453 |
</div>
|
| 1454 |
`;
|
| 1455 |
html += '</div>';
|
| 1456 |
+
|
| 1457 |
if (assessment.hazard_breakdown) {
|
| 1458 |
const hb = assessment.hazard_breakdown;
|
| 1459 |
html += '<div class="detail-section"><h3>Hazard Breakdown</h3>';
|
|
|
|
| 1476 |
html += `<p style="margin-top: 1.5rem;"><strong>Dominant Hazard:</strong> ${assessment.dominant_hazard.replace(/_/g, ' ').toUpperCase()}</p>`;
|
| 1477 |
html += '</div>';
|
| 1478 |
}
|
| 1479 |
+
|
| 1480 |
if (data.explanation) {
|
| 1481 |
const exp = data.explanation;
|
| 1482 |
html += '<div class="explanation-section">';
|
| 1483 |
html += '<h3>Risk Factor Analysis</h3>';
|
| 1484 |
html += `<p style="margin-bottom: 1.5rem; color: #cbd5e1;"><strong>Top Risk Driver:</strong> ${exp.top_risk_driver}</p>`;
|
| 1485 |
+
|
| 1486 |
exp.explanations.forEach(factor => {
|
| 1487 |
html += `
|
| 1488 |
<div class="factor-item">
|
|
|
|
| 1494 |
</div>
|
| 1495 |
`;
|
| 1496 |
});
|
| 1497 |
+
|
| 1498 |
html += '</div>';
|
| 1499 |
}
|
| 1500 |
+
|
| 1501 |
resultsDiv.innerHTML = html;
|
| 1502 |
resultsDiv.style.display = 'block';
|
| 1503 |
}
|
| 1504 |
+
|
| 1505 |
+
async function getHeightFromGBA(latId, lonId, heightId, errorId, button) {
|
| 1506 |
+
const lat = parseFloat(document.getElementById(latId).value);
|
| 1507 |
+
const lon = parseFloat(document.getElementById(lonId).value);
|
| 1508 |
+
|
| 1509 |
+
const errorBox = document.getElementById(errorId);
|
| 1510 |
+
if (errorBox) errorBox.textContent = '';
|
| 1511 |
+
|
| 1512 |
+
if (Number.isNaN(lat) || Number.isNaN(lon)) {
|
| 1513 |
+
if (errorBox) errorBox.textContent = 'Please enter valid latitude and longitude.';
|
| 1514 |
+
return;
|
| 1515 |
+
}
|
| 1516 |
+
|
| 1517 |
+
const originalText = button.textContent;
|
| 1518 |
+
button.disabled = true;
|
| 1519 |
+
button.textContent = 'Fetching...';
|
| 1520 |
+
|
| 1521 |
+
try {
|
| 1522 |
+
const resp = await fetch('/get_height_gba', {
|
| 1523 |
+
method: 'POST',
|
| 1524 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1525 |
+
body: JSON.stringify({ latitude: lat, longitude: lon, height: 0, basement: 0 })
|
| 1526 |
+
});
|
| 1527 |
+
|
| 1528 |
+
const data = await resp.json();
|
| 1529 |
+
if (!resp.ok) {
|
| 1530 |
+
const msg = data?.detail || 'Failed to get GBA height';
|
| 1531 |
+
throw new Error(msg);
|
| 1532 |
+
}
|
| 1533 |
+
|
| 1534 |
+
const h = data.predicted_height;
|
| 1535 |
+
if (h === null || h === undefined) {
|
| 1536 |
+
throw new Error('No height returned');
|
| 1537 |
+
}
|
| 1538 |
+
|
| 1539 |
+
document.getElementById(heightId).value = Number(h).toFixed(2);
|
| 1540 |
+
} catch (e) {
|
| 1541 |
+
if (errorBox) errorBox.textContent = String(e.message || e);
|
| 1542 |
+
} finally {
|
| 1543 |
+
button.disabled = false;
|
| 1544 |
+
button.textContent = originalText;
|
| 1545 |
+
}
|
| 1546 |
+
}
|
| 1547 |
+
|
| 1548 |
+
document.querySelectorAll('.gba-height-btn').forEach(btn => {
|
| 1549 |
+
btn.addEventListener('click', () => {
|
| 1550 |
+
getHeightFromGBA(
|
| 1551 |
+
btn.dataset.latId,
|
| 1552 |
+
btn.dataset.lonId,
|
| 1553 |
+
btn.dataset.heightId,
|
| 1554 |
+
btn.dataset.errorId,
|
| 1555 |
+
btn
|
| 1556 |
+
);
|
| 1557 |
+
});
|
| 1558 |
+
});
|
| 1559 |
+
|
| 1560 |
async function uploadBatch() {
|
| 1561 |
const fileInput = document.getElementById('csvFile');
|
| 1562 |
const file = fileInput.files[0];
|
| 1563 |
+
|
| 1564 |
if (!file) {
|
| 1565 |
alert('Please select a CSV file');
|
| 1566 |
return;
|
| 1567 |
}
|
| 1568 |
+
|
| 1569 |
document.getElementById('batch-loading').style.display = 'block';
|
| 1570 |
document.getElementById('batch-results').style.display = 'none';
|
| 1571 |
document.getElementById('batch-error').style.display = 'none';
|
| 1572 |
+
|
| 1573 |
const formData = new FormData();
|
| 1574 |
formData.append('file', file);
|
| 1575 |
+
|
| 1576 |
try {
|
| 1577 |
const mode = document.getElementById('batchMode').value;
|
| 1578 |
const usePredicted = document.getElementById('usePredictedHeight').checked;
|
|
|
|
| 1587 |
method: 'POST',
|
| 1588 |
body: formData
|
| 1589 |
});
|
| 1590 |
+
|
| 1591 |
if (response.ok) {
|
| 1592 |
const blob = await response.blob();
|
| 1593 |
const url = window.URL.createObjectURL(blob);
|
| 1594 |
const a = document.createElement('a');
|
| 1595 |
a.href = url;
|
| 1596 |
+
const filename = mode === 'multihazard'
|
| 1597 |
+
? 'multihazard_results.csv'
|
| 1598 |
: 'vulnerability_results.csv';
|
| 1599 |
a.download = filename;
|
| 1600 |
document.body.appendChild(a);
|
| 1601 |
a.click();
|
| 1602 |
window.URL.revokeObjectURL(url);
|
| 1603 |
+
|
| 1604 |
document.getElementById('batch-results').innerHTML = `
|
| 1605 |
<div class="results-header">
|
| 1606 |
<h2>✓ Processing Complete</h2>
|
|
|
|
| 1620 |
}
|
| 1621 |
</script>
|
| 1622 |
</body>
|
| 1623 |
+
|
| 1624 |
+
</html>
|