Google-maps-react: Is google clustering possible with this library?

Created on 29 Oct 2016  路  16Comments  路  Source: fullstackreact/google-maps-react

I want to know whether goole clustering is possible with this library

Most helpful comment

I found a simple solution to the clustering.
First, I'm using the google markerclusterer.js file you can download from google here:
https://developers.google.com/maps/documentation/javascript/marker-clustering

Then, I observed that the Maps component passes props to child components, including maps, and google. Both of these are needed to create the clusters. From there, I just created a "MarkerCluster" component. Full source is here

import React, {useEffect} from 'react'
import PropTypes from 'prop-types'

import MarkerClusterer from './markerclusterer'

const evtNames = [
  'click',
  'dblclick',
  'dragend',
  'mousedown',
  'mouseout',
  'mouseover',
  'mouseup',
  'recenter',
]

const markerCluster = (props) => {
  const {map, google, markers} = props

  const handleEvent = ({event, marker, entry}) => {
    if (props[event]) {
      props[event]({
        props: props,
        marker: marker,
        event: event,
        entry: entry
      })
    }
  }

  // This hook works like ComponentWillMount
  // The  hook isn't really needed, this whole thing worked without it,
  // I added the hook so that I could implement a cleanup function
  useEffect(() => {
    if (map && markers) {
      const mapMarkers = markers.map((marker) => {
        const entry = new google.maps.Marker({
          position: {
            lat: marker.position.lat,
            lng: marker.position.lng
          },
          map: map,
          name: marker.name
        })

        evtNames.forEach(e => {
          entry.addListener(e, () => handleEvent({
            event: e,
            marker: marker,
            entry: entry
          }))
        })

        return entry
      })

      const clusterer = new MarkerClusterer(map, mapMarkers, {imagePath: '/static/images/maps/m'})

      // Cleanup function. Note, this is only returned if we create the markers
      return () => {
        //console.log('Cleaning up markers')
        clusterer.clearMarkers()
      }
    }
  }, [map, google, markers])


  // Do we need to render anything??
  return (null)

}

markerCluster.propTypes = {
  map: PropTypes.object,
  google: PropTypes.object,
  markers: PropTypes.arrayOf(PropTypes.shape({
    position: PropTypes.shape({
      lat: PropTypes.number.isRequired,
      lng: PropTypes.number.isRequired,
    }).isRequired,
    name: PropTypes.string.isRequired,
  })),
}

export default markerCluster

Then in your map implemtation, something like this:

      <Map google={google}>
        <MarkerCluster
          markers={markers}
          click={this.onMarkerClick}
          mouseover={this.onMouseOver}
          mouseout={this.onMouseOut}
        />
      </Map>

I used the same technique found in the Marker component to map handlers to the markers that are created, and I pass enough information back so handlers can do what is needed. I probably pass too much... but, it works.

This could probably be added to the library pretty easily ;-)

All 16 comments

+1

Use this: https://www.npmjs.com/package/node-js-marker-clusterer
Works for me.

import MarkerClusterer from 'node-js-marker-clusterer';

const mc = new MarkerClusterer(
        this.map, // from new google.maps.Map();
        markers, // from const markers = locations.map()
        {
          styles: [{
            width: 40,
            height: 40,
            url: '/assets/icon-markercluster.png',
            textColor: 'white',
          }],
        },
      );

@joriscolours do you have a sample?

@scyrizales I used it here: https://www.vanlanschot.nl/contact/kantoren

@joriscolours Hi. I have a question. Could you please show us a code snippet, how did you use this Cluster with this plugin. Thank you.

Something like this.

The 'parent' component:

/**
 * This component requires the 3rd party HOC to add the <script> tag dynamically and pass the google (google.maps) object to the subcomponent.
 */

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { GoogleApiWrapper } from 'google-maps-react';

import GoogleMap from 'Feature/Locations/code/Patterns/Molecules/GoogleMap';

class GoogleMapContainer extends Component {
  render() {
    return (
      <GoogleMap
        google={this.props.google}
        {...this.props}
      />
    );
  }
}

GoogleMapContainer.propTypes = {
  google: PropTypes.objectOf(PropTypes.any),
  settings: PropTypes.objectOf(PropTypes.any),
};
GoogleMapContainer.defaultProps = {
  google: null,
  settings: null,
};

export default GoogleApiWrapper(props => ({
  apiKey: props.settings.apiKey,
}
))(GoogleMapContainer);

The child component, <GoogleMap>.

import React, { Component } from 'react';
import PropTypes from 'prop-types';

import MarkerClusterer from 'node-js-marker-clusterer';

class GoogleMap extends Component {
  componentDidMount() {
    this.loadMap();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.google !== this.props.google || prevProps.locations !== this.props.locations) {
      this.loadMap();
    }
  }

  loadMap() {
    if (this.props && this.props.google) {
      const { google } = this.props;
      const node = this.mapRef;
      const mapConfig = Object.assign({}, {
        center: { lat: this.props.settings.defaultLatitude, lng: this.props.settings.defaultLongitude },
        zoom: this.props.settings.zoom,
        mapTypeControl: false,
        streetViewControl: false,
        gestureHandling: 'cooperative',
      });

      this.map = new google.maps.Map(node, mapConfig);

      const infowindow = new google.maps.InfoWindow({
        content: this.props.labels.loading,
      });

      const markers = this.props.locations.map((location) => {
        const marker = new google.maps.Marker({
          position: { lat: location.lat, lng: location.lng },
          map: this.map,
          content: `<div class="c-maps__callout">
            ...
          </div>`,
          icon: '/static/assets/icon-mapmarker.png',
        });

        if (location.isOpen) {
          setTimeout(() => {
            infowindow.setContent(marker.content);
            infowindow.open(this.map, marker);
          }, 1000);
        }

        google.maps.event.addListener(marker, 'click', () => {
          infowindow.setContent(marker.content);
          infowindow.open(this.map, marker);
        });

        return marker;
      });

      return new MarkerClusterer(
        this.map,
        markers,
        {
          styles: [{
            width: 40,
            height: 40,
            url: '/static/assets/icon-markercluster1.png',
            textColor: 'white',
          }],
        },
      );
    }

    return {};
  }

  render() {
    return (
      <div
        className="c-maps"
        ref={(e) => {
          this.mapRef = e;
        }}
      />
    );
  }
}

By the way @joriscolours, I was able to find it from your page, just a little unminifying action :D
To be clear with anyone, in order to use MarkerClusterer, you need to stop using the Marker component exposed by this google-maps-react

I found a simple solution to the clustering.
First, I'm using the google markerclusterer.js file you can download from google here:
https://developers.google.com/maps/documentation/javascript/marker-clustering

Then, I observed that the Maps component passes props to child components, including maps, and google. Both of these are needed to create the clusters. From there, I just created a "MarkerCluster" component. Full source is here

import React, {useEffect} from 'react'
import PropTypes from 'prop-types'

import MarkerClusterer from './markerclusterer'

const evtNames = [
  'click',
  'dblclick',
  'dragend',
  'mousedown',
  'mouseout',
  'mouseover',
  'mouseup',
  'recenter',
]

const markerCluster = (props) => {
  const {map, google, markers} = props

  const handleEvent = ({event, marker, entry}) => {
    if (props[event]) {
      props[event]({
        props: props,
        marker: marker,
        event: event,
        entry: entry
      })
    }
  }

  // This hook works like ComponentWillMount
  // The  hook isn't really needed, this whole thing worked without it,
  // I added the hook so that I could implement a cleanup function
  useEffect(() => {
    if (map && markers) {
      const mapMarkers = markers.map((marker) => {
        const entry = new google.maps.Marker({
          position: {
            lat: marker.position.lat,
            lng: marker.position.lng
          },
          map: map,
          name: marker.name
        })

        evtNames.forEach(e => {
          entry.addListener(e, () => handleEvent({
            event: e,
            marker: marker,
            entry: entry
          }))
        })

        return entry
      })

      const clusterer = new MarkerClusterer(map, mapMarkers, {imagePath: '/static/images/maps/m'})

      // Cleanup function. Note, this is only returned if we create the markers
      return () => {
        //console.log('Cleaning up markers')
        clusterer.clearMarkers()
      }
    }
  }, [map, google, markers])


  // Do we need to render anything??
  return (null)

}

markerCluster.propTypes = {
  map: PropTypes.object,
  google: PropTypes.object,
  markers: PropTypes.arrayOf(PropTypes.shape({
    position: PropTypes.shape({
      lat: PropTypes.number.isRequired,
      lng: PropTypes.number.isRequired,
    }).isRequired,
    name: PropTypes.string.isRequired,
  })),
}

export default markerCluster

Then in your map implemtation, something like this:

      <Map google={google}>
        <MarkerCluster
          markers={markers}
          click={this.onMarkerClick}
          mouseover={this.onMouseOver}
          mouseout={this.onMouseOut}
        />
      </Map>

I used the same technique found in the Marker component to map handlers to the markers that are created, and I pass enough information back so handlers can do what is needed. I probably pass too much... but, it works.

This could probably be added to the library pretty easily ;-)

@kutenai nice. thanks for the solution.

@kutenai i tried the same but I am getting an error

Uncaught TypeError: _markerclusterer__WEBPACK_IMPORTED_MODULE_2___default.a is not a constructor
at markercluster.js:57

Hi @kutenai,
The link for the markerclusterer.js from https://developers.google.com/maps/documentation/javascript/marker-clustering isn't available at this point in time. I have used this file instead Archive which I assume is the same file you were talking about. However, I am getting an error that tells me that in the markerclusterer.js file that google is undefined. Is it because the file I have found is not the same as the one you have used? or is there an underlying problem elsewhere?

You need to instantiate the google map. You can't do this from your compiled code, since node does not expose global variables, so I add the logic to my template. Here is my initialization code

```javascript{% block extra_js %}

```

You will need to secure an API key, which can cost you money, but this is generally a few cents, or a few dollars until you get to high traffic. Hopefully, by then you won't care.

You need to instantiate the google map. You can't do this from your compiled code, since node does not expose global variables, so I add the logic to my template. Here is my initialization code

<script type="text/javascript">
    function createMap(location) {
        var mapOptions = {
            center: location['center'],
            zoom: location.zoom_level
        };
        return new google.maps.Map(document.getElementById("map-canvas"), mapOptions);
    }

    function initMap() {
        var location = {{ location|safe }};
        var map = createMap(location);

        if ('polymap'.indexOf(location['type']) !== -1) {
            createPolyMap(map, location);
        } else {
            createCircleMap(map, location);
        }
    }

    function createPolyMap(map, options) {
        var polyPath = new google.maps.Polygon({
            paths: options['paths'],
            strokeColor: '#FF0000',
            strokeOpacity: 0.8,
            strokeWeight: 2,
            fillColor: '#FF0000',
            fillOpacity: 0.35
        });
        polyPath.setMap(map);
        return polyPath;
    }

    function createCircleMap(map, options) {
        var radius = options['radius'];
        return new google.maps.Circle({
            strokeColor: '#FF0000',
            strokeOpacity: 0.8,
            strokeWeight: 2,
            fillColor: '#FF0000',
            fillOpacity: 0.35,
            center: options['center'],
            map: map,
            radius: radius
        });
    }

    function createMarker(map, latLong, title, desc) {
        return new google.maps.Marker({
            position: latLong,
            map: map,
            title: title,
            desc: desc
        });
    }
</script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAP_API_KEY }}&callback=initMap" type="text/javascript"></script>

You will need to secure an API key, which can cost you money, but this is generally a few cents, or a few dollars until you get to high traffic. Hopefully, by then you won't care.

Hi @kutenai , may I know where did you put the initialization code? Where is this "template" located? Thank you!

Is it possible to cluster with Marker component?

Hi @kutenai I implemented google-maps-react and its working fine but I need to implement clustering please let me know what is the best way to implement it. Thanks

@kutenai Is it possible to cluster with Marker component?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ktosiek picture ktosiek  路  3Comments

lishi500 picture lishi500  路  4Comments

joeyfigaro picture joeyfigaro  路  3Comments

t1a2l picture t1a2l  路  5Comments

kevinSuttle picture kevinSuttle  路  4Comments