steinway
steinway

Reputation: 11

How to cluster polygons with react-leaflet?

I'm looking for a way to cluster polygons using react-leaflet v4 and react-leaflet-markercluster. I have not found any up-to-date examples of how I can achieve this, so I'm hoping I might get some help here.

Any example code to get me started would be a great help!

Upvotes: 0

Views: 944

Answers (1)

Disco
Disco

Reputation: 1634

This will probably not solve your problem directly but hopefully show that using markercluster is rather simple. The only thing you need is to have a createMarkerCluster function.

clusterProps has a field for polygonOptions:

        /*
        * Options to pass when creating the L.Polygon(points, options) to show the bounds of a cluster.
        * Defaults to empty
        */
        polygonOptions?: PolylineOptions | undefined;

Since you now use a plain leaflet plugin it opens up for mor information on the internet, these two might help how you should configure polygonOptions How to make MarkerClusterGroup cluster polygons https://gis.stackexchange.com/questions/197882/is-it-possible-to-cluster-polygons-in-leaflet

Below is my general code to make clustermarkers work with React:

import { createPathComponent } from "@react-leaflet/core";
import L, { LeafletMouseEventHandlerFn } from "leaflet";
import "leaflet.markercluster";
import { ReactElement, useMemo } from "react";
import { Building, BuildingStore, Circle } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
import styles from "./LeafletMarkerCluster.module.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
type ClusterType = { [key in string]: any };

type ClusterEvents = {
  onClick?: LeafletMouseEventHandlerFn;
  onDblClick?: LeafletMouseEventHandlerFn;
  onMouseDown?: LeafletMouseEventHandlerFn;
  onMouseUp?: LeafletMouseEventHandlerFn;
  onMouseOver?: LeafletMouseEventHandlerFn;
  onMouseOut?: LeafletMouseEventHandlerFn;
  onContextMenu?: LeafletMouseEventHandlerFn;
};

// Leaflet is badly typed, if more props needed add them to the interface.
// Look in this file to see what is available.
// node_modules/@types/leaflet.markercluster/index.d.ts
// MarkerClusterGroupOptions
export interface LeafletMarkerClusterProps {
  spiderfyOnMaxZoom?: boolean;
  children: React.ReactNode;
  size?: number;
  icon?: ReactElement;
}
const createMarkerCluster = (
  {
    children: _c,
    size = 30,
    icon = <Circle size={size} />,
    ...props
  }: LeafletMarkerClusterProps,
  context: any
) => {
  const markerIcons = {
    default: <Circle size={size} />,
    property: <Building size={size} />,
    business: <BuildingStore size={size} />,
  } as { [key in string]: ReactElement };

  const clusterProps: ClusterType = {
    iconCreateFunction: (cluster: any) => {
      const markers = cluster.getAllChildMarkers();

      const types = markers.reduce(
        (
          acc: { [x: string]: number },
          marker: {
            key: string;
            options: { icon: { options: { className: string } } };
          }
        ) => {
          const key = marker?.key || "";
          const type =
            marker.options.icon.options.className || key.split("-")[0];
          const increment = (key.split("-")[1] as unknown as number) || 1;

          if (type in markerIcons) {
            return { ...acc, [type]: (acc[type] || 0) + increment };
          }
          return { ...acc, default: (acc.default || 0) + increment };
        },
        {}
      ) as { [key in string]: number };
      const typeIcons = Object.entries(types).map(([type, count], index) => {
        if (count > 0) {
          const typeIcon = markerIcons[type];
          return (
            <div key={`${type}-${count}`} style={{ display: "flex" }}>
              <span>{typeIcon}</span>
              <span style={{ width: "max-content" }}>{count}</span>
            </div>
          );
        }
      });
      const iconWidth = typeIcons.length * size;

      return createLeafletIcon(
        <div style={{ display: "flex" }} className={"cluster-marker"}>
          {typeIcons}
        </div>,
        iconWidth,
        undefined,
        iconWidth,
        30
      );
    },
    showCoverageOnHover: false,
    animate: true,
    animateAddingMarkers: false,
    removeOutsideVisibleBounds: false,
  };
  const clusterEvents: ClusterType = {};
  // Splitting props and events to different objects
  Object.entries(props).forEach(([propName, prop]) =>
    propName.startsWith("on")
      ? (clusterEvents[propName] = prop)
      : (clusterProps[propName] = prop)
  );

  const instance = new (L as any).MarkerClusterGroup(clusterProps);

  instance.on("spiderfied", (e: any) => {
    e.cluster._icon?.classList.add(styles.spiderfied);
  });
  instance.on("unspiderfied", (e: any) => {
    e.cluster._icon?.classList.remove(styles.spiderfied);
  });

  // This is not used at the moment, but could be used to add events to the cluster.
  // Initializing event listeners
  Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
    const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
    instance.on(clusterEvent, callback);
  });
  return {
    instance,
    context: {
      ...context,
      layerContainer: instance,
    },
  };
};
const updateMarkerCluster = (instance: any, props: any, prevProps: any) => {};
const LeafletMarkerCluster = createPathComponent(
  createMarkerCluster,
  updateMarkerCluster
);

const LeafletMarkerClusterWrapper: React.FC<LeafletMarkerClusterProps> = ({
  children,
  ...props
}) => {
  const markerCluster = useMemo(() => {
    return <LeafletMarkerCluster>{children}</LeafletMarkerCluster>;
  }, [children]);
  return <>{markerCluster}</>;
};

export default LeafletMarkerClusterWrapper;

Below is my function to create a marker icon from react elements:

import { divIcon } from "leaflet";
import { ReactElement } from "react";
import { renderToString } from "react-dom/server";

export const createLeafletIcon = (
  icon: ReactElement,
  size: number,
  className?: string,
  width: number = size,
  height: number = size
) => {
  return divIcon({
    html: renderToString(icon),
    iconSize: [width, height],
    iconAnchor: [width / 2, height],
    popupAnchor: [0, -height],
    className: className ? className : "",
  });
};

Upvotes: 1

Related Questions