Updating map layers visibility in react-map-gl

155 views Asked by At

I am trying to learn how to use 'react-map-gl/maplibre' layer hide/show functionality and the issue I am facing is with updating both layer and paint properties of my map instance.

I am very new to the web map apps and sure miss something very basic here;( Is that even possible to update a map properties without passing down the whole style object on each change? I assume mapRef.current is an API of an initialized Map instance which allows control of both symbols and layers, but why is it missing set methods? Why non-react examples can perfectly use sets?

Here is the detailed issue: Calling to setLayoutProperty when the map is loaded fails with

"setLayoutProperty is not a function"

On the other hand, getLayoutProperty works fine and can retrieve "visibility" property.

Here is how I define my map :

"use client";
import { useCallback, useRef, useState } from "react";
import { Map, Source, Layer, type MapRef } from "react-map-gl/maplibre";

export default function MapComponent() {

  // I suspect that this is where I am presumably wrong.
  // I assume that 'maplibregl' type Map is the same as 'react-map-gl/maplibre' Map component ref={mapRef}
  // This is where I try to declare my mapRef following answer
  // from here https://stackoverflow.com/questions/68368898/typescript-type-for-mapboxgljs-react-ref-hook
  // const mapRef = useRef<maplibregl.Map | null>(null);
  //
  // trying other approach, still same error, but now it is clear that those methods do not exist from intellisense
  const mapRef = useRef<MapRef>(null);
  
  const sourceId = "places";
  const circleLayerId = "circles";
  const symbolLayerId = "symbols";
  const visibility = "visibility";
  const circleRadius = "circle-radius";
  
  return (
    <>
      <Map
        reuseMaps
        {...viewport}
        ref={mapRef}
        style={{ width: "100vw", height: "100vh", display: "flex" }}
        mapStyle={
          "https://api.maptiler.com/maps/basic-v2/style.json?key=xxxxxxxx"
        }
      >
        <Source
          id={sourceId}
          type="geojson"
          maxzoom={15}
          data="https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson"
        >
          <Layer
            {...{
              source: sourceId,
              id: circleLayerId,
              type: "circle",
              paint: {
                "circle-color": "#ff0000",
                "circle-radius": 12,
                "circle-stroke-width": 1,
                "circle-stroke-color": "#000",
              },
              layout: {
                visibility: "visible",
              },
            }}
          />
          <Layer
            {...{
              id: symbolLayerId,
              type: "symbol",
              source: sourceId,
              layout: {
                visibility: "visible",
                "text-allow-overlap": true,
                "text-font": ["Arial Italic"],
                "text-field": ["get", "mag"],
                "text-size": [
                  "interpolate",
                  ["linear"],
                  ["zoom"],
                  // zoom is 5 (or less)
                  5,
                  12,
                  // zoom is 10 (or greater)
                  10,
                  11,
                ],
                "text-anchor": "center",
                // "text-offset": [0, -2],
              },
            }}
          />
        </Source>
      </Map>
      
      <div
        style={{
          position: "absolute",
          display: "flex",
          zIndex: 100,
          width: "90%",
          justifyContent: "space-between",
          padding: 10,
        }}
      >
        <ul>
          <li>
            <span>
              <button
                onClick={(e) => {
                  handleVisibility(symbolLayerId, false);
                }}
              >
                Hide "Symbols" using visibility
              </button>
            </span>
          </li>
          <li>
            <span>
              <button
                onClick={(e) => {
                  handleVisibility(circleLayerId, false);
                }}
              >
                Hide "Circles" using visibility
              </button>
            </span>
          </li>
          <li>
            <span>
              <button onClick={(e) => handleRadius(circleLayerId, 0)}>
                Scale "Circles" radius to 0
              </button>
            </span>
          </li>
        </ul>
      </div>
    </>
  );
}

Reproduction in sandbox (just use your own token for tiles) https://codesandbox.io/p/github/muxalko/smokemap-ui/main

1

There are 1 answers

0
Michael Klimenko On

Instead of setLayoutProperty method, use state to control any desired property inside LayoutProps. For example:

Lets try create a state for symbol layer visibility. First, you would need to define a state:

// define state 
const [symbolLayerVisibility, setSymbolLayerVisibility] = useState("visible");

Then, use the defined state variable in Layer props:

// under layout, use our state variable for visibility
<Layer
  ...
              layout: {
                visibility: symbolLayerVisibility,
               ...
              }
 ...
          />

Now, we can control the visibility prop by changing symbolLayerVisibility state with any of the events like onClick, onHover, onChange , etc...

// For example, trigger state change with a button that will switch layer visibility on and off
<button 
    onClick={(e) => {
     setSymbolLayerVisibility(
                symbolLayerVisibility === "visible" ? "none": "visible",
              );
            }}>
            Hide/Show "Symbols" using visibility
</button>

Using this approach, I was able to successfully hide/show layers both by layer visibility and circle radius as well as control other params as colors and text size.

Sometimes, when you have a lot of layers, it becomes hard to manage states in separate useState hooks. This is how I manage param for multiple layers with a Map dictionary.

Define a map dict to hold values:

// indicate which category is visible
  const [categoriesSelectorMap, setCategoriesSelectorMap] = useState<
    Map<string, boolean>
  >(
    new Map([
      ["-1", false],
      ["1", false],
      ["2", false],
      ["3", false],
      ["4", false],
      ["5", false],
      ["6", false],
      ["7", false],
      ["8", false],
      ["9", false],
      ["10", false],
    ])
  );

Then, to use it, we just iterate over the array of Layers and fetch values from our state variables.

For example, component that render Layers may look like:

import { useState, Fragment } from "react";
import { Layer, SymbolLayer } from "react-map-gl/maplibre";

// This component will receive source id , list of categories and selector.
// will output two Layers per category (symbol and circle) with visibility set per selector.
export default function CategoryLayers({
  sourceLayerId,
  categories,
  selector,
}: {
  sourceLayerId: string;
  categories: CategoryType[];
  selector: Map<string, boolean>;
}): React.ReactElement<SymbolLayer> {

  const symbolLayerIdName = "symbol_name";
  const symbolLayerIdCategory = "symbol_category";
  const paintLayerIdCategory = "paint_category";
  
  //here is simple colors constant:
  const colors: Map<string, string> = new Map([
    ["-1", "#FFFFFF"],
    ["1", "#277551"],
    ["2", "#ebeb96"],
    ["3", "#00fe08"],
    ["4", "#2472a9"],
    ["5", "#de22f6"],
    ["6", "#8dbd8e"],
    ["7", "#96642f"],
    ["8", "#4869bd"],
    ["9", "#7ecece"],
    ["10", "#40c9b2"],
  ]);

  return (
    <>
      {categories.map((item: CategoryType) => (
        <Fragment key={item.id}>
          <Layer
            {...{
              id: paintLayerIdCategory + "_" + item.name,
              source: sourceLayerId,
              type: "circle",
              filter: ["==", ["get", "category"], Number(item.id)],
              paint: {
                "circle-color": colors.get(item.id),
                "circle-radius": selector.get(item.id) ? 10 : 0,
                "circle-stroke-width": 0,
                "circle-stroke-color": "#000",
              },
            }}
          />
          <Layer
            {...{
              id: symbolLayerIdCategory + "_" + item.name,
              type: "symbol",
              source: sourceLayerId,
              filter: ["==", ["get", "category"], Number(item.id)],
              layout: {
                visibility: selector.get(item.id) ? "visible" : "none",
                "text-allow-overlap": true,
                "text-font": ["Arial Italic"],
                "text-field": item.name,
                "text-size": [12],
                "text-anchor": "top",
              },
            }}
          />
        </Fragment>
      ))}
    </>
  );
}

Here is a little demo on YouTube: Updating map layers visibility in react-map-gl

Codesandbox with full solution