let map; let osmLayer; let satelliteLayer; // Initialize map when DOM is ready document.addEventListener('DOMContentLoaded', function() { // Initialize map layers osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxNativeZoom: 19, maxZoom: 23 }); satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: '© Esri, DigitalGlobe, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community', maxNativeZoom: 18, maxZoom: 23 }); // Initialize map map = L.map('map', { maxZoom: 23, preferCanvas: true }).setView([0, 0], 2); osmLayer.addTo(map); // Invalidate map size to ensure proper rendering setTimeout(() => { map.invalidateSize(); }, 100); // Initialize DOM elements csvInput = document.getElementById('csvFile'); fileNameDisplay = document.getElementById('fileName'); errorMessage = document.getElementById('errorMessage'); successMessage = document.getElementById('successMessage'); statsSection = document.getElementById('statsSection'); featuresSection = document.getElementById('featuresSection'); recordCountEl = document.getElementById('recordCount'); columnCountEl = document.getElementById('columnCount'); mappedCountEl = document.getElementById('mappedCount'); featureLst = document.getElementById('featuresList'); clearBtn = document.getElementById('clearBtn'); downloadBtn = document.getElementById('downloadBtn'); featureSearch = document.getElementById('featureSearch'); polygonSection = document.getElementById('polygonSection'); createPolygonsBtn = document.getElementById('createPolygonsBtn'); downloadPolygonsBtn = document.getElementById('downloadPolygonsBtn'); polygonCount = document.getElementById('polygonCount'); // Setup event listeners csvInput.addEventListener('change', handleCsvUpload); clearBtn.addEventListener('click', handleClear); downloadBtn.addEventListener('click', handleDownload); featureSearch.addEventListener('input', handleSearch); createPolygonsBtn.addEventListener('click', handleCreatePolygons); downloadPolygonsBtn.addEventListener('click', handleDownloadPolygons); }); let currentLayer = 'osm'; let markerLayer = null; let polygonLayer = null; let currentCsvData = null; let featuresList = []; let markers = []; let selectedIndex = -1; let currentPolygons = null; // Toggle map layer let mapToggle; document.addEventListener('DOMContentLoaded', function() { mapToggle = document.getElementById('mapToggle'); mapToggle.addEventListener('change', () => { if (mapToggle.checked) { map.removeLayer(currentLayer === 'osm' ? osmLayer : satelliteLayer); satelliteLayer.addTo(map); currentLayer = 'satellite'; } else { map.removeLayer(currentLayer === 'satellite' ? satelliteLayer : osmLayer); osmLayer.addTo(map); currentLayer = 'osm'; } }); }); // DOM Elements - will be initialized on DOMContentLoaded let csvInput, fileNameDisplay, errorMessage, successMessage, statsSection, featuresSection; let recordCountEl, columnCountEl, mappedCountEl, featureLst, clearBtn; let downloadBtn, featureSearch, polygonSection, createPolygonsBtn, downloadPolygonsBtn, polygonCount; // Show message function showMessage(message, type = 'error') { const el = type === 'error' ? errorMessage : successMessage; el.textContent = message; el.classList.add('active'); setTimeout(() => el.classList.remove('active'), 5000); } // Parse CSV text function parseCSV(csvText) { const lines = csvText.trim().split('\n'); if (lines.length === 0) return null; const headers = lines[0].split(',').map(h => h.trim()); const data = []; for (let i = 1; i < lines.length; i++) { const fields = parseCSVLine(lines[i]); if (fields.length === 0) continue; const row = {}; for (let j = 0; j < headers.length; j++) { row[headers[j]] = fields[j] || ''; } data.push(row); } return { headers, data }; } // 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; } // Find column by name (case-insensitive, handle underscores and spaces) function findColumn(headers, searchName) { const normalized = searchName.toLowerCase().replace(/[\s_]/g, ''); return headers.find(header => header.toLowerCase().replace(/[\s_]/g, '') === normalized ); } // Load CSV data function loadCSV(csvData) { const { headers, data } = csvData; // Find required columns const latCol = findColumn(headers, 'start-geopoint-Latitude'); const lonCol = findColumn(headers, 'start-geopoint-Longitude'); const nameCol = findColumn(headers, 'field_id-field_name'); const codeCol = findColumn(headers, 'field_id-field_code'); if (!latCol || !lonCol) { showMessage('CSV must contain "start-geopoint-Latitude" and "start-geopoint-Longitude" columns'); return false; } currentCsvData = { headers, data, latCol, lonCol, nameCol, codeCol }; featuresList = []; markers = []; selectedIndex = -1; // Clear previous layer if (markerLayer) { map.removeLayer(markerLayer); } markerLayer = L.featureGroup(); let mappedCount = 0; const bounds = L.latLngBounds(); // Create markers data.forEach((row, index) => { const lat = parseFloat(row[latCol]); const lon = parseFloat(row[lonCol]); if (isNaN(lat) || isNaN(lon)) return; const fieldName = nameCol ? row[nameCol] : `Point ${index + 1}`; const fieldCode = codeCol ? row[codeCol] : ''; const marker = L.circleMarker([lat, lon], { radius: 6, fillColor: '#667eea', color: '#4a5fc1', weight: 2, opacity: 0.8, fillOpacity: 0.7 }); // Create popup content const popupContent = `
${fieldName} ${fieldCode ? `
Code: ${fieldCode}` : ''}
Latitude: ${lat.toFixed(6)}
Longitude: ${lon.toFixed(6)}
`; marker.bindPopup(popupContent); marker.on('click', () => selectFeature(index)); markerLayer.addLayer(marker); bounds.extend([lat, lon]); featuresList.push({ name: fieldName, code: fieldCode, lat, lon, index, row }); mappedCount++; }); // Add layer to map markerLayer.addTo(map); // Update stats recordCountEl.textContent = data.length; columnCountEl.textContent = headers.length; mappedCountEl.textContent = mappedCount; // Display features list displayFeaturesList(); // Fit bounds if (mappedCount > 0) { map.fitBounds(bounds, { padding: [50, 50] }); } // Show sections statsSection.style.display = 'block'; featuresSection.style.display = 'block'; polygonSection.style.display = 'block'; downloadBtn.style.display = 'block'; showMessage(`Successfully loaded ${mappedCount} field locations`, 'success'); return true; } // Display features list function displayFeaturesList() { featureLst.innerHTML = ''; if (featuresList.length === 0) { featureLst.innerHTML = '
No features loaded
'; return; } featuresList.forEach((feature, index) => { const item = document.createElement('div'); item.className = 'feature-item' + (selectedIndex === index ? ' active' : ''); item.innerHTML = ` ${feature.name} ${feature.code ? `
Code: ${feature.code}` : ''} `; item.addEventListener('click', () => selectFeature(index)); featureLst.appendChild(item); }); } // Select feature function selectFeature(index) { selectedIndex = index; const feature = featuresList[index]; // Update feature list highlight displayFeaturesList(); // Zoom to marker map.setView([feature.lat, feature.lon], 16); } // Handle file upload function handleCsvUpload(e) { const file = e.target.files[0]; if (!file) return; fileNameDisplay.textContent = `📄 ${file.name}`; const reader = new FileReader(); reader.onload = (event) => { try { const csvData = parseCSV(event.target.result); if (!csvData) { showMessage('Invalid CSV file'); return; } loadCSV(csvData); } catch (err) { showMessage(`Error parsing CSV: ${err.message}`); } }; reader.onerror = () => { showMessage('Error reading file'); }; reader.readAsText(file); } // Clear all function handleClear() { csvInput.value = ''; fileNameDisplay.textContent = 'No file selected'; if (markerLayer) { map.removeLayer(markerLayer); markerLayer = null; } currentCsvData = null; featuresList = []; markers = []; selectedIndex = -1; statsSection.style.display = 'none'; featuresSection.style.display = 'none'; polygonSection.style.display = 'none'; downloadBtn.style.display = 'none'; downloadPolygonsBtn.style.display = 'none'; if (polygonLayer) { map.removeLayer(polygonLayer); polygonLayer = null; } currentPolygons = null; showMessage('Data cleared', 'success'); } // Download CSV function handleDownload() { if (!currentCsvData) return; const { headers, data } = currentCsvData; let csv = headers.map(h => `"${h}"`).join(',') + '\n'; data.forEach(row => { const values = headers.map(header => { const value = row[header] || ''; return `"${String(value).replace(/"/g, '""')}"`; }); csv += values.join(',') + '\n'; }); const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'data.csv'; a.click(); window.URL.revokeObjectURL(url); showMessage('CSV downloaded', 'success'); } // Search features function handleSearch(e) { const searchTerm = e.target.value.toLowerCase(); const items = document.querySelectorAll('.feature-item'); items.forEach((item, index) => { const feature = featuresList[index]; const match = feature.name.toLowerCase().includes(searchTerm) || feature.code.toLowerCase().includes(searchTerm); item.style.display = match ? 'block' : 'none'; }); } // Parse boundary string to coordinates function parseBoundary(boundaryString) { if (!boundaryString || boundaryString.trim() === '') return null; const coordinates = []; const parts = boundaryString.split(';').map(p => p.trim()).filter(p => p.length > 0); for (const part of parts) { const values = part.split(/\s+/); if (values.length >= 2) { const lat = parseFloat(values[0]); const lon = parseFloat(values[1]); if (!isNaN(lat) && !isNaN(lon)) { // GeoJSON uses [longitude, latitude] format coordinates.push([lon, lat]); } } } // Close the polygon ring if not already closed if (coordinates.length > 0 && (coordinates[0][0] !== coordinates[coordinates.length - 1][0] || coordinates[0][1] !== coordinates[coordinates.length - 1][1])) { coordinates.push(coordinates[0]); } return coordinates.length > 3 ? [coordinates] : null; } // Create polygons from CSV data function handleCreatePolygons() { if (!currentCsvData || !featuresList.length) { showMessage('No data loaded. Please upload a CSV file first.'); return; } const { headers, data, latCol, lonCol, nameCol, codeCol } = currentCsvData; const boundaryCol = findColumn(headers, 'field_id-field_boundary'); if (!boundaryCol) { showMessage('CSV does not contain "field_id-field_boundary" column'); return; } // Clear previous polygons if (polygonLayer) { map.removeLayer(polygonLayer); } polygonLayer = L.featureGroup(); const features = []; let polygonCount_val = 0; // Create polygons from boundary data data.forEach((row, index) => { const boundaryString = row[boundaryCol]; const coordinates = parseBoundary(boundaryString); if (!coordinates) return; const fieldName = nameCol ? row[nameCol] : `Field ${index + 1}`; const fieldCode = codeCol ? row[codeCol] : ''; // Create GeoJSON feature const feature = { type: 'Feature', geometry: { type: 'Polygon', coordinates: coordinates }, properties: { name: fieldName, code: fieldCode, index: index } }; features.push(feature); // Create Leaflet polygon const polygon = L.geoJSON(feature, { style: { color: '#667eea', weight: 2, opacity: 0.7, fillOpacity: 0.3 }, onEachFeature: (feature, layer) => { const popupContent = `
${feature.properties.name} ${feature.properties.code ? `
Code: ${feature.properties.code}` : ''}
`; layer.bindPopup(popupContent); } }).addTo(polygonLayer); polygonCount_val++; }); // Add polygon layer to map polygonLayer.addTo(map); // Store polygons for download currentPolygons = { type: 'FeatureCollection', features: features }; // Update count polygonCount.textContent = polygonCount_val; downloadPolygonsBtn.style.display = 'block'; showMessage(`Successfully created ${polygonCount_val} polygons`, 'success'); } // Download polygons as GeoJSON function handleDownloadPolygons() { if (!currentPolygons) return; const geojsonString = JSON.stringify(currentPolygons, null, 2); const blob = new Blob([geojsonString], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'polygons.geojson'; a.click(); window.URL.revokeObjectURL(url); showMessage('GeoJSON downloaded', 'success'); }