| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>MSPO Deforestation</title> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" /> |
| <link rel="stylesheet" href="style2.css"> |
| <style> |
| |
| </style> |
| </head> |
| <body> |
| <div class="header"> |
| <div class="logo"></div> |
| <img src="https://images.squarespace-cdn.com/content/v1/604db3a6dad32a12b2415387/1636475927545-PK57PVLIB7AEKX1AJQJ8/Logo_MSPO_2020.png" alt="MSPO Logo" style="height: 60px;"> |
| <div class="logo" "textalign=left">MSPO Deforestation Mapping</div> |
| <div class="logo"></div> |
| <div class="controls"> |
| <div class="control-group"> |
| <label>Search Location</label> |
| <input type="text" class="search-box" id="searchBox" placeholder="Enter city or address..."> |
| </div> |
| <div class="control-group"> |
| <label>Left Panel</label> |
| <select class="layer-select" id="leftLayer"> |
| <option value="osm">OpenStreetMap</option> |
| <option value="satellite">Satellite</option> |
| <option value="terrain">Terrain</option> |
| <option value="sentinel2">Sentinel-2</option> |
| <option value="satelogic">satelogic</option> |
| </select> |
| </div> |
| <div class="control-group"> |
| <label>Right Panel</label> |
| <select class="layer-select" id="rightLayer"> |
| <option value="satellite" selected>Satellite</option> |
| <option value="osm">OpenStreetMap</option> |
| <option value="terrain">Terrain</option> |
| <option value="sentinel2">SENTINEL-2</option> |
| <option value="satelogic">Satelogic</option> |
| </select> |
| </div> |
| <input type="file" class="file-input" id="importFile" accept=".geojson,.json,.kml,.gpx" style="display: none;"> |
| <button class="search-btn" id="searchBtn">Search</button> |
| </div> |
| </div> |
|
|
| <div class="map-container"> |
| <div class="map-panel left-panel" id="leftPanel"> |
| <div class="panel-label">OpenStreetMap</div> |
| <div class="map" id="leftMap"></div> |
| <div class="coordinates" id="leftCoords">Lat: 0.0000, Lng: 0.0000</div> |
| </div> |
| |
| <div class="map-panel right-panel" id="rightPanel"> |
| <div class="panel-label">Satellite</div> |
| <div class="map" id="rightMap"></div> |
| <div class="coordinates" id="rightCoords">Lat: 0.0000, Lng: 0.0000</div> |
| </div> |
| |
| <div class="slider-container" id="sliderContainer"> |
| <div class="slider-handle" id="sliderHandle"></div> |
| </div> |
| |
| <button class="sync-toggle active" id="syncToggle" title="Toggle map synchronization"> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> |
| <path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/> |
| </svg> |
| </button> |
| </div> |
|
|
| <div class="loading-indicator" id="loadingIndicator"> |
| <div>Loading...</div> |
| </div> |
|
|
| <div class="error-message" id="errorMessage"> |
| <div id="errorText"></div> |
| </div> |
|
|
| <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script> |
| <script> |
| |
| const DEFAULT_LOCATION = [40.7128, -74.0060]; |
| const DEFAULT_ZOOM = 10; |
| const SEARCH_ZOOM = 12; |
| const MIN_SLIDER_POSITION = 15; |
| const MAX_SLIDER_POSITION = 85; |
| |
| |
| |
| const tileLayers = { |
| osm: { |
| url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', |
| attribution: '© OpenStreetMap contributors', |
| name: 'OpenStreetMap' |
| }, |
| satellite: { |
| url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', |
| attribution: 'Tiles © Esri', |
| name: 'Satellite' |
| }, |
| terrain: { |
| url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', |
| attribution: '© OpenTopoMap contributors', |
| name: 'Terrain' |
| }, |
| sentinel2: { |
| |
| url: 'https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2020_3857/default/g/{z}/{y}/{x}.jpg', |
| attribution: '© SENTINEL-2 Cloudless 2020, EOX IT Services GmbH', |
| name: 'sentinel2 Mode' |
| }, |
| satelogic: { |
| url: 'http://localhost:8080/data/forest/{z}/{x}/{y}.png', |
| attribution: '© Satelogic', |
| name: 'satelogic Mode' |
| } |
| |
| }; |
| |
| |
| let leftMap, rightMap; |
| let leftLayer, rightLayer; |
| let isSynced = true; |
| let mapSyncLock = false; |
| let isSliderActive = false; |
| let searchMarkers = []; |
| |
| |
| const elements = { |
| leftPanel: document.getElementById('leftPanel'), |
| rightPanel: document.getElementById('rightPanel'), |
| sliderContainer: document.getElementById('sliderContainer'), |
| sliderHandle: document.getElementById('sliderHandle'), |
| mapContainer: document.querySelector('.map-container'), |
| searchBox: document.getElementById('searchBox'), |
| searchBtn: document.getElementById('searchBtn'), |
| leftLayer: document.getElementById('leftLayer'), |
| rightLayer: document.getElementById('rightLayer'), |
| syncToggle: document.getElementById('syncToggle'), |
| leftCoords: document.getElementById('leftCoords'), |
| rightCoords: document.getElementById('rightCoords'), |
| loadingIndicator: document.getElementById('loadingIndicator'), |
| errorMessage: document.getElementById('errorMessage'), |
| errorText: document.getElementById('errorText') |
| }; |
| |
| |
| function showError(message) { |
| elements.errorText.textContent = message; |
| elements.errorMessage.style.display = 'block'; |
| setTimeout(() => { |
| elements.errorMessage.style.display = 'none'; |
| }, 5000); |
| } |
| |
| function showLoading() { |
| elements.loadingIndicator.style.display = 'block'; |
| } |
| |
| function hideLoading() { |
| elements.loadingIndicator.style.display = 'none'; |
| } |
| |
| function updatePanelLabels() { |
| const leftLabel = document.querySelector('.left-panel .panel-label'); |
| const rightLabel = document.querySelector('.right-panel .panel-label'); |
| |
| if (leftLabel) { |
| leftLabel.textContent = tileLayers[elements.leftLayer.value].name; |
| } |
| if (rightLabel) { |
| rightLabel.textContent = tileLayers[elements.rightLayer.value].name; |
| } |
| } |
| |
| |
| function initializeMaps() { |
| try { |
| |
| leftMap = L.map('leftMap', { |
| zoomControl: false, |
| attributionControl: true |
| }).setView(DEFAULT_LOCATION, DEFAULT_ZOOM); |
| |
| |
| rightMap = L.map('rightMap', { |
| zoomControl: false, |
| attributionControl: true |
| }).setView(DEFAULT_LOCATION, DEFAULT_ZOOM); |
| |
| |
| L.control.zoom({ |
| position: 'topright' |
| }).addTo(leftMap); |
| |
| L.control.zoom({ |
| position: 'topright' |
| }).addTo(rightMap); |
| |
| |
| leftLayer = L.tileLayer(tileLayers.osm.url, { |
| attribution: tileLayers.osm.attribution, |
| maxZoom: 18 |
| }).addTo(leftMap); |
| |
| rightLayer = L.tileLayer(tileLayers.satellite.url, { |
| attribution: tileLayers.satellite.attribution, |
| maxZoom: 18 |
| }).addTo(rightMap); |
| |
| |
| setupMapEvents(); |
| |
| |
| updateCoordinates(leftMap, elements.leftCoords); |
| updateCoordinates(rightMap, elements.rightCoords); |
| |
| console.log('Maps initialized successfully'); |
| |
| } catch (error) { |
| console.error('Error initializing maps:', error); |
| showError('Failed to initialize maps. Please refresh the page.'); |
| } |
| } |
| |
| |
| function setupMapEvents() { |
| leftMap.on('moveend', () => { |
| updateCoordinates(leftMap, elements.leftCoords); |
| if (isSynced && !mapSyncLock && !isSliderActive) { |
| syncMaps(leftMap, rightMap); |
| } |
| }); |
| |
| rightMap.on('moveend', () => { |
| updateCoordinates(rightMap, elements.rightCoords); |
| if (isSynced && !mapSyncLock && !isSliderActive) { |
| syncMaps(rightMap, leftMap); |
| } |
| }); |
| |
| |
| leftMap.on('tileerror', (e) => { |
| console.warn('Left map tile error:', e); |
| }); |
| |
| rightMap.on('tileerror', (e) => { |
| console.warn('Right map tile error:', e); |
| }); |
| } |
| |
| |
| function updateCoordinates(map, coordsElement) { |
| if (!map || !coordsElement) return; |
| |
| const center = map.getCenter(); |
| const zoom = map.getZoom(); |
| coordsElement.textContent = `Lat: ${center.lat.toFixed(4)}, Lng: ${center.lng.toFixed(4)}, Zoom: ${zoom}`; |
| } |
| |
| |
| function syncMaps(sourceMap, targetMap) { |
| if (!isSynced || !sourceMap || !targetMap || mapSyncLock) return; |
| |
| mapSyncLock = true; |
| |
| try { |
| const center = sourceMap.getCenter(); |
| const zoom = sourceMap.getZoom(); |
| targetMap.setView(center, zoom); |
| } catch (error) { |
| console.error('Error syncing maps:', error); |
| } |
| |
| |
| setTimeout(() => { |
| mapSyncLock = false; |
| }, 100); |
| } |
| |
| |
| let isSliderDragging = false; |
| let startMouseX = 0; |
| let startSliderPosition = 50; |
| |
| function updateSliderPosition(positionPercentage) { |
| |
| positionPercentage = Math.max(MIN_SLIDER_POSITION, Math.min(MAX_SLIDER_POSITION, positionPercentage)); |
| |
| |
| elements.sliderContainer.style.left = `${positionPercentage}%`; |
| |
| |
| elements.leftPanel.style.clipPath = `polygon(0 0, ${positionPercentage}% 0, ${positionPercentage}% 100%, 0 100%)`; |
| elements.rightPanel.style.clipPath = `polygon(${positionPercentage}% 0, 100% 0, 100% 100%, ${positionPercentage}% 100%)`; |
| } |
| |
| function getMouseX(e) { |
| return e.clientX || (e.touches && e.touches[0] ? e.touches[0].clientX : 0); |
| } |
| |
| function startSliderDrag(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| isSliderDragging = true; |
| isSliderActive = true; |
| startMouseX = getMouseX(e); |
| |
| |
| const containerRect = elements.mapContainer.getBoundingClientRect(); |
| const sliderRect = elements.sliderContainer.getBoundingClientRect(); |
| startSliderPosition = ((sliderRect.left - containerRect.left) / containerRect.width) * 100; |
| |
| |
| elements.sliderContainer.classList.add('dragging'); |
| elements.sliderHandle.classList.add('dragging'); |
| |
| |
| document.addEventListener('mousemove', onSliderDrag, { passive: false }); |
| document.addEventListener('mouseup', stopSliderDrag); |
| document.addEventListener('touchmove', onSliderDrag, { passive: false }); |
| document.addEventListener('touchend', stopSliderDrag); |
| |
| |
| document.body.style.cursor = 'ew-resize'; |
| document.body.style.userSelect = 'none'; |
| } |
| |
| function onSliderDrag(e) { |
| if (!isSliderDragging) return; |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| const currentMouseX = getMouseX(e); |
| const deltaX = currentMouseX - startMouseX; |
| const containerWidth = elements.mapContainer.offsetWidth; |
| const deltaPercentage = (deltaX / containerWidth) * 100; |
| |
| const newPosition = startSliderPosition + deltaPercentage; |
| updateSliderPosition(newPosition); |
| } |
| |
| function stopSliderDrag(e) { |
| if (!isSliderDragging) return; |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| isSliderDragging = false; |
| |
| |
| elements.sliderContainer.classList.remove('dragging'); |
| elements.sliderHandle.classList.remove('dragging'); |
| |
| |
| document.removeEventListener('mousemove', onSliderDrag); |
| document.removeEventListener('mouseup', stopSliderDrag); |
| document.removeEventListener('touchmove', onSliderDrag); |
| document.removeEventListener('touchend', stopSliderDrag); |
| |
| |
| document.body.style.cursor = ''; |
| document.body.style.userSelect = ''; |
| |
| |
| setTimeout(() => { |
| isSliderActive = false; |
| }, 300); |
| } |
| |
| |
| function switchLayer(map, currentLayer, layerType) { |
| if (!map || !currentLayer) return null; |
| |
| try { |
| map.removeLayer(currentLayer); |
| const selectedLayer = tileLayers[layerType]; |
| const newLayer = L.tileLayer(selectedLayer.url, { |
| attribution: selectedLayer.attribution, |
| maxZoom: 18 |
| }).addTo(map); |
| |
| return newLayer; |
| } catch (error) { |
| console.error('Error switching layer:', error); |
| showError('Failed to switch map layer'); |
| return currentLayer; |
| } |
| } |
| |
| |
| function clearSearchMarkers() { |
| searchMarkers.forEach(marker => { |
| if (leftMap && leftMap.hasLayer(marker.left)) { |
| leftMap.removeLayer(marker.left); |
| } |
| if (rightMap && rightMap.hasLayer(marker.right)) { |
| rightMap.removeLayer(marker.right); |
| } |
| }); |
| searchMarkers = []; |
| } |
| |
| async function searchLocation(query) { |
| if (!query || query.trim() === '') { |
| showError('Please enter a search term'); |
| return false; |
| } |
| |
| showLoading(); |
| elements.searchBtn.disabled = true; |
| |
| try { |
| const response = await fetch( |
| `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=1&addressdetails=1`, |
| { |
| headers: { |
| 'User-Agent': 'Split Panel Maps' |
| } |
| } |
| ); |
| |
| if (!response.ok) { |
| throw new Error('Search service unavailable'); |
| } |
| |
| const data = await response.json(); |
| |
| if (data.length > 0) { |
| const lat = parseFloat(data[0].lat); |
| const lng = parseFloat(data[0].lon); |
| |
| if (isNaN(lat) || isNaN(lng)) { |
| throw new Error('Invalid coordinates received'); |
| } |
| |
| |
| clearSearchMarkers(); |
| |
| |
| const markerLeft = L.marker([lat, lng]).addTo(leftMap) |
| .bindPopup(`<b>${data[0].display_name}</b>`) |
| .openPopup(); |
| |
| const markerRight = L.marker([lat, lng]).addTo(rightMap) |
| .bindPopup(`<b>${data[0].display_name}</b>`) |
| .openPopup(); |
| |
| searchMarkers.push({ left: markerLeft, right: markerRight }); |
| |
| |
| leftMap.setView([lat, lng], SEARCH_ZOOM); |
| if (isSynced) { |
| rightMap.setView([lat, lng], SEARCH_ZOOM); |
| } |
| |
| return true; |
| } else { |
| showError('Location not found. Please try a different search term.'); |
| return false; |
| } |
| } catch (error) { |
| console.error('Search error:', error); |
| showError('Search failed. Please try again.'); |
| return false; |
| } finally { |
| hideLoading(); |
| elements.searchBtn.disabled = false; |
| } |
| } |
| |
| |
| function setupEventListeners() { |
| |
| elements.sliderContainer.addEventListener('mousedown', startSliderDrag); |
| elements.sliderHandle.addEventListener('mousedown', startSliderDrag); |
| elements.sliderContainer.addEventListener('touchstart', startSliderDrag); |
| elements.sliderHandle.addEventListener('touchstart', startSliderDrag); |
| |
| |
| elements.sliderContainer.addEventListener('contextmenu', (e) => e.preventDefault()); |
| elements.sliderHandle.addEventListener('contextmenu', (e) => e.preventDefault()); |
| |
| |
| elements.syncToggle.addEventListener('click', () => { |
| isSynced = !isSynced; |
| elements.syncToggle.classList.toggle('active', isSynced); |
| |
| if (isSynced && leftMap && rightMap) { |
| syncMaps(leftMap, rightMap); |
| } |
| }); |
| |
| |
| elements.leftLayer.addEventListener('change', (e) => { |
| if (leftMap && leftLayer) { |
| leftLayer = switchLayer(leftMap, leftLayer, e.target.value); |
| updatePanelLabels(); |
| } |
| }); |
| |
| elements.rightLayer.addEventListener('change', (e) => { |
| if (rightMap && rightLayer) { |
| rightLayer = switchLayer(rightMap, rightLayer, e.target.value); |
| updatePanelLabels(); |
| } |
| }); |
| |
| |
| elements.searchBtn.addEventListener('click', async () => { |
| const query = elements.searchBox.value.trim(); |
| await searchLocation(query); |
| }); |
| |
| elements.searchBox.addEventListener('keypress', async (e) => { |
| if (e.key === 'Enter') { |
| const query = e.target.value.trim(); |
| await searchLocation(query); |
| } |
| }); |
| |
| |
| window.addEventListener('resize', () => { |
| setTimeout(() => { |
| if (leftMap) leftMap.invalidateSize(); |
| if (rightMap) rightMap.invalidateSize(); |
| }, 300); |
| }); |
| |
| |
| elements.errorMessage.addEventListener('click', () => { |
| elements.errorMessage.style.display = 'none'; |
| }); |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| console.log('DOM loaded, initializing application...'); |
| |
| |
| setupEventListeners(); |
| |
| |
| setTimeout(() => { |
| initializeMaps(); |
| updatePanelLabels(); |
| }, 100); |
| }); |
| |
| |
| document.addEventListener('visibilitychange', () => { |
| if (!document.hidden) { |
| |
| setTimeout(() => { |
| if (leftMap) leftMap.invalidateSize(); |
| if (rightMap) rightMap.invalidateSize(); |
| }, 100); |
| } |
| }); |
| </script> |
| </body> |
| </html> |