Spaces:
Running
Running
| "use client"; | |
| import React, { useEffect, useRef } from 'react'; | |
| import { MapContainer, useMap } from 'react-leaflet'; | |
| import L from 'leaflet'; | |
| import 'leaflet/dist/leaflet.css'; | |
| // Inject CSS for leaflet-side-by-side dynamically to prevent Next.js / Turbopack importing errors | |
| const sideBySideStyles = ` | |
| .leaflet-sbs-range { | |
| position: absolute; | |
| top: 50%; | |
| width: 100%; | |
| z-index: 999; | |
| -webkit-appearance: none; | |
| display: inline-block!important; | |
| vertical-align: middle; | |
| height: 0; | |
| padding: 0; | |
| margin: 0; | |
| border: 0; | |
| background: rgba(0, 0, 0, 0.25); | |
| min-width: 100px; | |
| cursor: pointer; | |
| pointer-events: none; | |
| } | |
| .leaflet-sbs-divider { | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| left: 50%; | |
| margin-left: -2px; | |
| width: 4px; | |
| background-color: #fff; | |
| pointer-events: none; | |
| z-index: 999; | |
| } | |
| .leaflet-sbs-range::-ms-fill-upper { | |
| background: transparent; | |
| } | |
| .leaflet-sbs-range::-ms-fill-lower { | |
| background: rgba(255, 255, 255, 0.25); | |
| } | |
| .leaflet-sbs-range::-moz-range-track { | |
| opacity: 0; | |
| } | |
| .leaflet-sbs-range::-ms-track { | |
| opacity: 0; | |
| } | |
| .leaflet-sbs-range::-ms-tooltip { | |
| display: none; | |
| } | |
| .leaflet-sbs-range::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| margin: 0; | |
| padding: 0; | |
| background: #fff; | |
| height: 40px; | |
| width: 40px; | |
| border-radius: 20px; | |
| cursor: ew-resize; | |
| pointer-events: auto; | |
| border: 1px solid #ddd; | |
| background-position: 50% 50%; | |
| background-repeat: no-repeat; | |
| background-size: 40px 40px; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.3); | |
| } | |
| .leaflet-sbs-range::-ms-thumb { | |
| margin: 0; | |
| padding: 0; | |
| background: #fff; | |
| height: 40px; | |
| width: 40px; | |
| border-radius: 20px; | |
| cursor: ew-resize; | |
| pointer-events: auto; | |
| border: 1px solid #ddd; | |
| background-position: 50% 50%; | |
| background-repeat: no-repeat; | |
| background-size: 40px 40px; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.3); | |
| } | |
| .leaflet-sbs-range::-moz-range-thumb { | |
| padding: 0; | |
| right: 0; | |
| background: #fff; | |
| height: 40px; | |
| width: 40px; | |
| border-radius: 20px; | |
| cursor: ew-resize; | |
| pointer-events: auto; | |
| border: 1px solid #ddd; | |
| background-position: 50% 50%; | |
| background-repeat: no-repeat; | |
| background-size: 40px 40px; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.3); | |
| } | |
| .leaflet-sbs-range:disabled::-moz-range-thumb { | |
| cursor: default; | |
| } | |
| .leaflet-sbs-range:disabled::-ms-thumb { | |
| cursor: default; | |
| } | |
| .leaflet-sbs-range:disabled::-webkit-slider-thumb { | |
| cursor: default; | |
| } | |
| .leaflet-sbs-range:disabled { | |
| cursor: default; | |
| } | |
| .leaflet-sbs-range:focus { | |
| outline: none!important; | |
| } | |
| .leaflet-sbs-range::-moz-focus-outer { | |
| border: 0; | |
| } | |
| `; | |
| function createSideBySide() { | |
| let mapWasDragEnabled: boolean; | |
| let mapWasTapEnabled: boolean; | |
| function getRangeEvent(rangeInput: HTMLInputElement) { | |
| return 'oninput' in rangeInput ? 'input' : 'change'; | |
| } | |
| function cancelMapDrag(this: any) { | |
| mapWasDragEnabled = this._map.dragging.enabled(); | |
| mapWasTapEnabled = this._map.tap && this._map.tap.enabled(); | |
| this._map.dragging.disable(); | |
| this._map.tap && this._map.tap.disable(); | |
| } | |
| function uncancelMapDrag(this: any, e: any) { | |
| this._refocusOnMap(e); | |
| if (mapWasDragEnabled) { | |
| this._map.dragging.enable(); | |
| } | |
| if (mapWasTapEnabled) { | |
| this._map.tap.enable(); | |
| } | |
| } | |
| function asArray(arg: any) { | |
| return (arg === undefined || arg === 'undefined') ? [] : Array.isArray(arg) ? arg : [arg]; | |
| } | |
| const SideBySide = L.Control.extend({ | |
| includes: L.Evented.prototype, | |
| options: { | |
| thumbSize: 42, | |
| padding: 0 | |
| }, | |
| initialize: function (this: any, leftLayers: any, rightLayers: any, options: any) { | |
| this.setLeftLayers(leftLayers); | |
| this.setRightLayers(rightLayers); | |
| L.setOptions(this, options); | |
| }, | |
| getPosition: function (this: any) { | |
| const rangeValue = this._range.value; | |
| const offset = (0.5 - rangeValue) * (2 * this.options.padding + this.options.thumbSize); | |
| return this._map.getSize().x * rangeValue + offset; | |
| }, | |
| setPosition: function () {}, | |
| addTo: function (this: any, map: any) { | |
| this.remove(); | |
| this._map = map; | |
| const container = this._container = L.DomUtil.create('div', 'leaflet-sbs', map._controlContainer); | |
| this._divider = L.DomUtil.create('div', 'leaflet-sbs-divider', container); | |
| const range = this._range = L.DomUtil.create('input', 'leaflet-sbs-range', container); | |
| range.type = 'range'; | |
| range.min = '0'; | |
| range.max = '1'; | |
| range.step = 'any'; | |
| range.value = '0.5'; | |
| range.style.paddingLeft = range.style.paddingRight = this.options.padding + 'px'; | |
| this._addEvents(); | |
| this._updateLayers(); | |
| return this; | |
| }, | |
| remove: function (this: any) { | |
| if (!this._map) { | |
| return this; | |
| } | |
| const getLayerElement = (layer: any) => { | |
| if (!layer) return null; | |
| if (typeof layer.getContainer === 'function') return layer.getContainer(); | |
| if (typeof layer.getElement === 'function') return layer.getElement(); | |
| return null; | |
| }; | |
| const leftEl = getLayerElement(this._leftLayer); | |
| if (leftEl) leftEl.style.clip = ''; | |
| const rightEl = getLayerElement(this._rightLayer); | |
| if (rightEl) rightEl.style.clip = ''; | |
| this._removeEvents(); | |
| L.DomUtil.remove(this._container); | |
| this._map = null; | |
| return this; | |
| }, | |
| setLeftLayers: function (this: any, leftLayers: any) { | |
| this._leftLayers = asArray(leftLayers); | |
| this._updateLayers(); | |
| return this; | |
| }, | |
| setRightLayers: function (this: any, rightLayers: any) { | |
| this._rightLayers = asArray(rightLayers); | |
| this._updateLayers(); | |
| return this; | |
| }, | |
| _updateClip: function (this: any) { | |
| const map = this._map; | |
| if (!map) return; | |
| const nw = map.containerPointToLayerPoint([0, 0]); | |
| const se = map.containerPointToLayerPoint(map.getSize()); | |
| const clipX = nw.x + this.getPosition(); | |
| const dividerX = this.getPosition(); | |
| this._divider.style.left = dividerX + 'px'; | |
| this.fire('dividermove', { x: dividerX }); | |
| const clipLeft = 'rect(' + [nw.y, clipX, se.y, nw.x].join('px,') + 'px)'; | |
| const clipRight = 'rect(' + [nw.y, se.x, se.y, clipX].join('px,') + 'px)'; | |
| const getLayerElement = (layer: any) => { | |
| if (!layer) return null; | |
| if (typeof layer.getContainer === 'function') return layer.getContainer(); | |
| if (typeof layer.getElement === 'function') return layer.getElement(); | |
| return null; | |
| }; | |
| const leftEl = getLayerElement(this._leftLayer); | |
| if (leftEl) leftEl.style.clip = clipLeft; | |
| const rightEl = getLayerElement(this._rightLayer); | |
| if (rightEl) rightEl.style.clip = clipRight; | |
| }, | |
| _updateLayers: function (this: any) { | |
| if (!this._map) { | |
| return this; | |
| } | |
| const prevLeft = this._leftLayer; | |
| const prevRight = this._rightLayer; | |
| this._leftLayer = this._rightLayer = null; | |
| this._leftLayers.forEach(function (this: any, layer: any) { | |
| if (this._map.hasLayer(layer)) { | |
| this._leftLayer = layer; | |
| } | |
| }, this); | |
| this._rightLayers.forEach(function (this: any, layer: any) { | |
| if (this._map.hasLayer(layer)) { | |
| this._rightLayer = layer; | |
| } | |
| }, this); | |
| if (prevLeft !== this._leftLayer) { | |
| prevLeft && this.fire('leftlayerremove', { layer: prevLeft }); | |
| this._leftLayer && this.fire('leftlayeradd', { layer: this._leftLayer }); | |
| } | |
| if (prevRight !== this._rightLayer) { | |
| prevRight && this.fire('rightlayerremove', { layer: prevRight }); | |
| this._rightLayer && this.fire('rightlayeradd', { layer: this._rightLayer }); | |
| } | |
| this._updateClip(); | |
| }, | |
| _addEvents: function (this: any) { | |
| const range = this._range; | |
| const map = this._map; | |
| if (!map || !range) return; | |
| map.on("move", this._updateClip, this); | |
| map.on("layeradd layerremove", this._updateLayers, this); | |
| L.DomEvent.on(range, getRangeEvent(range), this._updateClip, this); | |
| L.DomEvent.on(range, "touchstart", cancelMapDrag, this); | |
| L.DomEvent.on(range, "touchend", uncancelMapDrag, this); | |
| L.DomEvent.on(range, "mousedown", cancelMapDrag, this); | |
| L.DomEvent.on(range, "mouseup", uncancelMapDrag, this); | |
| }, | |
| _removeEvents: function (this: any) { | |
| const range = this._range; | |
| const map = this._map; | |
| if (range) { | |
| L.DomEvent.off(range, getRangeEvent(range), this._updateClip, this); | |
| L.DomEvent.off(range, "touchstart", cancelMapDrag, this); | |
| L.DomEvent.off(range, "touchend", uncancelMapDrag, this); | |
| L.DomEvent.off(range, "mousedown", cancelMapDrag, this); | |
| L.DomEvent.off(range, "mouseup", uncancelMapDrag, this); | |
| } | |
| if (map) { | |
| map.off("layeradd layerremove", this._updateLayers, this); | |
| map.off("move", this._updateClip, this); | |
| } | |
| }, | |
| }); | |
| return (leftLayers: any, rightLayers: any, options?: any) => { | |
| // @ts-ignore | |
| return new SideBySide(leftLayers, rightLayers, options); | |
| }; | |
| } | |
| interface LeafletCompareMapProps { | |
| leftUrl: string; | |
| rightUrl: string; | |
| bounds?: [number, number, number, number]; | |
| } | |
| function SideBySideControl({ leftUrl, rightUrl, bounds }: { leftUrl: string, rightUrl: string, bounds: L.LatLngBoundsExpression }) { | |
| const map = useMap(); | |
| const controlRef = useRef<any>(null); | |
| useEffect(() => { | |
| // Dynamically inject styles into document head | |
| let styleTag = document.getElementById('leaflet-sbs-styles'); | |
| if (!styleTag) { | |
| styleTag = document.createElement('style'); | |
| styleTag.id = 'leaflet-sbs-styles'; | |
| styleTag.innerHTML = sideBySideStyles; | |
| document.head.appendChild(styleTag); | |
| } | |
| // @ts-ignore | |
| map.fitBounds(L.latLngBounds(bounds)); | |
| const leftLayer = L.imageOverlay(leftUrl, bounds, { opacity: 1, crossOrigin: "anonymous" }).addTo(map); | |
| const rightLayer = L.imageOverlay(rightUrl, bounds, { opacity: 1, crossOrigin: "anonymous" }).addTo(map); | |
| const sideBySideFn = createSideBySide(); | |
| controlRef.current = sideBySideFn(leftLayer, rightLayer).addTo(map); | |
| return () => { | |
| if (controlRef.current) { | |
| map.removeControl(controlRef.current); | |
| } | |
| map.removeLayer(leftLayer); | |
| map.removeLayer(rightLayer); | |
| }; | |
| }, [map, leftUrl, rightUrl, bounds]); | |
| return null; | |
| } | |
| import { TileLayer } from 'react-leaflet'; | |
| export default function LeafletCompareMap({ leftUrl, rightUrl, bounds }: LeafletCompareMapProps) { | |
| const isValidBounds = bounds && Array.isArray(bounds) && bounds.length === 4 && bounds.every(n => typeof n === 'number' && !isNaN(n)); | |
| const mapBounds: L.LatLngBoundsExpression = isValidBounds | |
| ? [[bounds[0], bounds[1]], [bounds[2], bounds[3]]] | |
| : [[8.4, 68.7], [37.6, 97.25]]; | |
| return ( | |
| <MapContainer | |
| bounds={mapBounds} | |
| className="w-full h-full z-0 bg-[#0a0a0a]" | |
| zoomControl={true} | |
| minZoom={2} | |
| maxZoom={12} | |
| style={{ height: '100%', width: '100%', minHeight: '500px' }} | |
| > | |
| <TileLayer | |
| url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" | |
| attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>' | |
| /> | |
| <SideBySideControl leftUrl={leftUrl} rightUrl={rightUrl} bounds={mapBounds} /> | |
| </MapContainer> | |
| ); | |
| } |