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>
|
||||
<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>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue