Download split_panel_map.html from zh4rif/forestweb: direct link, hf CLI and curl.
- Browser
- Download file 22.5 kB
-
https://huggingface.co/spaces/zh4rif/forestweb/resolve/main/split_panel_map.html
- Command line
-
hf download hf://spaces/zh4rif/forestweb/split_panel_map.html
-
curl -L -o split_panel_map.html https://huggingface.co/spaces/zh4rif/forestweb/resolve/main/split_panel_map.html
22.5 kB
| <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> | |
| // Configuration | |
| const DEFAULT_LOCATION = [40.7128, -74.0060]; // New York City | |
| const DEFAULT_ZOOM = 10; | |
| const SEARCH_ZOOM = 12; | |
| const MIN_SLIDER_POSITION = 15; // 15% from left | |
| const MAX_SLIDER_POSITION = 85; // 85% from left | |
| // Map layer definitions | |
| 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' | |
| } | |
| }; | |
| // Global variables | |
| let leftMap, rightMap; | |
| let leftLayer, rightLayer; | |
| let isSynced = true; | |
| let mapSyncLock = false; | |
| let isSliderActive = false; | |
| let searchMarkers = []; | |
| // UI elements | |
| 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') | |
| }; | |
| // Utility functions | |
| 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; | |
| } | |
| } | |
| // Map initialization | |
| function initializeMaps() { | |
| try { | |
| // Initialize left map | |
| leftMap = L.map('leftMap', { | |
| zoomControl: false, | |
| attributionControl: true | |
| }).setView(DEFAULT_LOCATION, DEFAULT_ZOOM); | |
| // Initialize right map | |
| rightMap = L.map('rightMap', { | |
| zoomControl: false, | |
| attributionControl: true | |
| }).setView(DEFAULT_LOCATION, DEFAULT_ZOOM); | |
| // Add zoom controls | |
| L.control.zoom({ | |
| position: 'topright' | |
| }).addTo(leftMap); | |
| L.control.zoom({ | |
| position: 'topright' | |
| }).addTo(rightMap); | |
| // Add initial tile layers | |
| 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); | |
| // Set up map event listeners | |
| setupMapEvents(); | |
| // Initial coordinate update | |
| 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.'); | |
| } | |
| } | |
| // Map event setup | |
| 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); | |
| } | |
| }); | |
| // Handle map loading errors | |
| leftMap.on('tileerror', (e) => { | |
| console.warn('Left map tile error:', e); | |
| }); | |
| rightMap.on('tileerror', (e) => { | |
| console.warn('Right map tile error:', e); | |
| }); | |
| } | |
| // Coordinates update | |
| 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}`; | |
| } | |
| // Map synchronization | |
| 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); | |
| } | |
| // Release lock after a short delay | |
| setTimeout(() => { | |
| mapSyncLock = false; | |
| }, 100); | |
| } | |
| // Slider functionality - Now with overlapping panels | |
| let isSliderDragging = false; | |
| let startMouseX = 0; | |
| let startSliderPosition = 50; | |
| function updateSliderPosition(positionPercentage) { | |
| // Clamp between MIN_SLIDER_POSITION and MAX_SLIDER_POSITION | |
| positionPercentage = Math.max(MIN_SLIDER_POSITION, Math.min(MAX_SLIDER_POSITION, positionPercentage)); | |
| // Move the slider | |
| elements.sliderContainer.style.left = `${positionPercentage}%`; | |
| // Update clip-path for overlapping effect | |
| 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); | |
| // Get current slider position as percentage | |
| const containerRect = elements.mapContainer.getBoundingClientRect(); | |
| const sliderRect = elements.sliderContainer.getBoundingClientRect(); | |
| startSliderPosition = ((sliderRect.left - containerRect.left) / containerRect.width) * 100; | |
| // Add visual feedback | |
| elements.sliderContainer.classList.add('dragging'); | |
| elements.sliderHandle.classList.add('dragging'); | |
| // Add global event listeners | |
| document.addEventListener('mousemove', onSliderDrag, { passive: false }); | |
| document.addEventListener('mouseup', stopSliderDrag); | |
| document.addEventListener('touchmove', onSliderDrag, { passive: false }); | |
| document.addEventListener('touchend', stopSliderDrag); | |
| // Prevent text selection and set cursor | |
| 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; | |
| // Remove visual feedback | |
| elements.sliderContainer.classList.remove('dragging'); | |
| elements.sliderHandle.classList.remove('dragging'); | |
| // Remove global event listeners | |
| document.removeEventListener('mousemove', onSliderDrag); | |
| document.removeEventListener('mouseup', stopSliderDrag); | |
| document.removeEventListener('touchmove', onSliderDrag); | |
| document.removeEventListener('touchend', stopSliderDrag); | |
| // Reset cursor and selection | |
| document.body.style.cursor = ''; | |
| document.body.style.userSelect = ''; | |
| // Reset slider active state after a short delay | |
| setTimeout(() => { | |
| isSliderActive = false; | |
| }, 300); | |
| } | |
| // Layer switching | |
| 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; | |
| } | |
| } | |
| // Search functionality | |
| 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'); | |
| } | |
| // Clear existing markers | |
| clearSearchMarkers(); | |
| // Add markers to both maps | |
| 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 }); | |
| // Move maps to location | |
| 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; | |
| } | |
| } | |
| // Event listeners setup | |
| function setupEventListeners() { | |
| // Slider events | |
| elements.sliderContainer.addEventListener('mousedown', startSliderDrag); | |
| elements.sliderHandle.addEventListener('mousedown', startSliderDrag); | |
| elements.sliderContainer.addEventListener('touchstart', startSliderDrag); | |
| elements.sliderHandle.addEventListener('touchstart', startSliderDrag); | |
| // Prevent context menu on slider | |
| elements.sliderContainer.addEventListener('contextmenu', (e) => e.preventDefault()); | |
| elements.sliderHandle.addEventListener('contextmenu', (e) => e.preventDefault()); | |
| // Sync toggle | |
| elements.syncToggle.addEventListener('click', () => { | |
| isSynced = !isSynced; | |
| elements.syncToggle.classList.toggle('active', isSynced); | |
| if (isSynced && leftMap && rightMap) { | |
| syncMaps(leftMap, rightMap); | |
| } | |
| }); | |
| // Layer switching | |
| 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(); | |
| } | |
| }); | |
| // Search functionality | |
| 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 resize | |
| window.addEventListener('resize', () => { | |
| setTimeout(() => { | |
| if (leftMap) leftMap.invalidateSize(); | |
| if (rightMap) rightMap.invalidateSize(); | |
| }, 300); | |
| }); | |
| // Error message click to dismiss | |
| elements.errorMessage.addEventListener('click', () => { | |
| elements.errorMessage.style.display = 'none'; | |
| }); | |
| } | |
| // Initialize everything when DOM is ready | |
| document.addEventListener('DOMContentLoaded', () => { | |
| console.log('DOM loaded, initializing application...'); | |
| // Set up event listeners first | |
| setupEventListeners(); | |
| // Initialize maps after a short delay | |
| setTimeout(() => { | |
| initializeMaps(); | |
| updatePanelLabels(); | |
| }, 100); | |
| }); | |
| // Handle page visibility changes | |
| document.addEventListener('visibilitychange', () => { | |
| if (!document.hidden) { | |
| // Page became visible, invalidate map sizes | |
| setTimeout(() => { | |
| if (leftMap) leftMap.invalidateSize(); | |
| if (rightMap) rightMap.invalidateSize(); | |
| }, 100); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |