// Color scheme for countries const countryColors = { 'South Africa': '#FF6B6B', 'Zimbabwe': '#4ECDC4', 'eSwatini': '#45B7D1', 'Mozambique': '#96CEB4', 'Malawi': '#FFEAA7', 'Zambia': '#DDA15E', 'Uganda': '#EE6C4D', 'Kenya': '#F1A208', 'Tanzania': '#A23B72', 'Rwanda': '#8E7DBE', 'Burundi': '#C9ADA7', 'Mexico': '#006846', }; let map; let mills = []; let millMarkers = {}; let drawnItems = new Map(); let currentMode = 'view'; let currentEditingId = null; let featureGroup; let drawControl; let osmLayer; let satelliteLayer; let currentMapType = 'osm'; let measureControl = null; let filteredMills = []; let currentFilters = { search: '', country: '', minProduction: 0 }; let isMeasuring = false; let measurePoints = []; // Initialize map function initMap() { if (!document.getElementById('map')) { console.error('Map container not found!'); return; } try { if (typeof L === 'undefined') { throw new Error('Leaflet library (L) is not defined!'); } map = L.map('map').setView([-20, 33], 5); } catch (e) { console.error('Error initializing map:', e); return; } // OpenStreetMap layer osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: 'Β© OpenStreetMap contributors', maxZoom: 19 }).addTo(map); // Satellite layer (Esri) satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Β© Esri', maxZoom: 19 }); // Initialize drawing featureGroup = L.featureGroup().addTo(map); drawControl = new L.Control.Draw({ edit: { featureGroup: featureGroup, poly: { allowIntersection: false } }, draw: { polygon: false, polyline: false, rectangle: false, circle: false, circlemarker: false, marker: true } }); map.addControl(drawControl); // Hide draw controls initially (show only in draw mode) const drawToolbar = document.querySelector('.leaflet-draw-toolbar'); if (drawToolbar) { drawToolbar.style.display = 'none'; } // Handle drawn items map.on(L.Draw.Event.CREATED, function(e) { const layer = e.layer; featureGroup.addLayer(layer); if (layer instanceof L.Marker) { // Auto-switch to draw mode if not already if (currentMode !== 'draw') { document.querySelector('[data-mode="draw"]').click(); } const id = Date.now(); const latlng = layer.getLatLng(); // Round coordinates to 4 decimal places (~11m accuracy) const roundedLat = Math.round(latlng.lat * 10000) / 10000; const roundedLng = Math.round(latlng.lng * 10000) / 10000; drawnItems.set(id, { type: 'marker', lat: roundedLat, lng: roundedLng, data: {} }); layer.drawId = id; updateDrawnItemsList(); showEditForm(id); } }); map.on(L.Draw.Event.DELETED, function(e) { e.layers.eachLayer(function(layer) { drawnItems.delete(layer.drawId); }); updateDrawnItemsList(); }); // Satellite toggle document.getElementById('mapToggleBtn').addEventListener('click', () => { const btn = document.getElementById('mapToggleBtn'); if (currentMapType === 'osm') { map.removeLayer(osmLayer); map.addLayer(satelliteLayer); currentMapType = 'satellite'; btn.textContent = 'πŸ—ΊοΈ Map'; } else { map.removeLayer(satelliteLayer); map.addLayer(osmLayer); currentMapType = 'osm'; btn.textContent = 'πŸ›°οΈ Satellite'; } }); // Measurement tool toggle document.getElementById('measureBtn').addEventListener('click', () => { const btn = document.getElementById('measureBtn'); const panel = document.getElementById('measurementPanel'); isMeasuring = !isMeasuring; if (isMeasuring) { measurePoints = []; btn.textContent = 'πŸ“ Click points to measure'; btn.style.background = 'rgba(255,255,255,0.3)'; btn.style.fontWeight = '600'; panel.style.display = 'flex'; panel.style.flex = '1'; updateMeasurementPanel(); } else { btn.textContent = 'πŸ“ Measure'; btn.style.background = 'rgba(255,255,255,0.1)'; btn.style.fontWeight = '400'; panel.style.display = 'none'; panel.style.flex = ''; // Clear measurement points from map map.eachLayer(layer => { if (layer.options && layer.options.className === 'measurement-point') { map.removeLayer(layer); } }); } }); // Reset measurement button document.getElementById('resetMeasurementBtn').addEventListener('click', () => { measurePoints = []; // Clear measurement points from map map.eachLayer(layer => { if (layer.options && layer.options.className === 'measurement-point') { map.removeLayer(layer); } }); updateMeasurementPanel(); }); // Add map click listener for measurement map.on('click', function(e) { if (!isMeasuring) return; const point = e.latlng; measurePoints.push(point); // Add marker L.circleMarker(point, { radius: 5, fillColor: '#2596be', color: 'white', weight: 2, opacity: 1, fillOpacity: 0.8, className: 'measurement-point' }).addTo(map); // If we have 2 or more points, show distance if (measurePoints.length >= 2) { const lastPoint = measurePoints[measurePoints.length - 2]; const currentPoint = measurePoints[measurePoints.length - 1]; const distance = lastPoint.distanceTo(currentPoint); // Add polyline between points L.polyline([lastPoint, currentPoint], { color: '#2596be', weight: 2, opacity: 0.7, dashArray: '5, 5', className: 'measurement-point' }).addTo(map); } // Update the measurement panel updateMeasurementPanel(); }); // Load CSV data loadMillsData(); // Initialize Google Sheets auto-refresh initGoogleSheetsAutoRefresh(); showGoogleSheetsSetup(); // Attach mode button listeners attachModeListeners(); // Attach filter listeners attachFilterListeners(); } // Attach filter event listeners function attachFilterListeners() { document.getElementById('searchInput').addEventListener('input', applyFilters); document.getElementById('countryFilter').addEventListener('change', applyFilters); document.getElementById('minProduction').addEventListener('change', applyFilters); document.getElementById('resetFiltersBtn').addEventListener('click', resetFilters); } // Update measurement panel with current points and distances function updateMeasurementPanel() { const listDiv = document.getElementById('measurementList'); const totalDiv = document.getElementById('totalDistance'); if (measurePoints.length === 0) { listDiv.innerHTML = '

Click on the map to add measurement points

'; totalDiv.textContent = '0.00 km'; return; } let html = ''; let totalDistance = 0; // Show each point measurePoints.forEach((point, index) => { let segmentDistance = ''; if (index > 0) { const prevPoint = measurePoints[index - 1]; const dist = prevPoint.distanceTo(point); const distKm = (dist / 1000).toFixed(2); segmentDistance = ` - Segment: ${distKm} km`; totalDistance += dist; } html += `
Point ${index + 1} (${point.lat.toFixed(4)}, ${point.lng.toFixed(4)})${segmentDistance}
`; }); listDiv.innerHTML = html; totalDiv.textContent = (totalDistance / 1000).toFixed(2) + ' km'; } // Load mills from CSV async function loadMillsData() { try { // Try to load from Google Sheets first let csvText = await fetchGoogleSheetData(); // Fallback to local CSV if Google Sheets fails if (!csvText) { console.log('Falling back to local CSV file...'); const response = await fetch('sugar_cane_factories_africa.csv'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } csvText = await response.text(); showNotification('Using local data (Google Sheet unavailable)', 'warning'); } else { showNotification('βœ“ Data loaded from Google Sheet', 'success'); } parseCSV(csvText); console.log('Mills loaded:', mills.length, mills.slice(0, 2)); renderMills(); console.log('Mills rendered'); updateLegend(); console.log('Legend updated'); } catch (error) { console.error('Error loading mills data:', error); const notification = document.createElement('div'); notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: #ff6b6b; color: white; padding: 15px 20px; border-radius: 5px; z-index: 9999;'; notification.textContent = '⚠️ Could not load mill data. Check console for details.'; document.body.appendChild(notification); setTimeout(() => notification.remove(), 5000); } } // Parse CSV function parseCSV(csvText) { const lines = csvText.trim().split('\n'); const headers = lines[0].split(',').map(h => h.trim()); mills = lines.slice(1).map((line, index) => { const values = parseCSVLine(line); const row = {}; headers.forEach((header, idx) => { row[header] = values[idx] ? values[idx].trim() : ''; }); row._id = index; // Internal unique ID return row; }).filter(row => row.Latitude && row.Longitude); } // Parse CSV line handling quoted values function parseCSVLine(line) { const result = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; const nextChar = line[i + 1]; if (char === '"') { if (inQuotes && nextChar === '"') { current += '"'; i++; } else { inQuotes = !inQuotes; } } else if (char === ',' && !inQuotes) { result.push(current); current = ''; } else { current += char; } } result.push(current); return result; } // Render mills on map function renderMills() { Object.values(millMarkers).forEach(marker => { if (map.hasLayer(marker)) map.removeLayer(marker); }); millMarkers = {}; mills.forEach(mill => { const lat = parseFloat(mill.Latitude); const lng = parseFloat(mill.Longitude); if (!isNaN(lat) && !isNaN(lng)) { const production = cleanNumber(mill['Annual Sugar Production (tons)']); const size = getCircleSize(production); const color = countryColors[mill.Country] || '#999'; const marker = L.circleMarker([lat, lng], { radius: size, fillColor: color, color: 'white', weight: 2, opacity: 0.8, fillOpacity: 0.7 }).addTo(map); const popup = createPopup(mill); marker.bindPopup(popup); millMarkers[mill._id] = marker; } }); // Initialize filtered mills array filteredMills = [...mills]; } // Get circle size based on production function getCircleSize(production) { if (production > 150000) return 15; if (production > 50000) return 10; return 6; } // Create popup content function createPopup(mill) { const production = mill['Annual Sugar Production (tons)'] || 'N/A'; const capacity = mill['Crushing Capacity (tons/year)'] || 'N/A'; const year = mill['Data Year'] || ''; return `
${mill['Mill/Factory']}
${mill['Country']} β€’ ${mill['Province/Region']}
${mill.Notes ? `` : ''}
Company: ${mill.Company || 'N/A'}
Production: ${formatNumber(production)} tons/year
Capacity: ${formatNumber(capacity)} tons/year
Coordinates: ${parseFloat(mill.Latitude).toFixed(4)}, ${parseFloat(mill.Longitude).toFixed(4)}
Data Year: ${year}
"${mill.Notes}"
πŸ“ Edit on Google Sheets
`; } // Format numbers function formatNumber(num) { if (!num || num === 'N/A') return 'N/A'; const n = parseFloat(num); if (isNaN(n)) return 'N/A'; return n.toLocaleString('en-US', { maximumFractionDigits: 0 }); } // Update legend and populate country filter function updateLegend() { const countries = [...new Set(mills.map(m => m.Country))].sort(); const legendHTML = countries.map(country => { const color = countryColors[country] || '#999'; const count = mills.filter(m => m.Country === country).length; return `
${country} (${count})
`; }).join(''); document.getElementById('legendContainer').innerHTML = legendHTML; // Update country filter dropdown const countryFilter = document.getElementById('countryFilter'); const currentValue = countryFilter.value; countryFilter.innerHTML = '' + countries.map(c => ``).join(''); countryFilter.value = currentValue; } // Helper to clean numbers (remove commas and spaces) function cleanNumber(val) { if (!val) return 0; const cleanStr = String(val).replace(/[, ]/g, ''); return parseFloat(cleanStr) || 0; } // Apply filters to mills function applyFilters() { const search = document.getElementById('searchInput').value.toLowerCase(); const country = document.getElementById('countryFilter').value; const minProduction = parseFloat(document.getElementById('minProduction').value) || 0; console.log('Applying filters:', { search, country, minProduction, totalMills: mills.length }); currentFilters = { search, country, minProduction }; // Filter mills filteredMills = mills.filter(mill => { const matchesSearch = !search || mill['Mill/Factory'].toLowerCase().includes(search) || mill['Company'].toLowerCase().includes(search); const matchesCountry = !country || mill.Country === country; const production = cleanNumber(mill['Annual Sugar Production (tons)']); const matchesProduction = production >= minProduction; return matchesSearch && matchesCountry && matchesProduction; }); console.log('Filtered mills:', filteredMills.length); // Update map display updateMillsVisibility(); // Update legend to show only filtered countries updateLegendFiltered(); // Zoom to filtered results if any exist if (filteredMills.length > 0) { zoomToFilteredBounds(); } } // Update visibility of mill markers based on filters function updateMillsVisibility() { mills.forEach(mill => { const marker = millMarkers[mill._id]; if (marker) { const isVisible = filteredMills.includes(mill); if (isVisible) { if (!map.hasLayer(marker)) { marker.addTo(map); } marker.setStyle({ opacity: 0.8, fillOpacity: 0.7 }); } else { if (map.hasLayer(marker)) { map.removeLayer(marker); } } } }); } // Zoom map to encapsulate all filtered results function zoomToFilteredBounds() { if (filteredMills.length === 0) return; const bounds = L.latLngBounds(); let hasValidBounds = false; filteredMills.forEach(mill => { const lat = parseFloat(mill.Latitude); const lng = parseFloat(mill.Longitude); if (!isNaN(lat) && !isNaN(lng)) { bounds.extend([lat, lng]); hasValidBounds = true; } }); if (hasValidBounds) { map.fitBounds(bounds, { padding: [50, 50], maxZoom: 10 }); } } // Update legend to show only countries in filtered results function updateLegendFiltered() { const filteredCountries = [...new Set(filteredMills.map(m => m.Country))].sort(); const legendHTML = filteredCountries.map(country => { const color = countryColors[country] || '#999'; const count = filteredMills.filter(m => m.Country === country).length; return `
${country} (${count})
`; }).join(''); document.getElementById('legendContainer').innerHTML = legendHTML || '

No mills match current filters

'; } // Reset all filters function resetFilters() { document.getElementById('searchInput').value = ''; document.getElementById('countryFilter').value = ''; document.getElementById('minProduction').value = ''; filteredMills = [...mills]; updateMillsVisibility(); updateLegend(); // Show all countries again // Reset map to initial view map.setView([-20, 33], 5); } // Mode switching function attachModeListeners() { const buttons = document.querySelectorAll('.mode-btn'); console.log(`Attaching listeners to ${buttons.length} mode buttons`); buttons.forEach(btn => { btn.addEventListener('click', (e) => { console.log('Mode button clicked:', e.target.dataset.mode); document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); currentMode = e.target.dataset.mode; console.log('Current mode:', currentMode); document.getElementById('viewModePanel').style.display = currentMode === 'view' ? 'block' : 'none'; document.getElementById('drawModePanel').style.display = currentMode === 'draw' ? 'block' : 'none'; // Show/hide draw toolbar based on mode const drawToolbar = document.querySelector('.leaflet-draw-toolbar'); if (drawToolbar) { drawToolbar.style.display = currentMode === 'draw' ? 'block' : 'none'; } updateDrawnItemsList(); }); }); } // Update drawn items list function updateDrawnItemsList() { const list = document.getElementById('drawnItemsList'); if (drawnItems.size === 0) { list.innerHTML = '

No items drawn yet

'; document.getElementById('showFormBtn').classList.remove('visible'); document.getElementById('exportBtn').classList.remove('visible'); document.getElementById('clearDrawnBtn').classList.remove('visible'); } else { const items = Array.from(drawnItems.entries()).map(([id, item]) => `
${item.data['Mill/Factory'] || `Point ${id.toString().slice(-4)}`}
`).join(''); list.innerHTML = items; document.getElementById('showFormBtn').classList.add('visible'); document.getElementById('exportBtn').classList.add('visible'); document.getElementById('clearDrawnBtn').classList.add('visible'); } } // Remove drawn item function removeDrawnItem(id) { drawnItems.delete(id); featureGroup.eachLayer(layer => { if (layer.drawId === id) { featureGroup.removeLayer(layer); } }); updateDrawnItemsList(); } // Show edit form function showEditForm(id) { currentEditingId = id; const item = drawnItems.get(id); document.getElementById('editMill').value = item.data['Mill/Factory'] || ''; document.getElementById('editCountry').value = item.data['Country'] || ''; document.getElementById('editCompany').value = item.data['Company'] || ''; document.getElementById('editProvince').value = item.data['Province/Region'] || ''; document.getElementById('editLat').value = item.lat.toFixed(4); document.getElementById('editLng').value = item.lng.toFixed(4); document.getElementById('editCapacity').value = item.data['Crushing Capacity (tons/year)'] || ''; document.getElementById('editProduction').value = item.data['Annual Sugar Production (tons)'] || ''; document.getElementById('editNotes').value = item.data['Notes'] || ''; document.getElementById('editYear').value = item.data['Data Year'] || new Date().getFullYear(); document.getElementById('editAnnotations').value = item.data['Annotations'] || ''; document.getElementById('editModal').classList.add('active'); } // Handle form submission document.getElementById('editForm').addEventListener('submit', (e) => { e.preventDefault(); const item = drawnItems.get(currentEditingId); item.data = { 'Country': document.getElementById('editCountry').value, 'Mill/Factory': document.getElementById('editMill').value, 'Company': document.getElementById('editCompany').value, 'Province/Region': document.getElementById('editProvince').value, 'Latitude': parseFloat(document.getElementById('editLat').value), 'Longitude': parseFloat(document.getElementById('editLng').value), 'Crushing Capacity (tons/year)': document.getElementById('editCapacity').value, 'Annual Sugar Production (tons)': document.getElementById('editProduction').value, 'Notes': document.getElementById('editNotes').value, 'Data Year': document.getElementById('editYear').value, 'Annotations': document.getElementById('editAnnotations').value }; drawnItems.set(currentEditingId, item); updateDrawnItemsList(); closeModal('editModal'); }); // Modal controls function closeModal(modalId) { document.getElementById(modalId).classList.remove('active'); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.modal').forEach(m => m.classList.remove('active')); } }); // Export to CSV - Option 1: Download locally function exportToCSV() { if (drawnItems.size === 0) { alert('No items to export'); return; } // Header row const headers = ['Country', 'Mill/Factory', 'Company', 'Province/Region', 'Latitude', 'Longitude', 'Crushing Capacity (tons/year)', 'Annual Sugar Production (tons)', 'Notes', 'Data Year', 'Annotations']; const rows = [headers]; // Data rows drawnItems.forEach(item => { const row = [ item.data['Country'] || '', item.data['Mill/Factory'] || '', item.data['Company'] || '', item.data['Province/Region'] || '', item.lat, item.lng, item.data['Crushing Capacity (tons/year)'] || '', item.data['Annual Sugar Production (tons)'] || '', `"${(item.data['Notes'] || '').replace(/"/g, '""')}"`, item.data['Data Year'] || '', `"${(item.data['Annotations'] || '').replace(/"/g, '""')}"` ]; rows.push(row); }); // Convert to CSV const csv = rows.map(row => row.map(cell => { if (typeof cell === 'string' && (cell.includes(',') || cell.includes('"') || cell.includes('\n'))) { return `"${cell.replace(/"/g, '""')}"`; } return cell; }).join(',') ).join('\n'); return csv; } // Export button - show options document.getElementById('exportBtn').addEventListener('click', () => { if (drawnItems.size === 0) { alert('No items to export'); return; } showExportModal(); }); // Export modal function showExportModal() { const modal = document.createElement('div'); modal.className = 'modal active'; modal.id = 'exportModal'; modal.innerHTML = ` `; document.body.appendChild(modal); } // Submit via Formspree function submitViaFormspree() { const csv = exportToCSV(); // Build form data const formData = new FormData(); formData.append('submissionType', 'New Mill Submissions'); formData.append('millCount', drawnItems.size); formData.append('csvData', csv); // Add individual mill details to the message let millDetails = 'New Mills Submitted:\n\n'; let idx = 1; drawnItems.forEach((item) => { millDetails += `${idx}. ${item.data['Mill/Factory'] || 'Unnamed'}\n`; millDetails += ` Country: ${item.data['Country'] || 'N/A'}\n`; millDetails += ` Company: ${item.data['Company'] || 'N/A'}\n`; millDetails += ` Province/Region: ${item.data['Province/Region'] || 'N/A'}\n`; millDetails += ` Location: ${item.lat.toFixed(4)}, ${item.lng.toFixed(4)}\n`; millDetails += ` Production: ${item.data['Annual Sugar Production (tons)'] || 'N/A'} tons/year\n`; millDetails += ` Capacity: ${item.data['Crushing Capacity (tons/year)'] || 'N/A'} tons/year\n`; millDetails += ` Notes: ${item.data['Notes'] || 'None'}\n`; millDetails += ` Annotations: ${item.data['Annotations'] || 'None'}\n\n`; idx++; }); formData.append('message', millDetails); // Submit to Formspree fetch('https://formspree.io/f/xgvgybwl', { method: 'POST', body: formData, headers: { 'Accept': 'application/json' } }) .then(response => { if (response.ok) { alert(`βœ… Successfully submitted ${drawnItems.size} new mill(s)! Thank you for the contribution.`); drawnItems.clear(); featureGroup.clearLayers(); updateDrawnItemsList(); closeModal('exportModal'); } else { alert('❌ Error submitting mills. Please try again.'); } }) .catch(error => { console.error('Error:', error); alert('❌ Error submitting mills. Please check your internet connection.'); }); } // Download CSV function downloadCSV() { const csv = exportToCSV(); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `sugar_mills_additions_${new Date().toISOString().split('T')[0]}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); closeModal('exportModal'); } // Email CSV via mailto function emailCSV() { const csv = exportToCSV(); const filename = `sugar_mills_additions_${new Date().toISOString().split('T')[0]}.csv`; // Create summary for email body let summary = 'New Sugar Cane Mill Locations\n\n'; summary += `Attached: ${filename}\n\n`; summary += 'Summary of new mills:\n\n'; drawnItems.forEach((item, idx) => { summary += `${idx + 1}. ${item.data['Mill/Factory'] || 'Unnamed'} (${item.data['Country'] || 'Unknown'})\n`; summary += ` Company: ${item.data['Company'] || 'N/A'}\n`; summary += ` Location: ${item.lat.toFixed(4)}, ${item.lng.toFixed(4)}\n`; summary += ` Production: ${item.data['Annual Sugar Production (tons)'] || 'N/A'} tons/year\n\n`; }); // First, download the CSV file const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Then open email client with instructions const subject = encodeURIComponent(`New Sugar Cane Mills Data - ${new Date().toISOString().split('T')[0]}`); const body = encodeURIComponent(summary); const mailto = `mailto:timon@resiliencebv.com?subject=${subject}&body=${body}`; // Show info that file was downloaded setTimeout(() => { alert(`βœ… CSV file downloaded: ${filename}\n\nPlease attach it to the email that will now open, then send it.`); window.open(mailto); }, 500); closeModal('exportModal'); } // Show form button document.getElementById('showFormBtn').addEventListener('click', () => { if (drawnItems.size > 0) { const firstId = drawnItems.keys().next().value; showEditForm(firstId); } }); // Clear all document.getElementById('clearDrawnBtn').addEventListener('click', () => { if (confirm('Clear all drawn items?')) { drawnItems.clear(); featureGroup.clearLayers(); updateDrawnItemsList(); } }); // Initialize on load window.addEventListener('load', function() { initMap(); }); // Also try immediate initialization if document is already loaded if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(function() { initMap(); }, 500); }