Search code examples
reactjsjsonpolygonreact-leaflet

How to identify, inside an onClick event handler, which out of many polygons plotted on the same map (using react-leaflet) was clicked?


Situation: I am plotting a country and all its state boundaries on a map using the react-leaflet. I am plotting each state boundary as a polygon (definitions are passed in a JSON file.) using the function . All the state boundary definition is passed in a single JSON file as different JSON objects. Each object has a unique id.

My Code:

import React from 'react'
import { MapContainer, TileLayer, GeoJSON } from 'react-leaflet'
import * as L from "leaflet";


const Map = (props) => {
let cordinates = [14.716, -14.467]       // Dispaly coordinates
return (
    // Cordinates of map 
    <MapContainer center={cordinates} zoom={7} scrollWheelZoom={false}>
         {/* Display map  */}
         <TileLayer
            attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
            url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
     {/* <GeoJSON id={Math.random()} data={country} /> */}

     {/* Highlited area in map  */}
        <GeoJSON data={props.state} onEachFeature={props.handleEachFeature} />
   </MapContainer> 
)
}

export default Map

I am passing the JSON file and handleEachFeature function(returning the whole JSON file in the console) as props.

What I want to do: When a user clicks on the map, I want to clear up the entire map and only plot the state within which the click was. Basically, the state will be zoomed and I also want to plot its district boundaries (I have definitions for the district boundaries for each state as well).

Approach I am taking: I am trying to capture the id associated with the polygon (corresponding to the state) that was clicked inside the onClick event. I can then erase the existing map and using the captured id I can plot the state (and its districts) clicked. However, no matter which state is clicked, the onClick event is returning me the whole data of all the polygons. Following is my code:

On click handleEachFeature function:

 function handleEachFeature(feature, layer) {
         layer.on("click", L.bind(handleClick, null, layer));
}

 // This is returning whole json file in console. But, I want only Polygon id on which user clicked. 
 function handleClick(e) {
    console.log(e);
}

Things I already tried:

  1. I used a single JSON file that contains multiple polygons. However, onClick event I get the whole JSON file, not any unique value to identify the polygon.
  2. I also tried using different JSON files for each polygon (state) and add them to the map one by one but got the same result.

Please suggest any approach using react-leaflet or some other library.


Solution

  • You can do this by storing the unique identifier (cartodb_id in the provided example) in a variable and then use it to change the style of the geojson and render the clicked district with a specific style.

    Using onEachFeature function you can derive the unique id and zoom to the clicked district. Once you store it in a var you can then filter the geojson by showing only this object that contains the unique id. Since react-leaflet's geojson comp data property is immutable you have to play with the reference (ref). You can use leaflet's eachLayer to attach specific style to all objects apart from the clicked. The latter will be achieved by setting the clicked layer style once you filter the geojson via a useeffect (see code below). Then using leaflet's addData you can readd the filtered geojson on the map.

    export default function Map() {
      const [map, setMap] = useState(null);
      const geojsonRef = useRef();
      const [featureId, setFeatureId] = useState(null);
    
      const handleEachFeature = (feature, layer) => {
        layer.on({
          click: (e) => {
            setFeatureId(feature.properties.cartodb_id);
            map.fitBounds(e.target.getBounds());
          }
        });
      };
    
      useEffect(() => {
        if (!featureId || !geojsonRef.current) return;
        geojsonRef.current.eachLayer((layer) => {
          layer.setStyle({
            opacity: 0.5,
            weight: 0
          });
        }); // inherited from LayerGroup
        const newDistricts = districts.features.filter((district) => {
          return district.properties.cartodb_id === featureId;
        });
        geojsonRef.current.addData(newDistricts);
      }, [featureId]);
    
      return (
        <MapContainer
          center={position}
          zoom={9}
          style={{ height: "100vh" }}
          ref={setMap}
        >
          <TileLayer
            attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
            url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
          />
          {map && (
            <GeoJSON
              ref={geojsonRef}
              data={districts}
              onEachFeature={handleEachFeature}
            />
          )}
        </MapContainer>
      );
    }
    

    You could erase it entirely using clearLayers method but that makes no sense for me because you will end up with showing only the clicked district once you click it. I tried another approach by changing the style of all other districts apart from the clicked one. This way you can click a new one and revert the style of the previous one.

    A simple free geojson is used to present the result.

    Demo