Add mill layer option to GeoJSON viewer
This commit is contained in:
parent
7c55b38954
commit
4b1a51d0e9
135
webapps/geojson_viewer/google-sheets-config.js
Normal file
135
webapps/geojson_viewer/google-sheets-config.js
Normal 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 ║
|
||||||
|
║ ║
|
||||||
|
╚════════════════════════════════════════════════════════════╝
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
@ -384,9 +384,9 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
if (sessionStorage.getItem('authenticated') !== 'true') {
|
// if (sessionStorage.getItem('authenticated') !== 'true') {
|
||||||
window.location.href = '../login.html';
|
// window.location.href = '../login.html';
|
||||||
}
|
// }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
|
@ -401,6 +401,10 @@
|
||||||
<input type="checkbox" id="labelsToggle">
|
<input type="checkbox" id="labelsToggle">
|
||||||
<span>Labels</span>
|
<span>Labels</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="map-toggle-wrapper" style="cursor: pointer;">
|
||||||
|
<input type="checkbox" id="millsToggle">
|
||||||
|
<span>🏭 Mills</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -513,6 +517,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
<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>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -20,9 +20,29 @@
|
||||||
let currentLayer = 'osm';
|
let currentLayer = 'osm';
|
||||||
let geojsonLayer = null;
|
let geojsonLayer = null;
|
||||||
let labelsLayer = null;
|
let labelsLayer = null;
|
||||||
|
let millsLayer = null;
|
||||||
|
let millMarkers = {};
|
||||||
|
let mills = [];
|
||||||
let currentGeojsonData = null;
|
let currentGeojsonData = null;
|
||||||
let featuresList = [];
|
let featuresList = [];
|
||||||
let showLabels = false;
|
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
|
// Toggle map layer
|
||||||
const mapToggle = document.getElementById('mapToggle');
|
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
|
// Elements
|
||||||
const geojsonInput = document.getElementById('geojsonFile');
|
const geojsonInput = document.getElementById('geojsonFile');
|
||||||
const fileNameDisplay = document.getElementById('fileName');
|
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
|
// Handle file upload
|
||||||
geojsonInput.addEventListener('change', (e) => {
|
geojsonInput.addEventListener('change', (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
|
|
@ -736,3 +935,7 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize Google Sheets integration
|
||||||
|
initGoogleSheetsAutoRefresh();
|
||||||
|
showGoogleSheetsSetup();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue