How to use Google Map MarkerClusterer on OverlayView which is rendered via ReactDOM createPortal

Viewed 520

I've been banging my head against the wall with google maps. See my initial issue here

I fixed the issue of using a react component as a map marker, by creating a portal with the OverlayView into a div. But now I'm faced with a bump in a rug now where I am now unsure how to create clustered markers?

Previously I used @googlemaps/markerclustererplus to create a cluster from an array of of markers created from a class CustomMarker that generates an OverlayView with the code snippet below.

const markers = mapMarkers.map((m) => {
  const { position, icon, draggable } = m;
  const marker = new CustomMarker(
    position,
    draggable,
    icon,
    map,
    onChildMouseUp
  );
  return marker;
});

// Add a marker clusterer to manage the markers.
new MarkerClusterer(map, markers, {
  calculator: (gMarkers, numStyles) => {
    let index = 0;
    const count = gMarkers.length;
    let dv = count;
    while (dv !== 0) {
      dv = parseInt(dv / 10, 10);
      index += 1;
    }
    index = Math.min(index, numStyles);
    return {
      text: `${count}`,
      index
    };
  },
  gridSize: 50,
  styles: [
    {
      className: "clusterMarker",
      width: 50,
      height: 18
    }
  ]
});

Now I'm confused how to populate the 2nd argument in MarkerClusterer with the markers now I add markers in the components return statement

<Map id="map" mapHeight={height} mapWidth={width}>
  {gmap && mapMarkers?.length > 0 ? (
    <>
      {mapMarkers.map((m, i) => {
        return (
          <CustomMarkerNew key={i} position={m.position} googleMap={gmap}>
            {m.icon}
          </CustomMarkerNew>
        );
      })}
    </>
  ) : null}
</Map>

This is how the CustomMarkerNew component looks

CustomMarkerNew.js

import React, { useState, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";

const OverlayView = (props) => {
  const { google } = window;
  const { googleMap, position, children } = props;

  const [latlng, setLatlng] = useState(position);
  const [container, setContainer] = useState(document.createElement("div"));
  const [overlay, setOverlay] = useState(undefined);

  const initOverlay = useCallback(() => {
    const overlayView = new google.maps.OverlayView();
    overlayView.setMap(googleMap);
    overlayView.getDraggable = () => true;
    setOverlay(overlayView);
    setLatlng(position);
    google.maps.OverlayView.preventMapHitsFrom(container);
  }, [googleMap, container]);

  const onAdd = useCallback(() => {
    // only draw marker if it has not been assigned the customMarker class
    if (!container.classList.contains("customMarker")) {
      container.classList.add("customMarker");
      container.style.position = "absolute";
      const panes = overlay.getPanes();
      panes.overlayImage.appendChild(container);
    }

    overlay.setPosition();
  }, [overlay]);

  const draw = useCallback(() => {
    container.style.position = "absolute";
    container.draggable = true;

    overlay.getPanes().floatPane.appendChild(container);

    overlay.setPosition();
  }, [overlay, container, latlng]);

  const onRemove = useCallback(() => {
    ReactDOM.unmountComponentAtNode(container);
    setContainer(undefined);
    overlay.setMap(null);
  }, [container, overlay]);

  const setPosition = (pos) => {
    // update global latlng value with current position of marker
    if (pos) setLatlng(pos);

    const projection = overlay.getProjection();
    if (!projection) return;

    // convert latlng value to pixel equivalent
    const point = projection.fromLatLngToDivPixel(latlng);

    // set left and top values from values from point variable to place marker correctly on map
    if (point && point.x && point.y) {
      container.style.left = `${point.x}px`;
      container.style.top = `${point.y}px`;
    }
  };

  const bindFunctions = useCallback(() => {
    overlay.onAdd = onAdd.bind(this);
    overlay.draw = draw.bind(this);
    overlay.onRemove = onRemove.bind(this);
    overlay.setPosition = setPosition.bind(this);
    overlay.getDraggable = () => true;
  }, [overlay]);

  useEffect(() => {
    if (overlay) {
      bindFunctions();
    }
  }, [overlay]);

  useEffect(() => {
    initOverlay();
  }, []);

  return (
    (overlay &&
      children &&
      container &&
      ReactDOM.createPortal(children, container)) ||
    null
  );
};

export default React.memo(OverlayView);

Here is a CodeSandbox as an example

Any help would be much appreciated.

0 Answers
Related