SmartCane/webapps/sugar_mill_locator/app.js

912 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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'
};
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 = '<p style="color: #999;">Click on the map to add measurement points</p>';
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 += `<div style="padding: 8px; background: #f9f9f9; margin-bottom: 6px; border-radius: 3px; border-left: 3px solid #2596be;">
<strong>Point ${index + 1}</strong> (${point.lat.toFixed(4)}, ${point.lng.toFixed(4)})${segmentDistance}
</div>`;
});
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 => {
const values = parseCSVLine(line);
const row = {};
headers.forEach((header, idx) => {
row[header] = values[idx] ? values[idx].trim() : '';
});
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() {
mills.forEach(mill => {
const lat = parseFloat(mill.Latitude);
const lng = parseFloat(mill.Longitude);
if (!isNaN(lat) && !isNaN(lng)) {
const production = parseFloat(mill['Annual Sugar Production (tons)']) || 0;
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[`${lat},${lng}`] = 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 `
<div style="font-size: 13px; width: 280px;">
<strong style="font-size: 14px; color: #333;">${mill['Mill/Factory']}</strong><br>
<small style="color: #666;">${mill['Country']}${mill['Province/Region']}</small>
<hr style="margin: 8px 0; border: none; border-top: 1px solid #eee;">
<table style="width: 100%; font-size: 12px;">
<tr>
<td style="font-weight: 600; color: #333;">Company:</td>
<td>${mill.Company || 'N/A'}</td>
</tr>
<tr>
<td style="font-weight: 600; color: #333;">Production:</td>
<td>${formatNumber(production)} tons/year</td>
</tr>
<tr>
<td style="font-weight: 600; color: #333;">Capacity:</td>
<td>${formatNumber(capacity)} tons/year</td>
</tr>
<tr>
<td style="font-weight: 600; color: #333;">Coordinates:</td>
<td>${parseFloat(mill.Latitude).toFixed(4)}, ${parseFloat(mill.Longitude).toFixed(4)}</td>
</tr>
<tr>
<td style="font-weight: 600; color: #333;">Data Year:</td>
<td>${year}</td>
</tr>
${mill.Notes ? `<tr><td colspan="2" style="padding-top: 8px; color: #555; font-style: italic;">"${mill.Notes}"</td></tr>` : ''}
</table>
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #eee;">
<button onclick="showFeedbackModal('${mill['Mill/Factory']}', '${mill['Country']}', '${parseFloat(mill.Latitude).toFixed(4)}', '${parseFloat(mill.Longitude).toFixed(4)}')" style="width: 100%; padding: 8px; background: #2596be; color: white; border: none; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer;">💬 Send Feedback</button>
</div>
</div>
`;
}
// 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 `
<div class="country-item">
<div class="country-color" style="background: ${color};"></div>
<span>${country} (${count})</span>
</div>
`;
}).join('');
document.getElementById('legendContainer').innerHTML = legendHTML;
// Update country filter dropdown
const countryFilter = document.getElementById('countryFilter');
const currentValue = countryFilter.value;
countryFilter.innerHTML = '<option value="">All Countries</option>' +
countries.map(c => `<option value="${c}">${c}</option>`).join('');
countryFilter.value = currentValue;
}
// 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 = parseFloat(mill['Annual Sugar Production (tons)']) || 0;
const matchesProduction = production >= minProduction;
return matchesSearch && matchesCountry && matchesProduction;
});
console.log('Filtered mills:', filteredMills.length);
// Update map display
updateMillsVisibility();
}
// Update visibility of mill markers based on filters
function updateMillsVisibility() {
mills.forEach((mill, index) => {
const key = `${parseFloat(mill.Latitude)},${parseFloat(mill.Longitude)}`;
const marker = millMarkers[key];
if (marker) {
const isVisible = filteredMills.includes(mill);
marker.setOpacity(isVisible ? 1 : 0.3);
marker.setRadius(isVisible ? getCircleSize(parseFloat(mill['Annual Sugar Production (tons)']) || 0) : getCircleSize(parseFloat(mill['Annual Sugar Production (tons)']) || 0));
}
});
}
// Reset all filters
function resetFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('countryFilter').value = '';
document.getElementById('minProduction').value = '';
filteredMills = [...mills];
updateMillsVisibility();
}
// 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 = '<p style="color: #999; font-size: 12px;">No items drawn yet</p>';
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]) => `
<div class="drawn-item">
<span>${item.data['Mill/Factory'] || `Point ${id.toString().slice(-4)}`}</span>
<button class="drawn-item-btn" onclick="removeDrawnItem(${id})">×</button>
</div>
`).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');
}
// Show feedback modal for existing mill
function showFeedbackModal(mill, country, lat, lng) {
document.getElementById('feedbackMill').value = mill;
document.getElementById('feedbackCountry').value = country;
document.getElementById('feedbackLocation').value = `${lat}, ${lng}`;
document.getElementById('feedbackType').value = '';
document.getElementById('feedbackMessage').value = '';
document.getElementById('feedbackName').value = '';
document.getElementById('feedbackEmail').value = '';
const feedbackModal = document.getElementById('feedbackModal');
feedbackModal.classList.add('active');
}
// Handle feedback form submission via Formspree
document.addEventListener('DOMContentLoaded', () => {
const feedbackForm = document.getElementById('feedbackForm');
if (feedbackForm) {
feedbackForm.addEventListener('submit', function(e) {
e.preventDefault();
// Fetch will handle the form submission to Formspree
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json'
}
})
.then(response => {
if (response.ok) {
// Show success message
alert('✅ Feedback sent successfully! Thank you for your input.');
closeModal('feedbackModal');
feedbackForm.reset();
} else {
alert('❌ Error sending feedback. Please try again.');
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ Error sending feedback. Please check your internet connection.');
});
});
}
});
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 = `
<div class="modal-content">
<div class="modal-header">
<h2>Send to Timon</h2>
<button class="close-btn" onclick="closeModal('exportModal')">&times;</button>
</div>
<div class="alert alert-info">
You have ${drawnItems.size} new mill location(s) to submit. Choose how to send them:
</div>
<div style="margin-bottom: 20px;">
<h3 style="font-size: 16px; color: #333; margin-bottom: 15px;">📤 Submit Online</h3>
<p style="color: #666; font-size: 14px; margin-bottom: 10px;">Send all new mills directly to the database. Your data will be reviewed and added.</p>
<button class="btn-primary" style="width: 100%;" onclick="submitViaFormspree()">Submit New Mills</button>
</div>
<div style="border-top: 1px solid #ddd; padding-top: 20px;">
<h3 style="font-size: 16px; color: #333; margin-bottom: 15px;">⬇️ Download as CSV</h3>
<p style="color: #666; font-size: 14px; margin-bottom: 10px;">Download the data as a CSV file for your records or manual submission.</p>
<button class="btn-secondary" style="width: 100%;" onclick="downloadCSV()">Download CSV File</button>
</div>
<div style="margin-top: 20px; text-align: center;">
<button class="btn-secondary" onclick="closeModal('exportModal')">Cancel</button>
</div>
</div>
`;
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);
}