Add mill layer option to GeoJSON viewer

This commit is contained in:
Nik Verweel 2026-02-04 13:40:06 +01:00
parent 7c55b38954
commit 4b1a51d0e9
3 changed files with 346 additions and 3 deletions

View file

@ -0,0 +1,135 @@
// Google Sheets Configuration for GeoJSON Viewer
// This file connects to the Google Sheet for live mills data
const GOOGLE_SHEETS_CONFIG = {
// Your Google Sheet ID (from the URL)
SHEET_ID: '1ZHEIyhupNDHVd1EScBn0DnuiAzMFoZcAPZm3U65abkY',
// The sheet name or gid (the sheet tab you want to read from)
SHEET_NAME: 'Sheet1', // Change this to your actual sheet name if different
// Auto-refresh interval in milliseconds (5 minutes = 300000ms)
REFRESH_INTERVAL: 300000,
// Enable auto-refresh (set to false to disable)
AUTO_REFRESH_ENABLED: true
};
/**
* Fetch data from Google Sheet via Netlify Function
* The function keeps credentials secret on the server
*/
async function fetchGoogleSheetData() {
try {
// Call Netlify function instead of Google Sheets directly
// This keeps the Sheet ID and password hidden from browser dev tools
const response = await fetch('/.netlify/functions/get-mills');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const csvText = await response.text();
console.log('✓ Data fetched from Netlify Function (Google Sheet)');
return csvText;
} catch (error) {
console.error('Error fetching data from Netlify Function:', error);
return null;
}
}
/**
* Initialize auto-refresh of data from Google Sheet
*/
function initGoogleSheetsAutoRefresh() {
if (!GOOGLE_SHEETS_CONFIG.AUTO_REFRESH_ENABLED) {
console.log('Google Sheets auto-refresh is disabled');
return;
}
console.log(`✓ Auto-refresh enabled (every ${GOOGLE_SHEETS_CONFIG.REFRESH_INTERVAL / 1000 / 60} minutes)`);
// Refresh periodically
setInterval(async () => {
console.log('🔄 Refreshing mills data from Google Sheet...');
const csvData = await fetchGoogleSheetData();
if (csvData && millsToggle && millsToggle.checked) {
// Parse new data
mills = parseMillsCSV(csvData);
// Render updated mills if layer is visible
if (millsLayer && map.hasLayer(millsLayer)) {
renderMills();
}
console.log(`✓ Updated ${mills.length} mills from Google Sheet`);
}
}, GOOGLE_SHEETS_CONFIG.REFRESH_INTERVAL);
}
/**
* Show notification to user
*/
function showNotification(message, type = 'info') {
const colors = {
'success': '#4CAF50',
'warning': '#FF9800',
'error': '#F44336',
'info': '#2196F3'
};
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${colors[type] || colors.info};
color: white;
padding: 15px 20px;
border-radius: 5px;
z-index: 9999;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
font-weight: 500;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.transition = 'opacity 0.3s ease';
notification.style.opacity = '0';
setTimeout(() => notification.remove(), 300);
}, 4000);
}
/**
* Provide setup instructions to the user
*/
function showGoogleSheetsSetup() {
console.log(`
Google Sheets Integration for GeoJSON Viewer Mills
1. Sugar mills data is configured and ready!
2. The map will automatically update every 5 minutes
with new data from the Google Sheet
3. To change refresh interval, edit:
GOOGLE_SHEETS_CONFIG.REFRESH_INTERVAL
4. Column headers required (case-sensitive):
- Mill/Factory
- Country
- Latitude
- Longitude
- Crushing Capacity (tons/year)
- Annual Sugar Production (tons)
- Province/Region
- Company
- Data Year
`);
}

View file

@ -384,9 +384,9 @@
</head>
<body>
<script>
if (sessionStorage.getItem('authenticated') !== 'true') {
window.location.href = '../login.html';
}
// if (sessionStorage.getItem('authenticated') !== 'true') {
// window.location.href = '../login.html';
// }
</script>
<header>
@ -401,6 +401,10 @@
<input type="checkbox" id="labelsToggle">
<span>Labels</span>
</label>
<label class="map-toggle-wrapper" style="cursor: pointer;">
<input type="checkbox" id="millsToggle">
<span>🏭 Mills</span>
</label>
</div>
</header>
@ -513,6 +517,7 @@
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<script src="google-sheets-config.js"></script>
<script src="script.js"></script>
</body>
</html>

View file

@ -20,9 +20,29 @@
let currentLayer = 'osm';
let geojsonLayer = null;
let labelsLayer = null;
let millsLayer = null;
let millMarkers = {};
let mills = [];
let currentGeojsonData = null;
let featuresList = [];
let showLabels = false;
let showMills = false;
// Color scheme for countries (for mills)
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',
};
// Toggle map layer
const mapToggle = document.getElementById('mapToggle');
@ -61,6 +81,24 @@
}
});
// Toggle mills visibility
const millsToggle = document.getElementById('millsToggle');
millsToggle.addEventListener('change', () => {
showMills = millsToggle.checked;
if (showMills) {
if (mills.length === 0) {
loadAndRenderMills();
} else if (!map.hasLayer(millsLayer)) {
millsLayer.addTo(map);
}
} else {
if (millsLayer && map.hasLayer(millsLayer)) {
map.removeLayer(millsLayer);
}
}
});
// Elements
const geojsonInput = document.getElementById('geojsonFile');
const fileNameDisplay = document.getElementById('fileName');
@ -242,6 +280,167 @@
}
}
// Load mills data and render on map
async function loadAndRenderMills() {
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_mill_locator/sugar_cane_factories_africa.csv');
if (response.ok) {
csvText = await response.text();
showMessage('Using local data (Google Sheet unavailable)', 'error');
}
} else {
showMessage('Sugar mills loaded from Google Sheets', 'success');
}
if (csvText) {
mills = parseMillsCSV(csvText);
renderMills();
}
} catch (error) {
console.warn('Could not load mills data:', error);
showMessage('Sugar mills data not available', 'error');
}
}
// Parse CSV for mills data
function parseMillsCSV(csvText) {
const lines = csvText.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim());
return lines.slice(1).map((line, index) => {
const values = parseMillsCSVLine(line);
const row = {};
headers.forEach((header, idx) => {
row[header] = values[idx] ? values[idx].trim() : '';
});
row._id = index;
return row;
}).filter(row => row.Latitude && row.Longitude);
}
// Parse CSV line handling quoted values
function parseMillsCSVLine(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() {
// Create or clear mills layer
if (millsLayer) {
map.removeLayer(millsLayer);
}
millsLayer = L.featureGroup();
millMarkers = {};
mills.forEach(mill => {
const lat = parseFloat(mill.Latitude);
const lng = parseFloat(mill.Longitude);
if (!isNaN(lat) && !isNaN(lng)) {
const production = parseFloat(String(mill['Annual Sugar Production (tons)'] || '0').replace(/[, ]/g, '')) || 0;
const size = getMillCircleSize(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
});
const popup = createMillPopup(mill);
marker.bindPopup(popup);
millsLayer.addLayer(marker);
millMarkers[mill._id] = marker;
}
});
millsLayer.addTo(map);
}
// Get circle size based on production
function getMillCircleSize(production) {
if (production > 150000) return 15;
if (production > 50000) return 10;
return 6;
}
// Create popup content for mills
function createMillPopup(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>${formatMillNumber(production)} tons/year</td>
</tr>
<tr>
<td style="font-weight: 600; color: #333;">Capacity:</td>
<td>${formatMillNumber(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>
</table>
</div>
`;
}
// Format numbers for display
function formatMillNumber(num) {
if (!num || num === 'N/A') return 'N/A';
const n = parseFloat(String(num).replace(/[, ]/g, ''));
if (isNaN(n)) return 'N/A';
return n.toLocaleString('en-US', { maximumFractionDigits: 0 });
}
// Handle file upload
geojsonInput.addEventListener('change', (e) => {
const file = e.target.files[0];
@ -736,3 +935,7 @@
}
});
});
// Initialize Google Sheets integration
initGoogleSheetsAutoRefresh();
showGoogleSheetsSetup();