From 4b1a51d0e968911bb8db9e3df23071cc67f6dc3f Mon Sep 17 00:00:00 2001 From: Nik Verweel Date: Wed, 4 Feb 2026 13:40:06 +0100 Subject: [PATCH] Add mill layer option to GeoJSON viewer --- .../geojson_viewer/google-sheets-config.js | 135 ++++++++++++ webapps/geojson_viewer/index.html | 11 +- webapps/geojson_viewer/script.js | 203 ++++++++++++++++++ 3 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 webapps/geojson_viewer/google-sheets-config.js diff --git a/webapps/geojson_viewer/google-sheets-config.js b/webapps/geojson_viewer/google-sheets-config.js new file mode 100644 index 0000000..ac0304f --- /dev/null +++ b/webapps/geojson_viewer/google-sheets-config.js @@ -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 ║ + ║ ║ + ╚════════════════════════════════════════════════════════════╝ + `); +} diff --git a/webapps/geojson_viewer/index.html b/webapps/geojson_viewer/index.html index 48032be..bd3ecc0 100644 --- a/webapps/geojson_viewer/index.html +++ b/webapps/geojson_viewer/index.html @@ -384,9 +384,9 @@
@@ -401,6 +401,10 @@ Labels +
@@ -513,6 +517,7 @@ + \ No newline at end of file diff --git a/webapps/geojson_viewer/script.js b/webapps/geojson_viewer/script.js index 858573f..2991ef8 100644 --- a/webapps/geojson_viewer/script.js +++ b/webapps/geojson_viewer/script.js @@ -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 ` +
+ ${mill['Mill/Factory']}
+ ${mill['Country']} • ${mill['Province/Region']} +
+ + + + + + + + + + + + + + + + + + + + + +
Company:${mill.Company || 'N/A'}
Production:${formatMillNumber(production)} tons/year
Capacity:${formatMillNumber(capacity)} tons/year
Coordinates:${parseFloat(mill.Latitude).toFixed(4)}, ${parseFloat(mill.Longitude).toFixed(4)}
Data Year:${year}
+
+ `; + } + + // 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();