// Initialize map layers const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxNativeZoom: 19, maxZoom: 23 }); const 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 const map = L.map('map', { maxZoom: 23 }).setView([0, 0], 2); osmLayer.addTo(map); let currentLayer = 'osm'; let geojsonLayer = null; let labelsLayer = null; let currentGeojsonData = null; let featuresList = []; let showLabels = false; // Toggle map layer const mapToggle = document.getElementById('mapToggle'); const mapTypeLabel = document.getElementById('mapTypeLabel'); mapToggle.addEventListener('change', () => { if (mapToggle.checked) { // Switch to satellite map.removeLayer(osmLayer); satelliteLayer.addTo(map); mapTypeLabel.textContent = 'SAT'; currentLayer = 'satellite'; } else { // Switch to OSM map.removeLayer(satelliteLayer); osmLayer.addTo(map); mapTypeLabel.textContent = 'OSM'; currentLayer = 'osm'; } }); // Toggle labels visibility const labelsToggle = document.getElementById('labelsToggle'); labelsToggle.addEventListener('change', () => { showLabels = labelsToggle.checked; if (!labelsLayer) { console.warn('Labels layer not initialized'); return; } if (showLabels) { labelsLayer.addTo(map); updateLabelsVisibility(); } else { map.removeLayer(labelsLayer); } }); // Elements const geojsonInput = document.getElementById('geojsonFile'); const fileNameDisplay = document.getElementById('fileName'); const errorMessage = document.getElementById('errorMessage'); const successMessage = document.getElementById('successMessage'); const statsSection = document.getElementById('statsSection'); const featuresSection = document.getElementById('featuresSection'); const propertiesSection = document.getElementById('propertiesSection'); const propertiesContent = document.getElementById('propertiesContent'); const featureCountEl = document.getElementById('featureCount'); const geomTypeEl = document.getElementById('geomType'); const boundsEl = document.getElementById('bounds'); const featureLst = document.getElementById('featuresList'); const clearBtn = document.getElementById('clearBtn'); const downloadBtn = document.getElementById('downloadBtn'); const featureModal = document.getElementById('featureModal'); const modalTitle = document.getElementById('modalTitle'); const modalBody = document.getElementById('modalBody'); const summarySection = document.getElementById('summarySection'); const tableSection = document.getElementById('tableSection'); const clientNameEl = document.getElementById('clientName'); const totalFieldsEl = document.getElementById('totalFields'); const totalHectaresEl = document.getElementById('totalHectares'); const totalAcresEl = document.getElementById('totalAcres'); const featureSearch = document.getElementById('featureSearch'); // 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); } // Calculate area in square meters using Turf.js function getFeatureArea(feature) { try { if (!feature.geometry) return 0; const geomType = feature.geometry.type; if (geomType === 'Point') return 0; if (geomType === 'LineString' || geomType === 'MultiLineString') return 0; // Use Haversine formula for rough area calculation const coords = feature.geometry.coordinates; if (geomType === 'Polygon') { return calculatePolygonArea(coords[0]); } else if (geomType === 'MultiPolygon') { return coords.reduce((total, polygon) => total + calculatePolygonArea(polygon[0]), 0); } } catch (e) { console.warn('Error calculating area:', e); return 0; } return 0; } // Calculate polygon area using Shoelace formula function calculatePolygonArea(coords) { if (!coords || coords.length < 3) return 0; let area = 0; const R = 6371000; // Earth radius in meters for (let i = 0; i < coords.length - 1; i++) { const p1 = coords[i]; const p2 = coords[i + 1]; const lat1 = (p1[1] * Math.PI) / 180; const lat2 = (p2[1] * Math.PI) / 180; const dLng = ((p2[0] - p1[0]) * Math.PI) / 180; area += Math.sin(lat1) * Math.cos(lat1) * dLng; area += Math.sin(lat1) * Math.cos(lat2) * Math.sin(dLng); } area = Math.abs(area * R * R / 2); return area; } // Convert square meters to hectares and acres function convertArea(squareMeters) { const hectares = squareMeters / 10000; const acres = squareMeters / 4046.856; return { hectares, acres }; } // Get first property key (field code) function getFirstPropertyValue(properties) { if (!properties) return ''; const keys = Object.keys(properties); if (keys.length === 0) return ''; return properties[keys[0]]; } // Create labels layer from features function createLabelsLayer(features) { labelsLayer = L.featureGroup([]); // First pass: collect all label positions const labelPositions = []; features.forEach((feature, index) => { const props = feature.properties || {}; const fieldCode = getFirstPropertyValue(props); const displayLabel = fieldCode || `Feature ${index + 1}`; if (feature.geometry && feature.geometry.type !== 'Point') { // Get centroid for polygon features const bounds = L.geoJSON(feature).getBounds(); const center = bounds.getCenter(); labelPositions.push({ fieldName: displayLabel, latlng: center, originalLatlng: center, iconAnchor: [0, 0], isPoint: false }); } else if (feature.geometry && feature.geometry.type === 'Point') { // For points, add label above the marker const latlng = L.GeoJSON.coordsToLatLng(feature.geometry.coordinates); labelPositions.push({ fieldName: displayLabel, latlng: latlng, originalLatlng: latlng, iconAnchor: [0, 30], isPoint: true }); } }); // Create markers labelPositions.forEach((pos) => { const label = L.marker(pos.latlng, { icon: L.divIcon({ className: 'field-label', html: `
`, iconSize: [null, null], iconAnchor: pos.iconAnchor }), interactive: false }); labelsLayer.addLayer(label); }); } // Update labels visibility based on zoom level function updateLabelsVisibility() { const zoomLevel = map.getZoom(); const minZoomForLabels = 16; // Only show labels at zoom level 13 and above if (labelsLayer && showLabels) { if (zoomLevel >= minZoomForLabels) { labelsLayer.eachLayer(layer => { layer.setOpacity(1); }); } else { labelsLayer.eachLayer(layer => { layer.setOpacity(0); }); } } } // Handle file upload geojsonInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; fileNameDisplay.textContent = `📄 ${file.name}`; fileNameDisplay.classList.add('active'); const reader = new FileReader(); reader.onload = (event) => { try { const geojson = JSON.parse(event.target.result); const success = loadGeojson(geojson, file.name); if (success) { showMessage('GeoJSON loaded successfully!', 'success'); } } catch (error) { showMessage(`Error parsing file: ${error.message}`, 'error'); } }; reader.onerror = () => { showMessage('Error reading file', 'error'); }; reader.readAsText(file); }); // Validate CRS function validateCRS(geojson) { // If no CRS is specified, assume WGS84 (default for GeoJSON) if (!geojson.crs) { return { valid: true, message: 'No CRS specified (default WGS84)' }; } const crs = geojson.crs; let crsName = ''; // Extract CRS name from different possible formats if (crs.properties && crs.properties.name) { crsName = crs.properties.name; } else if (typeof crs === 'string') { crsName = crs; } // Check if it's WGS84 (various possible names) const wgs84Variations = [ 'EPSG:4326', 'epsg:4326', 'urn:ogc:def:crs:EPSG::4326', 'urn:ogc:def:crs:EPSG:4.3:4326', 'WGS84', 'wgs84', 'urn:ogc:def:crs:OGC:1.3:CRS84' ]; const isWGS84 = wgs84Variations.some(variant => crsName.toUpperCase().includes(variant.toUpperCase()) ); if (isWGS84) { return { valid: true, message: 'CRS: WGS84' }; } else { return { valid: false, message: `Non-supported CRS (${JSON.stringify(crs)})` }; } } // Load and display GeoJSON function loadGeojson(geojson, fileName) { // Validate CRS first const crsValidation = validateCRS(geojson); if (!crsValidation.valid) { showMessage(crsValidation.message, 'error'); return false; } // Clear previous layer if (geojsonLayer) { map.removeLayer(geojsonLayer); } // Clear previous labels if (labelsLayer) { map.removeLayer(labelsLayer); } currentGeojsonData = geojson; featuresList = []; // Handle both FeatureCollection and individual features const features = geojson.type === 'FeatureCollection' ? geojson.features : [geojson]; if (features.length === 0) { showMessage('GeoJSON contains no features', 'error'); return false; } // Create layer with popups geojsonLayer = L.geoJSON(geojson, { onEachFeature: (feature, layer) => { const index = featuresList.length; featuresList.push({ feature, layer, index }); // Add click handler to open modal layer.on('click', (e) => { L.DomEvent.stopPropagation(e); openFeatureModal(feature); selectFeature(index); }); // Hover effect layer.on('mouseover', () => { if (layer.setStyle) { layer.setStyle({ weight: 3, opacity: 1, dashArray: '' }); } }); layer.on('mouseout', () => { if (layer.setStyle) { geojsonLayer.resetStyle(layer); } }); }, style: { color: '#667eea', weight: 2, opacity: 0.7, fillOpacity: 0.5 }, pointToLayer: (feature, latlng) => { return L.circleMarker(latlng, { radius: 8, fillColor: '#667eea', color: '#fff', weight: 2, opacity: 1, fillOpacity: 0.8 }); } }).addTo(map); // Create labels layer createLabelsLayer(features); // Fit bounds const bounds = geojsonLayer.getBounds(); map.fitBounds(bounds, { padding: [50, 50] }); // Update labels visibility based on zoom level updateLabelsVisibility(); map.on('zoom', updateLabelsVisibility); // Update stats updateStats(geojson, features); // Display features list displayFeaturesList(); // Build attribute table buildAttributeTable(features); // Calculate and display summary updateSummary(geojson, features); // Show/hide elements summarySection.style.display = 'block'; statsSection.style.display = 'block'; featuresSection.style.display = 'block'; tableSection.style.display = 'block'; downloadBtn.style.display = 'block'; return true; } // Update statistics function updateStats(geojson, features) { featureCountEl.textContent = features.length; // Get geometry type const types = new Set(); features.forEach(f => { if (f.geometry) { types.add(f.geometry.type); } }); geomTypeEl.textContent = Array.from(types).join(', ') || '-'; // Get bounds const bounds = geojsonLayer.getBounds(); const sw = bounds.getSouthWest(); const ne = bounds.getNorthEast(); boundsEl.textContent = `[${sw.lat.toFixed(2)}, ${sw.lng.toFixed(2)}] - [${ne.lat.toFixed(2)}, ${ne.lng.toFixed(2)}]`; } // Display features list function displayFeaturesList() { featureLst.innerHTML = ''; featureSearch.value = ''; featuresList.forEach((item, index) => { const div = document.createElement('div'); div.className = 'feature-item'; // Get first property value (field code) const props = item.feature.properties || {}; const fieldCode = getFirstPropertyValue(props); const name = fieldCode || `Feature ${index + 1}`; div.textContent = name; div.dataset.index = index; div.addEventListener('click', () => selectFeature(index)); featureLst.appendChild(div); }); } // Select feature function selectFeature(index) { const item = featuresList[index]; if (!item) { console.error('Feature not found at index:', index); return; } // Update feature list UI document.querySelectorAll('.feature-item').forEach((el, i) => { el.classList.toggle('active', i === index); }); // Display properties in sidebar displayProperties(item.feature); // Open modal with properties openFeatureModal(item.feature); // Zoom to feature on map setTimeout(() => { try { if (item.feature.geometry.type === 'Point') { const latlng = item.layer.getLatLng(); map.setView(latlng, 14); } else { const bounds = L.geoJSON(item.feature).getBounds(); map.fitBounds(bounds, { padding: [100, 100] }); } } catch (e) { console.error('Error zooming to feature:', e); } }, 100); // Update map styling if (item.layer.setStyle) { item.layer.setStyle({ weight: 3, opacity: 1 }); } } // Display properties function displayProperties(feature) { propertiesSection.classList.add('active'); propertiesContent.innerHTML = ''; const props = feature.properties || {}; if (Object.keys(props).length === 0) { propertiesContent.innerHTML = '