CSV Upload
@@ -1227,6 +1237,7 @@
Original Address |
Geocoded Address |
Coordinates |
+
Provider |
diff --git a/map/app/public/css/admin/data-convert.css b/map/app/public/css/admin/data-convert.css
index f80b488..705f063 100644
--- a/map/app/public/css/admin/data-convert.css
+++ b/map/app/public/css/admin/data-convert.css
@@ -1,6 +1,72 @@
/* Data Conversion Interface Styles */
/* CSV upload, processing, and results preview components */
+/* Geocoding Provider Status */
+.geocoding-status-container {
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 6px;
+ padding: 15px;
+}
+
+.provider-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+
+.provider-item {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 5px 10px;
+ border-radius: 4px;
+ font-size: 14px;
+ background: white;
+ border: 1px solid #e9ecef;
+}
+
+.provider-item.provider-available {
+ border-color: #28a745;
+ background: #f8fff9;
+}
+
+.provider-item.provider-unavailable {
+ border-color: #dc3545;
+ background: #fff8f8;
+ opacity: 0.7;
+}
+
+.provider-premium {
+ background: #ffc107;
+ color: #212529;
+ font-size: 11px;
+ padding: 1px 4px;
+ border-radius: 3px;
+ font-weight: bold;
+}
+
+.provider-note {
+ margin-top: 10px;
+ padding: 10px;
+ background: #e8f4fd;
+ border: 1px solid #b8daff;
+ border-radius: 4px;
+ font-size: 13px;
+}
+
+.provider-note ul {
+ margin: 5px 0 0 20px;
+}
+
+.provider-note code {
+ background: #f8f9fa;
+ padding: 2px 4px;
+ border-radius: 3px;
+ font-family: monospace;
+}
+
.data-convert-container {
display: flex;
flex-direction: column;
@@ -192,11 +258,55 @@
animation: slideIn 0.3s ease-out;
}
+.result-warning {
+ background: #fff3cd;
+ border-left: 4px solid #ffc107;
+ animation: slideIn 0.3s ease-out;
+}
+
.result-error {
background: #f8d7da;
animation: slideIn 0.3s ease-out;
}
+/* Status icons */
+.status-icon {
+ font-weight: bold;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 12px;
+}
+
+.status-icon.success {
+ background: #d1e7dd;
+ color: #0f5132;
+}
+
+.status-icon.warning {
+ background: #fff3cd;
+ color: #664d03;
+}
+
+.status-icon.error {
+ background: #f8d7da;
+ color: #721c24;
+}
+
+/* Custom markers for warnings */
+.custom-marker {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.warning-marker {
+ filter: drop-shadow(0 2px 4px rgba(255, 193, 7, 0.4));
+}
+
+.success-marker {
+ filter: drop-shadow(0 2px 4px rgba(40, 167, 69, 0.4));
+}
+
.error-message {
color: #721c24;
font-style: italic;
diff --git a/map/app/public/js/data-convert.js b/map/app/public/js/data-convert.js
index ee96114..922a366 100644
--- a/map/app/public/js/data-convert.js
+++ b/map/app/public/js/data-convert.js
@@ -4,6 +4,83 @@ let resultsMap = null;
let markers = [];
let eventListenersInitialized = false;
+// Check and display geocoding provider status
+async function checkGeocodingProviders() {
+ const statusElement = document.getElementById('provider-status');
+ if (!statusElement) return;
+
+ try {
+ const response = await fetch('/api/geocode/provider-status', {
+ method: 'GET',
+ credentials: 'same-origin',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ if (data.success) {
+ const providers = data.providers;
+ let statusHTML = '
';
+
+ providers.forEach(provider => {
+ const icon = provider.available ? '✅' : '❌';
+ const status = provider.available ? 'Available' : 'Not configured';
+ const className = provider.available ? 'provider-available' : 'provider-unavailable';
+
+ statusHTML += `
+
+ ${icon}
+ ${provider.name}: ${status}
+ ${provider.name === 'Mapbox' && provider.available ?
+ '🌟 Premium' : ''}
+
+ `;
+ });
+
+ statusHTML += '
';
+
+ // Add configuration note if no premium providers
+ const hasPremium = providers.some(p => p.available && ['Mapbox', 'LocationIQ'].includes(p.name));
+ if (!hasPremium) {
+ statusHTML += `
+
+
💡 Tip: For better geocoding accuracy, configure a premium provider:
+
+ - Mapbox: Add
MAPBOX_ACCESS_TOKEN to your .env file
+ - LocationIQ: Add
LOCATIONIQ_API_KEY to your .env file
+
+
+ `;
+ } else if (providers.find(p => p.name === 'Mapbox' && p.available)) {
+ statusHTML += `
+
+
🌟 Mapbox Configured! Using premium geocoding for better accuracy.
+
+ `;
+ }
+
+ statusElement.innerHTML = statusHTML;
+ } else {
+ statusElement.innerHTML = '
❌ Failed to check provider status';
+ }
+ } catch (error) {
+ console.error('Failed to check geocoding providers:', error);
+ statusElement.innerHTML = `
+
+ ⚠️ Unable to check provider status
+ Using fallback providers: Nominatim, Photon, ArcGIS
+
+ `;
+ }
+}
+
// Utility function to show status messages
function showDataConvertStatus(message, type = 'info') {
// Try to use the global showStatus from admin.js if available
@@ -49,6 +126,9 @@ function setupDataConvertEventListeners() {
return;
}
+ // Check geocoding provider status when section loads
+ checkGeocodingProviders();
+
const fileInput = document.getElementById('csv-file-input');
const browseBtn = document.getElementById('browse-btn');
const uploadArea = document.getElementById('upload-area');
@@ -290,15 +370,36 @@ function handleProcessingUpdate(data) {
case 'progress':
updateProgress(data.current, data.total);
- document.getElementById('current-address').textContent = `Processing: ${data.address}`;
+
+ // Show current address with status
+ if (data.status === 'failed') {
+ document.getElementById('current-address').innerHTML = `
✗ ${data.currentAddress || data.address}`;
+ } else if (data.status === 'processing') {
+ document.getElementById('current-address').innerHTML = `
⟳ Processing: ${data.currentAddress || data.address}`;
+ } else {
+ document.getElementById('current-address').innerHTML = `
✓ ${data.currentAddress || data.address}`;
+ }
break;
case 'geocoded':
- // Mark as successful and add to processing data
- const successData = { ...data.data, geocode_success: true };
+ // Check if result has warnings
+ const isWarning = data.status === 'warning' || (data.data && data.data.is_malformed);
+ const confidence = data.confidence || (data.data && data.data.confidence_score) || 100;
+ const warnings = data.warnings || (data.data && data.data.warnings) || [];
+
+ // Mark data with appropriate status and add to processing data
+ const successData = {
+ ...data.data,
+ geocode_success: true,
+ confidence_score: confidence,
+ warnings: Array.isArray(warnings) ? warnings.join('; ') : warnings
+ };
processingData.push(successData);
- addResultToTable(successData, 'success');
- addMarkerToMap(successData);
+
+ // Add to table with appropriate status
+ const resultStatus = isWarning ? 'warning' : 'success';
+ addResultToTable(successData, resultStatus, confidence, warnings);
+ addMarkerToMap(successData, isWarning);
updateProgress(data.index + 1, data.total);
break;
@@ -312,6 +413,20 @@ function handleProcessingUpdate(data) {
case 'complete':
console.log('Received complete event:', data);
currentSessionId = data.sessionId; // Store session ID for report download
+
+ // Show comprehensive completion message
+ let completionMessage = `Complete! Processed ${data.total} addresses:\n`;
+ completionMessage += `✓ ${data.successful || 0} successful\n`;
+ if (data.warnings > 0) {
+ completionMessage += `⚠ ${data.warnings} with warnings (low confidence)\n`;
+ }
+ if (data.malformed > 0) {
+ completionMessage += `🔍 ${data.malformed} potentially malformed (need review)\n`;
+ }
+ completionMessage += `✗ ${data.errors || data.failed || 0} failed`;
+
+ document.getElementById('current-address').innerHTML = completionMessage.replace(/\n/g, '
');
+
onProcessingComplete(data);
break;
@@ -351,38 +466,85 @@ function initializeResultsMap() {
}, 100);
}
-function addResultToTable(data, status) {
+function addResultToTable(data, status, confidence = null, warnings = []) {
const tbody = document.getElementById('results-tbody');
const row = document.createElement('tr');
- row.className = status === 'success' ? 'result-success' : 'result-error';
+ // Set row class based on status
if (status === 'success') {
+ row.className = 'result-success';
+ } else if (status === 'warning') {
+ row.className = 'result-warning';
+ } else {
+ row.className = 'result-error';
+ }
+
+ if (status === 'success' || status === 'warning') {
+ // Add confidence indicator for successful geocoding
+ let statusIcon = status === 'warning' ?
+ `
⚠` :
+ `
✓`;
+
+ if (confidence !== null && confidence < 100) {
+ statusIcon += `
(${Math.round(confidence)}%)`;
+ }
+
+ let addressCell = escapeHtml(data.geocoded_address || '');
+ if (warnings && warnings.length > 0) {
+ const warningText = Array.isArray(warnings) ? warnings.join(', ') : warnings;
+ addressCell += `
⚠ ${escapeHtml(warningText)}`;
+ }
+
row.innerHTML = `
-
✓ |
+
${statusIcon} |
${escapeHtml(data.address || data.Address || '')} |
-
${escapeHtml(data.geocoded_address || '')} |
+
${addressCell} |
${data.latitude.toFixed(6)}, ${data.longitude.toFixed(6)} |
+
${data.provider ? escapeHtml(data.provider) : 'N/A'} |
`;
} else {
row.innerHTML = `
✗ |
-
${escapeHtml(data.address || '')} |
-
${escapeHtml(data.error || 'Geocoding failed')} |
+
${escapeHtml(data.address || data.Address || '')} |
+
${escapeHtml(data.geocode_error || data.error || 'Geocoding failed')} |
`;
}
tbody.appendChild(row);
}
-function addMarkerToMap(data) {
+function addMarkerToMap(data, isWarning = false) {
if (!resultsMap || !data.latitude || !data.longitude) return;
- const marker = L.marker([data.latitude, data.longitude])
- .bindPopup(`
-
${escapeHtml(data.geocoded_address || data.address)}
- ${data.latitude.toFixed(6)}, ${data.longitude.toFixed(6)}
- `);
+ // Choose marker color based on status
+ const markerColor = isWarning ? 'orange' : 'green';
+ const marker = L.marker([data.latitude, data.longitude], {
+ icon: L.divIcon({
+ className: `custom-marker ${isWarning ? 'warning-marker' : 'success-marker'}`,
+ html: `
`,
+ iconSize: [16, 16],
+ iconAnchor: [8, 8]
+ })
+ });
+
+ // Create popup content with warning information
+ let popupContent = `
${escapeHtml(data.geocoded_address || data.address)}`;
+ popupContent += `${data.latitude.toFixed(6)}, ${data.longitude.toFixed(6)}`;
+
+ if (data.provider) {
+ popupContent += `
Provider: ${data.provider}`;
+ }
+
+ if (isWarning && data.confidence_score) {
+ popupContent += `
⚠ Confidence: ${Math.round(data.confidence_score)}%`;
+ }
+
+ if (isWarning && data.warnings) {
+ popupContent += `
${escapeHtml(data.warnings)}`;
+ }
+
+ marker.bindPopup(popupContent);
marker.addTo(resultsMap);
markers.push(marker);
@@ -396,11 +558,9 @@ function addMarkerToMap(data) {
function onProcessingComplete(data) {
console.log('Processing complete called with data:', data);
- document.getElementById('current-address').textContent =
- `Complete! Processed ${data.processed || data.success || 0} addresses successfully, ${data.errors || data.failed || 0} errors.`;
-
- const processedCount = data.processed || data.success || processingData.filter(item => item.geocode_success !== false).length;
+ const processedCount = data.processed || data.successful || processingData.filter(item => item.geocode_success !== false).length;
+ // Show comprehensive processing actions with download option
if (processedCount > 0) {
console.log('Showing processing actions for', processedCount, 'successful items');
const actionsDiv = document.getElementById('processing-actions');
@@ -412,17 +572,105 @@ function onProcessingComplete(data) {
}
}
- // Show download report button if we have a session ID
- if (currentSessionId && data.total > 0) {
- showDownloadReportButton(data.total, data.errors || data.failed || 0);
+ // Add download report button if session ID is available
+ if (currentSessionId) {
+ showDownloadReportButton(data);
}
- const errorCount = data.errors || data.failed || 0;
- showDataConvertStatus(`Processing complete: ${processedCount} successful, ${errorCount} errors`,
- errorCount > 0 ? 'warning' : 'success');
+ // Update final message with detailed statistics
+ let finalMessage = `Geocoding Complete!\n\n`;
+ finalMessage += `📊 Summary:\n`;
+ finalMessage += `• Total Processed: ${data.total || 0}\n`;
+ finalMessage += `• ✅ Successful: ${data.successful || 0}\n`;
+
+ if (data.warnings > 0) {
+ finalMessage += `• ⚠️ Warnings: ${data.warnings} (low confidence, review recommended)\n`;
+ }
+
+ if (data.malformed > 0) {
+ finalMessage += `• 🔍 Potentially Malformed: ${data.malformed} (need manual review)\n`;
+ }
+
+ finalMessage += `• ❌ Failed: ${data.errors || data.failed || 0}\n\n`;
+
+ if (data.warnings > 0 || data.malformed > 0) {
+ finalMessage += `⚠️ Note: ${(data.warnings || 0) + (data.malformed || 0)} addresses may need manual verification.\n`;
+ finalMessage += `Please download the detailed report for review.`;
+ } else if ((data.successful || 0) > 0) {
+ finalMessage += `✅ All geocoded addresses appear to have high confidence results!`;
+ }
+
+ showDataConvertStatus(finalMessage, (data.errors || data.failed || 0) > 0 ? 'warning' : 'success');
}
-// Enhanced save function with better feedback
+// Add download report button function
+function showDownloadReportButton(data) {
+ const processingActions = document.getElementById('processing-actions');
+ if (!processingActions) return;
+
+ // Check if download button already exists
+ let downloadBtn = document.getElementById('download-report-btn');
+ if (!downloadBtn) {
+ downloadBtn = document.createElement('button');
+ downloadBtn.id = 'download-report-btn';
+ downloadBtn.className = 'btn btn-info';
+ downloadBtn.innerHTML = '📄 Download Detailed Report';
+ downloadBtn.addEventListener('click', downloadProcessingReport);
+
+ // Insert download button before save results button
+ const saveBtn = document.getElementById('save-results-btn');
+ if (saveBtn && saveBtn.parentNode === processingActions) {
+ processingActions.insertBefore(downloadBtn, saveBtn);
+ } else {
+ processingActions.appendChild(downloadBtn);
+ }
+ }
+
+ // Update button text with statistics
+ const warningsCount = (data.warnings || 0) + (data.malformed || 0);
+ if (warningsCount > 0) {
+ downloadBtn.innerHTML = `📄 Download Report (${warningsCount} need review)`;
+ downloadBtn.className = 'btn btn-warning';
+ }
+}
+
+// Download processing report function
+async function downloadProcessingReport() {
+ if (!currentSessionId) {
+ showDataConvertStatus('No report available to download', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/admin/data-convert/download-report/${currentSessionId}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ // Get the filename from the response headers
+ const filename = response.headers.get('content-disposition')
+ ?.split('filename=')[1]
+ ?.replace(/"/g, '') || `geocoding-report-${currentSessionId}.txt`;
+
+ // Create blob and download
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+
+ showDataConvertStatus('Report downloaded successfully', 'success');
+
+ } catch (error) {
+ console.error('Download report error:', error);
+ showDataConvertStatus('Failed to download report: ' + error.message, 'error');
+ }
+}
async function saveGeocodedResults() {
const successfulData = processingData.filter(item => item.geocode_success !== false && item.latitude && item.longitude);
diff --git a/map/app/routes/geocoding.js b/map/app/routes/geocoding.js
index e67b8d6..731101c 100644
--- a/map/app/routes/geocoding.js
+++ b/map/app/routes/geocoding.js
@@ -160,6 +160,54 @@ router.get('/search', geocodeLimiter, async (req, res) => {
}
});
+/**
+ * Get geocoding provider status
+ * GET /api/geocode/provider-status
+ */
+router.get('/provider-status', (req, res) => {
+ try {
+ const providers = [
+ {
+ name: 'Mapbox',
+ available: !!(process.env.MAPBOX_ACCESS_TOKEN || process.env.MAPBOX_API_KEY),
+ type: 'premium'
+ },
+ {
+ name: 'Nominatim',
+ available: true,
+ type: 'free'
+ },
+ {
+ name: 'Photon',
+ available: true,
+ type: 'free'
+ },
+ {
+ name: 'LocationIQ',
+ available: !!process.env.LOCATIONIQ_API_KEY,
+ type: 'premium'
+ },
+ {
+ name: 'ArcGIS',
+ available: true,
+ type: 'free'
+ }
+ ];
+
+ res.json({
+ success: true,
+ providers: providers,
+ hasPremium: providers.some(p => p.available && p.type === 'premium')
+ });
+ } catch (error) {
+ console.error('Error checking provider status:', error);
+ res.status(500).json({
+ success: false,
+ error: 'Failed to check provider status'
+ });
+ }
+});
+
/**
* Get geocoding cache statistics (admin endpoint)
* GET /api/geocode/cache/stats
diff --git a/map/app/services/geocoding.js b/map/app/services/geocoding.js
index fc2c70a..cfac6a6 100644
--- a/map/app/services/geocoding.js
+++ b/map/app/services/geocoding.js
@@ -30,11 +30,586 @@ setInterval(() => {
}, 60 * 60 * 1000); // Run every hour
/**
- * Reverse geocode coordinates to get address
- * @param {number} lat - Latitude
- * @param {number} lng - Longitude
- * @returns {Promise