// 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 = `

✓ Processing Complete

Results downloaded as ${filename}

`; 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 = '
'; html += '

Assessment Complete

'; const riskClass = 'risk-' + assessment.risk_level.replace(/_/g, '-'); html += `
${assessment.risk_level.replace(/_/g, ' ')}
`; html += '
'; html += '
'; if (assessment.confidence_interval) { const ci = assessment.confidence_interval; html += `
Vulnerability Index
${ci.point_estimate}

95% CI: ${ci.lower_bound_95}–${ci.upper_bound_95}

`; } else { html += `
Vulnerability Index
${assessment.vulnerability_index}
`; } html += `
Elevation
${assessment.elevation_m}m
`; if (assessment.distance_to_water_m !== null) { html += `
Distance to Water
${assessment.distance_to_water_m}m
`; } html += '
'; 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 += `

Assessment Confidence

Low
${barWidth}%
High

${ua.interpretation}

`; 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 += '

⚠ Data Quality Notes

'; } } } html += '

Terrain Analysis

'; html += `
Elevation ${assessment.elevation_m} m
Relative Elevation (TPI) ${assessment.relative_elevation_m !== null ? assessment.relative_elevation_m + ' m' : 'N/A'}
Slope ${assessment.slope_degrees !== null ? assessment.slope_degrees + '°' : 'N/A'}
Distance to Water ${assessment.distance_to_water_m !== null ? assessment.distance_to_water_m + ' m' : 'N/A'}
`; html += '
'; if (assessment.hazard_breakdown) { const hb = assessment.hazard_breakdown; html += '

Hazard Breakdown

'; html += '
'; html += `
Fluvial/Riverine
${hb.fluvial_riverine}
Coastal Surge
${hb.coastal_surge}
Pluvial/Drainage
${hb.pluvial_drainage}
`; html += '
'; html += `

Dominant Hazard: ${assessment.dominant_hazard.replace(/_/g, ' ').toUpperCase()}

`; html += '
'; } if (data.explanation) { const exp = data.explanation; html += '
'; html += '

Risk Factor Analysis

'; html += `

Top Risk Driver: ${exp.top_risk_driver}

`; exp.explanations.forEach(factor => { html += `
${factor.factor}
${factor.contribution_pct}%
`; }); html += '
'; } 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 ); }); }); });