adema5051's picture
Upload 8 files
95cb050 verified
// CACHE MANAGEMENT
const CACHE_KEY = 'flood_assessment_cache';
const MAX_CACHE_SIZE = 5;
// Get cached entries from localStorage
function getCachedEntries() {
try {
const cached = localStorage.getItem(CACHE_KEY);
return cached ? JSON.parse(cached) : [];
} catch (e) {
console.error('Error reading cache:', e);
return [];
}
}
function saveToCacheFunction(latitude, longitude, height, basement) {
try {
let entries = getCachedEntries();
const newEntry = {
latitude: parseFloat(latitude).toFixed(6),
longitude: parseFloat(longitude).toFixed(6),
height: parseFloat(height).toFixed(2),
basement: parseFloat(basement).toFixed(2),
timestamp: Date.now()
};
// Check if entry already exists
const exists = entries.some(entry =>
entry.latitude === newEntry.latitude &&
entry.longitude === newEntry.longitude &&
entry.height === newEntry.height &&
entry.basement === newEntry.basement
);
if (!exists) {
entries.unshift(newEntry);
if (entries.length > MAX_CACHE_SIZE) {
entries = entries.slice(0, MAX_CACHE_SIZE);
}
localStorage.setItem(CACHE_KEY, JSON.stringify(entries));
updateDatalistSuggestions();
}
} catch (e) {
console.error('Error saving to cache:', e);
}
}
// Update datalist suggestions from cache
function updateDatalistSuggestions() {
const entries = getCachedEntries();
updateDatalist('lat-suggestions', entries, 'latitude');
updateDatalist('lon-suggestions', entries, 'longitude');
updateDatalist('height-suggestions', entries, 'height');
updateDatalist('basement-suggestions', entries, 'basement');
}
// Update a specific datalist
function updateDatalist(datalistId, entries, field) {
const datalist = document.getElementById(datalistId);
if (!datalist) return;
datalist.innerHTML = '';
entries.forEach((entry) => {
const option = document.createElement('option');
option.value = entry[field];
if (field === 'latitude') {
option.label = `${entry.latitude} | Lon: ${entry.longitude}, H: ${entry.height}m, B: ${entry.basement}m`;
} else if (field === 'longitude') {
option.label = `${entry.longitude} | Lat: ${entry.latitude}, H: ${entry.height}m, B: ${entry.basement}m`;
} else if (field === 'height') {
option.label = `${entry.height}m | At: ${entry.latitude}, ${entry.longitude}`;
} else if (field === 'basement') {
option.label = `${entry.basement}m | At: ${entry.latitude}, ${entry.longitude}`;
}
datalist.appendChild(option);
});
}
// Setup autofill functionality
function setupAutoFill() {
const forms = [
{ suffix: '', latId: 'latitude', lonId: 'longitude' },
{ suffix: '2', latId: 'latitude2', lonId: 'longitude2' },
{ suffix: '3', latId: 'latitude3', lonId: 'longitude3' }
];
forms.forEach(({ suffix, latId, lonId }) => {
const latInput = document.getElementById(latId);
const lonInput = document.getElementById(lonId);
if (!latInput || !lonInput) return;
const tryAutoFill = () => {
const latValue = parseNumber(latInput.value);
const lonValue = parseNumber(lonInput.value);
if (isNaN(latValue) || isNaN(lonValue)) return;
const entries = getCachedEntries();
const normalizedLat = latValue.toFixed(6);
const normalizedLon = lonValue.toFixed(6);
// Find entry matching BOTH lat and lon
const match = entries.find(entry =>
entry.latitude === normalizedLat &&
entry.longitude === normalizedLon
);
if (match) {
const heightInput = document.getElementById('height' + suffix);
const basementInput = document.getElementById('basement' + suffix);
if (heightInput && basementInput) {
heightInput.value = match.height;
basementInput.value = match.basement;
[latInput, lonInput, heightInput, basementInput].forEach(input => {
input.classList.add('height-pulse');
setTimeout(() => input.classList.remove('height-pulse'), 800);
});
}
}
};
latInput.addEventListener('change', () => {
setTimeout(tryAutoFill, 100);
});
lonInput.addEventListener('change', () => {
setTimeout(tryAutoFill, 100);
});
});
}
// UTILITY FUNCTIONS
// Parse numbers with comma or dot as decimal separator
function parseNumber(value) {
if (typeof value === 'string') {
value = value.replace(',', '.');
}
return parseFloat(value);
}
// Format quality flag for display
function formatFlag(flag) {
const flagMessages = {
'missing_elevation': 'Elevation data unavailable',
'missing_tpi': 'Topographic position data incomplete',
'missing_slope': 'Slope data incomplete',
'water_distance_unknown': 'Water proximity uncertain',
'far_from_water_search_limited': 'Far from major water bodies (search radius limited)',
'steep_terrain_dem_error_high': 'Steep terrain increases measurement uncertainty',
'coastal_surge_risk_not_modeled': 'Coastal surge dynamics not fully captured'
};
return flagMessages[flag] || flag.replace(/_/g, ' ');
}
// UI INTERACTIONS
// Switch between assessment tabs
function switchTab(tabName) {
document.querySelectorAll('.assessment-card').forEach(card => {
card.classList.remove('active');
});
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
document.getElementById(tabName + '-card').classList.add('active');
event.target.classList.add('active');
}
// API COMMUNICATION
// Predict building height from coordinates
async function predictHeight(latId, lonId, heightId, errorId, button) {
const latInput = document.getElementById(latId);
const lonInput = document.getElementById(lonId);
const heightInput = document.getElementById(heightId);
const errorBox = document.getElementById(errorId);
if (!latInput || !lonInput || !heightInput || !errorBox) {
return;
}
errorBox.style.display = 'none';
errorBox.textContent = '';
const latitude = parseFloat(latInput.value);
const longitude = parseFloat(lonInput.value);
if (isNaN(latitude) || isNaN(longitude)) {
errorBox.textContent = 'Please enter latitude and longitude first.';
errorBox.style.display = 'block';
return;
}
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Predicting...';
try {
const response = await fetch('/predict_height', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
latitude,
longitude,
height: 0,
basement: 0
})
});
const data = await response.json();
if (!response.ok || data.status !== 'success' || data.predicted_height == null) {
const message = data.detail || data.error || 'Height prediction failed.';
throw new Error(message);
}
const h = Number(data.predicted_height);
heightInput.value = h.toFixed(2);
heightInput.classList.add('height-pulse');
setTimeout(() => {
heightInput.classList.remove('height-pulse');
}, 800);
} catch (err) {
errorBox.textContent = err.message || 'Height prediction failed.';
errorBox.style.display = 'block';
} finally {
button.disabled = false;
button.textContent = originalText;
}
}
// Get height from Global Building Atlas (GBA)
async function getHeightFromGBA(latId, lonId, heightId, errorId, button) {
const lat = parseNumber(document.getElementById(latId).value);
const lon = parseNumber(document.getElementById(lonId).value);
const errorBox = document.getElementById(errorId);
if (errorBox) errorBox.textContent = '';
if (Number.isNaN(lat) || Number.isNaN(lon)) {
if (errorBox) errorBox.textContent = 'Please enter valid latitude and longitude.';
return;
}
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Fetching...';
try {
const resp = await fetch('/get_height_gba', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ latitude: lat, longitude: lon, height: 0, basement: 0 })
});
const data = await resp.json();
if (!resp.ok) {
const msg = data?.detail || 'Failed to get GBA height';
throw new Error(msg);
}
const h = data.predicted_height;
if (h === null || h === undefined) {
throw new Error('No height returned');
}
document.getElementById(heightId).value = Number(h).toFixed(2);
} catch (e) {
if (errorBox) {
errorBox.textContent = String(e.message || e);
errorBox.style.display = 'block';
}
} finally {
button.disabled = false;
button.textContent = originalText;
}
}
// Assess location vulnerability
async function assessLocation(event, endpoint, resultsId) {
event.preventDefault();
const tabName = resultsId.split('-')[0];
const suffix = endpoint === '/assess' ? '' : (endpoint === '/explain' ? '2' : '3');
const latitude = parseFloat(document.getElementById('latitude' + suffix).value);
const longitude = parseFloat(document.getElementById('longitude' + suffix).value);
const height = parseFloat(document.getElementById('height' + suffix).value) || 0;
const basement = parseFloat(document.getElementById('basement' + suffix).value) || 0;
document.getElementById(tabName + '-loading').style.display = 'block';
document.getElementById(resultsId).style.display = 'none';
document.getElementById(tabName + '-error').style.display = 'none';
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ latitude, longitude, height, basement })
});
const data = await response.json();
if (data.status === 'success') {
// Save to cache on successful assessment
saveToCacheFunction(latitude, longitude, height, basement);
displayResults(data, resultsId, endpoint);
} else {
throw new Error(data.detail || 'Assessment failed');
}
} catch (error) {
document.getElementById(tabName + '-error').textContent = error.message;
document.getElementById(tabName + '-error').style.display = 'block';
} finally {
document.getElementById(tabName + '-loading').style.display = 'none';
}
}
/**
* Upload and process batch CSV file
*/
async function uploadBatch() {
const fileInput = document.getElementById('csvFile');
const file = fileInput.files[0];
if (!file) {
alert('Please select a CSV file');
return;
}
document.getElementById('batch-loading').style.display = 'block';
document.getElementById('batch-results').style.display = 'none';
document.getElementById('batch-error').style.display = 'none';
const formData = new FormData();
formData.append('file', file);
try {
const mode = document.getElementById('batchMode').value;
const heightSource = document.getElementById('heightSource').value;
let endpoint = mode === 'multihazard' ? '/assess_batch_multihazard' : '/assess_batch';
if (heightSource === 'gba') {
const sep = endpoint.includes('?') ? '&' : '?';
endpoint = endpoint + sep + 'use_gba_height=true';
} else if (heightSource === 'predicted') {
const sep = endpoint.includes('?') ? '&' : '?';
endpoint = endpoint + sep + 'use_predicted_height=true';
}
const response = await fetch(endpoint, {
method: 'POST',
body: formData
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const filename = mode === 'multihazard'
? 'multihazard_results.csv'
: 'vulnerability_results.csv';
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.getElementById('batch-results').innerHTML = `
<div class="results-header">
<h2>✓ Processing Complete</h2>
<p style="color: #94a3b8; margin-top: 1rem;">Results downloaded as ${filename}</p>
</div>
`;
document.getElementById('batch-results').style.display = 'block';
} else {
throw new Error('Batch processing failed');
}
} catch (error) {
document.getElementById('batch-error').textContent = error.message;
document.getElementById('batch-error').style.display = 'block';
} finally {
document.getElementById('batch-loading').style.display = 'none';
}
}
// ========================================
// RESULTS RENDERING
// ========================================
/**
* Display assessment results
*/
function displayResults(data, resultsId, endpoint) {
const resultsDiv = document.getElementById(resultsId);
const assessment = data.assessment;
let html = '<div class="results-header">';
html += '<h2>Assessment Complete</h2>';
const riskClass = 'risk-' + assessment.risk_level.replace(/_/g, '-');
html += `<div class="risk-badge ${riskClass}">${assessment.risk_level.replace(/_/g, ' ')}</div>`;
html += '</div>';
html += '<div class="stats-grid">';
if (assessment.confidence_interval) {
const ci = assessment.confidence_interval;
html += `
<div class="stat-card">
<div class="stat-label">Vulnerability Index</div>
<div class="stat-value">${ci.point_estimate}</div>
<p style="font-size: 0.85em; color: #64748b; margin-top: 0.5rem;">
95% CI: ${ci.lower_bound_95}${ci.upper_bound_95}
</p>
</div>
`;
} else {
html += `
<div class="stat-card">
<div class="stat-label">Vulnerability Index</div>
<div class="stat-value">${assessment.vulnerability_index}</div>
</div>
`;
}
html += `
<div class="stat-card">
<div class="stat-label">Elevation</div>
<div class="stat-value">${assessment.elevation_m}m</div>
</div>
`;
if (assessment.distance_to_water_m !== null) {
html += `
<div class="stat-card">
<div class="stat-label">Distance to Water</div>
<div class="stat-value">${assessment.distance_to_water_m}m</div>
</div>
`;
}
html += '</div>';
if (assessment.uncertainty_analysis) {
const ua = assessment.uncertainty_analysis;
const confidenceValue = parseFloat(ua.confidence) || 0;
const barWidth = Math.round(confidenceValue * 100);
let confidenceClass = 'confidence-low-fill';
if (confidenceValue >= 0.75) confidenceClass = 'confidence-high';
else if (confidenceValue >= 0.55) confidenceClass = 'confidence-moderate-fill';
html += `
<div class="confidence-section">
<h3>Assessment Confidence</h3>
<div class="confidence-bar-wrapper">
<span>Low</span>
<div class="confidence-bar">
<div class="confidence-fill ${confidenceClass}" style="width: ${barWidth}%;"></div>
<span class="confidence-text">${barWidth}%</span>
</div>
<span>High</span>
</div>
<p style="margin-top: 1rem; color: #94a3b8; font-style: italic;">
${ua.interpretation}
</p>
</div>
`;
if (ua.data_quality_flags && ua.data_quality_flags.length > 0) {
const criticalFlags = ua.data_quality_flags.filter(flag =>
flag === 'steep_terrain_dem_error_high' ||
flag === 'coastal_surge_risk_not_modeled'
);
if (criticalFlags.length > 0) {
html += '<div class="quality-warning"><h4>⚠ Data Quality Notes</h4><ul>';
criticalFlags.forEach(flag => {
html += `<li>${formatFlag(flag)}</li>`;
});
html += '</ul></div>';
}
}
}
html += '<div class="detail-section"><h3>Terrain Analysis</h3>';
html += `
<div class="metric-row">
<span class="metric-label">Elevation</span>
<span class="metric-value">${assessment.elevation_m} m</span>
</div>
<div class="metric-row">
<span class="metric-label">Relative Elevation (TPI)</span>
<span class="metric-value">${assessment.relative_elevation_m !== null ? assessment.relative_elevation_m + ' m' : 'N/A'}</span>
</div>
<div class="metric-row">
<span class="metric-label">Slope</span>
<span class="metric-value">${assessment.slope_degrees !== null ? assessment.slope_degrees + '°' : 'N/A'}</span>
</div>
<div class="metric-row">
<span class="metric-label">Distance to Water</span>
<span class="metric-value">${assessment.distance_to_water_m !== null ? assessment.distance_to_water_m + ' m' : 'N/A'}</span>
</div>
`;
html += '</div>';
if (assessment.hazard_breakdown) {
const hb = assessment.hazard_breakdown;
html += '<div class="detail-section"><h3>Hazard Breakdown</h3>';
html += '<div class="hazard-grid">';
html += `
<div class="hazard-card">
<div class="hazard-type">Fluvial/Riverine</div>
<div class="hazard-value">${hb.fluvial_riverine}</div>
</div>
<div class="hazard-card">
<div class="hazard-type">Coastal Surge</div>
<div class="hazard-value">${hb.coastal_surge}</div>
</div>
<div class="hazard-card">
<div class="hazard-type">Pluvial/Drainage</div>
<div class="hazard-value">${hb.pluvial_drainage}</div>
</div>
`;
html += '</div>';
html += `<p style="margin-top: 1.5rem;"><strong>Dominant Hazard:</strong> ${assessment.dominant_hazard.replace(/_/g, ' ').toUpperCase()}</p>`;
html += '</div>';
}
if (data.explanation) {
const exp = data.explanation;
html += '<div class="explanation-section">';
html += '<h3>Risk Factor Analysis</h3>';
html += `<p style="margin-bottom: 1.5rem; color: #cbd5e1;"><strong>Top Risk Driver:</strong> ${exp.top_risk_driver}</p>`;
exp.explanations.forEach(factor => {
html += `
<div class="factor-item">
<span class="factor-name">${factor.factor}</span>
<div class="factor-bar">
<div class="factor-fill" style="width: ${factor.contribution_pct}%"></div>
</div>
<span class="factor-percentage">${factor.contribution_pct}%</span>
</div>
`;
});
html += '</div>';
}
resultsDiv.innerHTML = html;
resultsDiv.style.display = 'block';
}
// ========================================
// INITIALIZATION
// ========================================
document.addEventListener('DOMContentLoaded', () => {
// Initialize cache and autofill
updateDatalistSuggestions();
setupAutoFill();
// Setup predict height buttons
const buttons = document.querySelectorAll('.predict-height-btn');
if (buttons.length > 0) {
buttons.forEach(button => {
const latId = button.dataset.latId;
const lonId = button.dataset.lonId;
const heightId = button.dataset.heightId;
const errorId = button.dataset.errorId;
button.addEventListener('click', () => {
predictHeight(latId, lonId, heightId, errorId, button);
});
});
}
// Setup GBA height buttons
document.querySelectorAll('.gba-height-btn').forEach(btn => {
btn.addEventListener('click', () => {
getHeightFromGBA(
btn.dataset.latId,
btn.dataset.lonId,
btn.dataset.heightId,
btn.dataset.errorId,
btn
);
});
});
});