Fill-the-Frames / frontend /src /features /validation /components /leaflet-compare-map.tsx
sid385's picture
fix(frontend): restore map zoom capabilities and fix slider visibility
8e5d16f
Raw
History Blame Contribute Delete
11.8 kB
"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='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
/>
<SideBySideControl leftUrl={leftUrl} rightUrl={rightUrl} bounds={mapBounds} />
</MapContainer>
);
}