// 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 = `
Results downloaded as ${filename}
95% CI: ${ci.lower_bound_95}–${ci.upper_bound_95}
${ua.interpretation}
Dominant Hazard: ${assessment.dominant_hazard.replace(/_/g, ' ').toUpperCase()}
`; html += 'Top Risk Driver: ${exp.top_risk_driver}
`; exp.explanations.forEach(factor => { html += `