Reputation: 8942
I am using React Leaflet to render Leaflet map and its GeoJSON component to render polygons. I am trying to implement dragging multiple polygons together at once, as a group.
I added Leaflet.Path.Drag library and tried to reuse this code. I am able to get transformation matrix which is in parent's state. If I want to apply this matrix to multiple polygons with _transform
method, it doesn't work. I think the reason is that matrix is not applied to correct layers, but I have no idea how to fix this.
App.js
import React from "react";
import { MapContainer, GeoJSON, TileLayer } from "react-leaflet";
import { geoJson, latLngBounds } from "leaflet";
import "./styles.css";
import "leaflet/dist/leaflet.css";
import { GeoJsonContainer } from "./GeoJsonContainer";
const objects = [
{
polygon: {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {},
geometry: {
type: "Polygon",
coordinates: [
[
[-104.98569488525392, 39.63431579014969],
[-104.98569488525392, 39.64165260123419],
[-104.97161865234376, 39.64165260123419],
[-104.97161865234376, 39.63431579014969]
]
]
}
}
]
}
},
{
polygon: {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {},
geometry: {
type: "Polygon",
coordinates: [
[
[-105.02964019775392, 39.6206315500488],
[-105.02964019775392, 39.65685252543906],
[-104.99067306518556, 39.65685252543906],
[-104.99067306518556, 39.6206315500488]
]
]
}
}
]
}
}
];
const getPolygonPointFromBounds = (latLngBounds) => {
const center = latLngBounds.getCenter();
const latlngs = [];
latlngs.push(latLngBounds.getSouthWest()); //bottom left
latlngs.push({ lat: latLngBounds.getSouth(), lng: center.lng }); //bottom center
latlngs.push(latLngBounds.getSouthEast()); //bottom right
latlngs.push({ lat: center.lat, lng: latLngBounds.getEast() }); // center right
latlngs.push(latLngBounds.getNorthEast()); //top right
latlngs.push({
lat: latLngBounds.getNorth(),
lng: latLngBounds.getCenter().lng
}); //top center
latlngs.push(latLngBounds.getNorthWest()); //top left
latlngs.push({
lat: latLngBounds.getCenter().lat,
lng: latLngBounds.getWest()
}); //center left
return latlngs;
};
export default function App() {
const [matrix, setMatrix] = React.useState(null);
let newBounds = [];
let selectBoundingBox = [];
objects.forEach((building) => {
const polygonBounds = geoJson(building.polygon).getBounds();
newBounds = [...newBounds, polygonBounds];
});
const polygonPoints = getPolygonPointFromBounds(latLngBounds(newBounds));
const convertedData = polygonPoints.map((point) => [point.lng, point.lat]);
convertedData.push([polygonPoints[0].lng, polygonPoints[0].lat]);
selectBoundingBox = convertedData;
let selectBoxData = null;
if (selectBoundingBox) {
selectBoxData = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {},
geometry: {
type: "Polygon",
coordinates: [selectBoundingBox]
}
}
]
};
}
const handleFeature = (layer) => {
layer.makeDraggable();
layer.dragging.enable();
layer.on("drag", function (e) {
setMatrix(layer.dragging._matrix);
});
};
return (
<MapContainer center={[39.63563779557324, -104.99234676361085]} zoom={12}>
<TileLayer
attribution='&copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.osm.org/{z}/{x}/{y}.png"
/>
{objects.map((object, i) => (
<GeoJsonContainer data={object} key={i} matrix={matrix} />
))}
<GeoJSON
data={selectBoxData}
style={() => ({
color: "green",
weight: 3,
opacity: 0.5
})}
draggable={true}
onEachFeature={(feature, layer) => handleFeature(layer)}
></GeoJSON>
</MapContainer>
);
}
GeoJsonContainer.js
import React from "react";
import { GeoJSON } from "react-leaflet";
require("leaflet-path-drag");
export const GeoJsonContainer = (props) => {
const geoJSONRef = React.useRef(null);
const layerRef = React.useRef(null);
React.useEffect(() => {
if (geoJSONRef?.current?._layers) {
console.log("mount layers", geoJSONRef.current?._layers);
}
}, []);
React.useEffect(() => {
if (geoJSONRef?.current._layers && props.matrix) {
console.log("transform layers", geoJSONRef.current._layers);
const key = Object.keys(geoJSONRef.current._layers)[0];
const geoJSONElementLayer = geoJSONRef.current._layers[key];
if (geoJSONElementLayer) {
console.log("geoJSONElementLayer", geoJSONElementLayer);
console.log("layerRef.current", layerRef.current);
geoJSONElementLayer._transform(props.matrix);
layerRef.current._transform(props.matrix);
}
}
}, [props.matrix]);
const handleFeature = (layer) => {
console.log("handleFeature layer", layer);
layerRef.current = layer;
layer.makeDraggable();
layer.dragging.enable();
};
return (
<GeoJSON
ref={geoJSONRef}
data={props.data.polygon}
style={() => ({
color: "#3388ff",
weight: 3,
opacity: 1
})}
dragging={true}
onEachFeature={(feature, layer) => handleFeature(layer)}
></GeoJSON>
);
};
Upvotes: 0
Views: 1308
Reputation: 59338
Regarding
If I want to apply this matrix to multiple polygons with _transform method, it doesn't work
This is the expected behavior in React since matrix
prop needs to be immutable meaning a new array needs to be passed each time there is a change:
layer.on("drag", function (e) {
setMatrix([...layer.dragging._matrix]);
});
instead of:
layer.on("drag", function (e) {
setMatrix(layer.dragging._matrix);
});
This way GeoJsonContainer
component should get re-rendered as expected.
Another matter concerns Leaflet.Path.Drag
plugin, according to the referenced thread, in fact both drop
and dropend
events needs to be captured to properly apply transformation, so maybe instead of matrix
prop, introduce a transform
prop to keep matrix array and a flag to determine whether drop
or is dropend
event is triggered:
const handleFeature = (layer) => {
layer.makeDraggable();
layer.dragging.enable();
layer.on("drag", function (e) {
setTransform({matrix: layer.dragging._matrix, "end": false});
});
layer.on("dragend", function (e) {
setTransform({matrix: layer.dragging._matrix, "end": true});
});
};
and pass it into GeoJsonContainer
component to apply geometry transform:
React.useEffect(() => {
if (props.transform) {
geoJSONRef.current.eachLayer((layer) => {
if (props.transform.end) dragDropTransform(layer);
else __dragTransform(layer);
});
}
}, [props.transform]);
where
function __dragTransform(layer) {
layer._transform(props.transform.matrix);
}
function dragDropTransform(layer) {
layer.dragging._transformPoints(props.transform.matrix);
layer._updatePath();
layer._project();
layer._transform(null);
}
A possible improvement(s):
GeoJSON
and apply a style per geometry?Solution improvements proposal
In the provided example two layers are instantiated:
in App
component
GeoJSON
layer which renders a single outer geometry (polygon)and in GeoJsonContainer
component another one
GeoJSON
layer which in turn renders a two inner geometries (polygons)How about to merge both GeoJSON objects, something like this:
const dataSource = {...selectBoxData,...objects[0],...objects[1]}
and create a single layer instead:
<GeoJSON data={dataSource}></GeoJSON>
This way twice initialization for dragable layers could be avoided (refactoring of duplicated code)
Upvotes: 1