🏛️ Church & SDOH Data Mapper
Interactive mapping of church locations with Social Determinants of Health data
${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);
});