react-map-gl icon indicating copy to clipboard operation
react-map-gl copied to clipboard

Dynamically changing the layer order is not possible

Open adrienj opened this issue 5 years ago • 10 comments

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

adrienj avatar Nov 13 '19 09:11 adrienj

You should be able to reorder by changing the beforeId prop.

Pessimistress avatar Jan 06 '20 23:01 Pessimistress

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?

yurivangeffen avatar Apr 29 '20 13:04 yurivangeffen

I used a different way of fixing this for my specific use case:

  1. 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={{}}
  />)
})
  1. 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.

yurivangeffen avatar May 07 '20 14:05 yurivangeffen

thanks @yurivangeffen - this z-index framework with the empty layers worked well for me.

maxwell-oroark avatar Mar 22 '21 19:03 maxwell-oroark

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]);

eastcoasting avatar Aug 08 '21 20:08 eastcoasting

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.

jo-chemla avatar Feb 22 '23 20:02 jo-chemla

Currently getting bit by this as well, requires extra record keeping. Fun :(

zbyte64 avatar Mar 30 '23 20:03 zbyte64

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.

adrienj avatar Mar 31 '23 06:03 adrienj

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}</>;
}

psusmars avatar Apr 19 '23 21:04 psusmars

+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.

davidemerritt avatar Mar 04 '24 21:03 davidemerritt