react-map-gl
react-map-gl copied to clipboard
Dynamically changing the layer order is not possible
Hello, In my use case I have a list of layers whose order can be messed up with. I made this sandbox to test the behaviour of inverting the second and third items in a list of four. The second item is put at the top of the layering.
Reproduce the bug: https://codesandbox.io/s/shy-waterfall-fwky3
I tried adding the array index in the layer key but the third layer goes to the top instead of bellow the forth. Maybe this is because the forth layer's key didn't change, so it is not kept above all other layers ?
I also tried using a random key for the layers. This eliminates the order issue but might be bad for performance. In the second sandbox I tried to dynamically change the key of the layers only after a change in order and not on every render.
Expected behaviour: https://codesandbox.io/s/brave-firefly-zr9oc
If a fix might break other use cases, maybe a new prop to the Layer component that specify we want to keep the order could be possible?
Thanks
You should be able to reorder by changing the beforeId
prop.
I personally find this a very minimal solution that doesn't really work if you want to switch layers in and out dynamically. Now all of a sudden my layer components need to know of the state of all other layers, weither they're shown or not, etc... I know this is what's exposed by mapbox, but is there another way to do ordering properly, through key for example?
I used a different way of fixing this for my specific use case:
- Create some layers that are always sorted relative to each other. They shouldn't have any content, so what I do is use the 'background' type and set visibility to 'none', e.g.:
['baselayer', 'some_other_layer', 'yet_another_one'].map(name => {
return (<Layer
id={'GROUP_' + name}
type='background'
layout={{ visibility: 'none' }}
paint={{}}
/>)
})
- For your dynamic layers, you sort assign them as a 'child' layer by setting their
beforeId
, e.g.:
<Source scheme='tms' type='raster' tiles={[url]}>
<Layer beforeId={'GROUP_' + nameOfLayerToAssignTo}
type='raster'
layout={{ }}
paint={{ }}
/>
</Source>
It seems that you can now re-assign the beforeId to re-sort the layers. If you want some sort of z-indexing you could create a 100 group layers statically ('z0' to 'z100') and assign the dynamic layers with the beforeId 'z'+zIndex
. I don't know how much this impacts performance though.
thanks @yurivangeffen - this z-index framework with the empty layers worked well for me.
Adding a different solution as the above solutions were producing flickering between reordering the layer object.
useEffect(() => {
if (mapRef) {
const map = mapRef.current.getMap()
if (map.loaded() && topLayer !== null) {
map.moveLayer(topLayer)
}
}
}, [topLayer]);
Hi there, checking in to see if there is a better way today to achieve this - layer ordering based on position in dom/layer list state. Would make it easier to have a re-orderable layer list in the form of a tree view, ala qgis, gimp or any layer based gis/drawings/etc app.
Currently getting bit by this as well, requires extra record keeping. Fun :(
The z-index/beforeId technique is sometimes not a solution. At our company we have third-party components unaware of each others, for background data, for live data, for drawing shapes, etc. They cannot have the beforeId prop to be the layer id of another component they dont know about.
I had a similar issue, we allow users to move layers around on the map. This is the solution I came up with so far, it does mess with beforeId
and could be improved, but should handle the general situation:
import isEqual from 'lodash/isEqual';
import React, { useEffect, useRef, useState } from 'react';
import { useMap } from 'react-map-gl';
function recursiveMap(children: any, fn: (element: React.ReactElement) => void) {
return React.Children.map(children, (child) => {
if (!React.isValidElement(child)) {
return child;
}
if (child.props['children']) {
child = React.cloneElement(child, {
children: recursiveMap(child.props['children'], fn),
} as any);
}
return fn(child);
});
}
/**
* This manager keeps track of all the mapbox gl layers on the stack where the
* top most layer is the layer that at the end of the list, keeping it in sync with
* the concept of mapbox gl (the last layer pushed on the stack is top most).
* It is ill advised to use beforeId along with this behavior as it might not interact
* properly.
* Usage:
* ```
* <Map>
* <LayerOrganizationManager>
* // Sources and layers
* </LayerOrganizationManager>
* </Map>
* ```
*/
export default function LayerOrganizationManager({
children,
layerToStackBeneath = 'building',
}: {
children: any;
/**
* The layer to stack all layers beneath this stack as, if you adjust and
* have draw tools enabled, it can result in the drawing tools being behind
* your layers, so keep that in mind.
*/
layerToStackBeneath?: string;
}) {
const { current: map } = useMap();
const layers: string[] = [];
const layerIdsRef = useRef<string[]>(layers);
const [mapLoaded, setMapLoaded] = useState(false);
recursiveMap(children, (e) => {
if (e.type['name'] === 'Layer') {
layers.push(e.props.id);
}
});
useEffect(() => {
map.once('load', () => setMapLoaded(true));
}, []);
if (!isEqual(layerIdsRef.current, layers) && mapLoaded) {
// Timeout makes sure that the layers actually get added to the map before
// moving
setTimeout(() => {
layers.forEach((id) => {
const layer = map.getLayer(id);
if (!layer) return;
map.moveLayer(id, layerToStackBeneath);
});
}, 0);
layerIdsRef.current = layers;
}
return <>{children}</>;
}
+1 - would love for this to have a proper solution in react-map-gl. We currently show all possible layers with visibility turned off for the ones that have been toggled off.