Church & SDOH Data Mapper

🏛️ Church & SDOH Data Mapper

Interactive mapping of church locations with Social Determinants of Health data

+ Math.round(value).toLocaleString(); } else if (indicator.includes('%') || indicator === 'Poverty Rate') { formattedValue = value.toFixed(1) + '%'; } polygon.bindPopup(` Census Tract ${tractId}
${indicator}: ${formattedValue}
Generated for church area analysis `); layerGroup.addLayer(polygon); } }); sdohLayers[indicator] = layerGroup; } // Handle file upload with improved processing document.getElementById('churchFile').onchange = function(e) { const files = e.target.files; if (files.length === 0) return; showStatus('churchStatus', 'Processing files...', 'info'); // Clear existing churches and data churchLayer.clearLayers(); churchData = []; // Clear existing SDOH layers Object.values(sdohLayers).forEach(layer => { if (map.hasLayer(layer)) { map.removeLayer(layer); } }); sdohLayers = {}; // Reset layer controls document.getElementById('layerControls').innerHTML = '
Upload churches first, then generate SDOH data
'; // Reset and disable SDOH button const sdohButton = document.getElementById('sdohButton'); sdohButton.disabled = true; sdohButton.textContent = 'Upload Churches First'; Array.from(files).forEach(file => { const reader = new FileReader(); if (file.name.endsWith('.csv')) { reader.onload = function(e) { parseCSV(e.target.result, file.name); }; reader.readAsText(file); } else if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { reader.onload = function(e) { parseExcel(e.target.result, file.name); }; reader.readAsArrayBuffer(file); } }); }; // Parse CSV function parseCSV(csvText, filename) { Papa.parse(csvText, { header: true, complete: function(results) { processChurchData(results.data, filename); }, error: function(error) { showStatus('churchStatus', 'Error parsing CSV: ' + error.message, 'error'); } }); } // Parse Excel function parseExcel(buffer, filename) { try { const workbook = XLSX.read(buffer, { type: 'array' }); const sheetName = workbook.SheetNames[0]; const sheet = workbook.Sheets[sheetName]; const data = XLSX.utils.sheet_to_json(sheet); processChurchData(data, filename); } catch (error) { showStatus('churchStatus', 'Error parsing Excel: ' + error.message, 'error'); } } // Process church data with geocoding support async function processChurchData(data, filename) { let processed = 0; let geocoded = 0; const churches = []; const geocodingQueue = []; showStatus('churchStatus', `Processing ${data.length} records from ${filename}...`, 'info'); data.forEach((row, index) => { // Try different column name variations for coordinates const lat = parseFloat(row.latitude || row.lat || row.Latitude || row.LAT || row.y); const lng = parseFloat(row.longitude || row.lng || row.lon || row.Longitude || row.LON || row.x); const name = row.name || row.Name || row.church_name || row.ChurchName || row.organization || row.Organization || `Church ${index + 1}`; // If we have valid coordinates, use them directly if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { churches.push({ name, lat, lng, source: 'coordinates' }); processed++; } else { // Try to build address from address components const streetAddress = row['Street Address'] || row.street_address || row.address || row.Address || row.street || row.Street; const city = row.City || row.city || row.CITY; const state = row.State || row.state || row.STATE || row.st || row.ST; const zipCode = row['Zip Code'] || row.zip_code || row.zip || row.ZIP || row.zipcode || row.postal_code; if (streetAddress || city) { geocodingQueue.push({ name, streetAddress, city, state, zipCode, index }); } } }); // Add churches with existing coordinates first if (churches.length > 0) { addChurches(churches); showStatus('churchStatus', `Added ${processed} churches with coordinates. Geocoding ${geocodingQueue.length} addresses...`, 'info'); } // Geocode addresses in batches to avoid rate limiting if (geocodingQueue.length > 0) { await geocodeAddresses(geocodingQueue, filename); } else if (churches.length === 0) { showStatus('churchStatus', 'No valid coordinates or addresses found. Please check your data has either lat/lng columns or address columns (Street Address, City, State, Zip Code).', 'error'); } } // Geocode addresses using Nominatim async function geocodeAddresses(addressQueue, filename) { const geocodedChurches = []; let successCount = 0; let failCount = 0; for (let i = 0; i < addressQueue.length; i++) { const item = addressQueue[i]; try { // Build address string let addressString = ''; if (item.streetAddress) addressString += item.streetAddress; if (item.city) addressString += (addressString ? ', ' : '') + item.city; if (item.state) addressString += (addressString ? ', ' : '') + item.state; if (item.zipCode) addressString += (addressString ? ' ' : '') + item.zipCode; if (!addressString) { failCount++; continue; } // Update status every 5 items if (i % 5 === 0) { showStatus('churchStatus', `Geocoding ${i + 1}/${addressQueue.length}: ${item.name}`, 'info'); } const coords = await geocodeAddress(addressString); if (coords) { geocodedChurches.push({ name: item.name, lat: coords.lat, lng: coords.lng, address: addressString, source: 'geocoded' }); successCount++; } else { failCount++; } // Add small delay to be respectful to the geocoding service if (i < addressQueue.length - 1) { await sleep(200); // 200ms delay between requests } } catch (error) { console.error(`Geocoding failed for ${item.name}:`, error); failCount++; } } // Add geocoded churches to map if (geocodedChurches.length > 0) { addChurches(geocodedChurches); } // Final status message const totalProcessed = successCount + (churchLayer.getLayers().length - geocodedChurches.length); showStatus('churchStatus', `Completed! Added ${totalProcessed} churches from ${filename}. ` + `Geocoded: ${successCount}, Failed: ${failCount}`, totalProcessed > 0 ? 'success' : 'error' ); // Fit map to show all churches if (churchLayer.getLayers().length > 0) { setTimeout(() => { const group = new L.featureGroup(churchLayer.getLayers()); if (group.getBounds().isValid()) { map.fitBounds(group.getBounds().pad(0.1)); } }, 500); } } // Geocode a single address using Nominatim async function geocodeAddress(address) { try { const encodedAddress = encodeURIComponent(address); const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodedAddress}&limit=1&countrycodes=us`; const response = await fetch(url, { headers: { 'User-Agent': 'Church-SDOH-Mapper/1.0' } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data && data.length > 0) { return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon) }; } return null; } catch (error) { console.error('Geocoding error:', error); return null; } } // Sleep utility function function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Show status message function showStatus(elementId, message, type) { const element = document.getElementById(elementId); element.className = `status status-${type}`; element.textContent = message; if (type === 'success') { setTimeout(() => { element.textContent = ''; element.className = ''; }, 5000); } } // Initialize when page loads document.addEventListener('DOMContentLoaded', function() { // Wait a moment for Leaflet to load setTimeout(initMap, 100); });