Large update to geo-coding functions in order to support better matching of street addresses. Added premium mapbox option
This commit is contained in:
@@ -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 = '<div class="provider-list">';
|
||||
|
||||
providers.forEach(provider => {
|
||||
const icon = provider.available ? '✅' : '❌';
|
||||
const status = provider.available ? 'Available' : 'Not configured';
|
||||
const className = provider.available ? 'provider-available' : 'provider-unavailable';
|
||||
|
||||
statusHTML += `
|
||||
<div class="provider-item ${className}">
|
||||
<span class="provider-icon">${icon}</span>
|
||||
<strong>${provider.name}</strong>: ${status}
|
||||
${provider.name === 'Mapbox' && provider.available ?
|
||||
'<span class="provider-premium">🌟 Premium</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
statusHTML += '</div>';
|
||||
|
||||
// Add configuration note if no premium providers
|
||||
const hasPremium = providers.some(p => p.available && ['Mapbox', 'LocationIQ'].includes(p.name));
|
||||
if (!hasPremium) {
|
||||
statusHTML += `
|
||||
<div class="provider-note">
|
||||
<p><strong>💡 Tip:</strong> For better geocoding accuracy, configure a premium provider:</p>
|
||||
<ul>
|
||||
<li><strong>Mapbox</strong>: Add <code>MAPBOX_ACCESS_TOKEN</code> to your .env file</li>
|
||||
<li><strong>LocationIQ</strong>: Add <code>LOCATIONIQ_API_KEY</code> to your .env file</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
} else if (providers.find(p => p.name === 'Mapbox' && p.available)) {
|
||||
statusHTML += `
|
||||
<div class="provider-note" style="background: #d1ecf1; border-color: #bee5eb;">
|
||||
<p><strong>🌟 Mapbox Configured!</strong> Using premium geocoding for better accuracy.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
statusElement.innerHTML = statusHTML;
|
||||
} else {
|
||||
statusElement.innerHTML = '<span class="error">❌ Failed to check provider status</span>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check geocoding providers:', error);
|
||||
statusElement.innerHTML = `
|
||||
<div class="provider-error">
|
||||
<span class="error">⚠️ Unable to check provider status</span>
|
||||
<small>Using fallback providers: Nominatim, Photon, ArcGIS</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = `<span style="color: red;">✗ ${data.currentAddress || data.address}</span>`;
|
||||
} else if (data.status === 'processing') {
|
||||
document.getElementById('current-address').innerHTML = `<span style="color: blue;">⟳ Processing: ${data.currentAddress || data.address}</span>`;
|
||||
} else {
|
||||
document.getElementById('current-address').innerHTML = `<span style="color: green;">✓ ${data.currentAddress || data.address}</span>`;
|
||||
}
|
||||
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, '<br>');
|
||||
|
||||
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' ?
|
||||
`<span class="status-icon warning" title="Low confidence result">⚠</span>` :
|
||||
`<span class="status-icon success">✓</span>`;
|
||||
|
||||
if (confidence !== null && confidence < 100) {
|
||||
statusIcon += ` <small>(${Math.round(confidence)}%)</small>`;
|
||||
}
|
||||
|
||||
let addressCell = escapeHtml(data.geocoded_address || '');
|
||||
if (warnings && warnings.length > 0) {
|
||||
const warningText = Array.isArray(warnings) ? warnings.join(', ') : warnings;
|
||||
addressCell += `<br><small style="color: orange;" title="${escapeHtml(warningText)}">⚠ ${escapeHtml(warningText)}</small>`;
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<td><span class="status-icon success">✓</span></td>
|
||||
<td>${statusIcon}</td>
|
||||
<td>${escapeHtml(data.address || data.Address || '')}</td>
|
||||
<td>${escapeHtml(data.geocoded_address || '')}</td>
|
||||
<td>${addressCell}</td>
|
||||
<td>${data.latitude.toFixed(6)}, ${data.longitude.toFixed(6)}</td>
|
||||
<td>${data.provider ? escapeHtml(data.provider) : 'N/A'}</td>
|
||||
`;
|
||||
} else {
|
||||
row.innerHTML = `
|
||||
<td><span class="status-icon error">✗</span></td>
|
||||
<td>${escapeHtml(data.address || '')}</td>
|
||||
<td colspan="2" class="error-message">${escapeHtml(data.error || 'Geocoding failed')}</td>
|
||||
<td>${escapeHtml(data.address || data.Address || '')}</td>
|
||||
<td colspan="3" class="error-message">${escapeHtml(data.geocode_error || data.error || 'Geocoding failed')}</td>
|
||||
`;
|
||||
}
|
||||
|
||||
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(`
|
||||
<strong>${escapeHtml(data.geocoded_address || data.address)}</strong><br>
|
||||
${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: `<div style="background-color: ${markerColor}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 1px 3px rgba(0,0,0,0.3);"></div>`,
|
||||
iconSize: [16, 16],
|
||||
iconAnchor: [8, 8]
|
||||
})
|
||||
});
|
||||
|
||||
// Create popup content with warning information
|
||||
let popupContent = `<strong>${escapeHtml(data.geocoded_address || data.address)}</strong><br>`;
|
||||
popupContent += `${data.latitude.toFixed(6)}, ${data.longitude.toFixed(6)}`;
|
||||
|
||||
if (data.provider) {
|
||||
popupContent += `<br><small>Provider: ${data.provider}</small>`;
|
||||
}
|
||||
|
||||
if (isWarning && data.confidence_score) {
|
||||
popupContent += `<br><span style="color: orange;">⚠ Confidence: ${Math.round(data.confidence_score)}%</span>`;
|
||||
}
|
||||
|
||||
if (isWarning && data.warnings) {
|
||||
popupContent += `<br><small style="color: orange;">${escapeHtml(data.warnings)}</small>`;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user