Clustering in Google Maps made simple: the SimpleCluster class

In combination to my SimpleMarker class I have made an engine for clustering those markers if many of them are in a similar location. However, before you use this engine in your application, please note the following: there are probably much better classes for managing high counts of markers, e.g. the MarkerManager class from the Google Maps Utility Library. My SimpleMarker class is also compatible to this MarkerManager, if you want to combine lightweight markers with managing classes.

You still might want to take a look on my code if you want to learn how clustering can be done, as I tried to design the code as understandable as possible. This is why I wrote this article, so keep on reading and see how it works ;-).

Basically clustering is used in cases where you have many markers in a small area. You can imagine that the viewer could hardly distinguish between those markers which is a bad user-experience. Wouldn't it be better to replace all those markers with a single one which magnifies the affected area when you click on it? This is what my class does. Additionally, my class looks at the bounds that are currently shown and hides all markers that are outside of them, further improving the speed of your map (less markers == better performance, even if they aren't directly visible).

The main question is, when are two markers close enough to be clustered? You could check if the physical location of the two markers are in a specific range (e.g. 1 mile/kilometer). But there is a problem: a fixed range can not be chosen as the zoom level might change. That means if the chosen range works well in zoom level 12, this could look totally different in larger or smaller zoom settings. Also, if the sizes of your Marker icons would vary a lot, this method would fail completely.

Instead, what I have done in the SimpleCluster class is looking at the bounds of a marker icon and checking if those bounds overlap. Because of this, I gave my markers the SimpleMarker.getBounds() method. This method maps the corners of a marker icon to latitude/longitude values on the map on a given (or the current) zoom level. Now the only thing we have to check if the bounds of marker A overlap with the bounds of marker B by using the API method LatLngBounds.intersects(). If we have a match, hide the two markers and create a special cluster-marker. Here is the corresponding code:

// Markers where the bounds are nulled were already assigned to sth
for(var i = 0; i < this._markers.length - 1; i++) {
  if (this._markers[i].sc_visible[zoom]) {
    var matches = 0;
    var bounds = new google.maps.LatLngBounds();
 
    for (var j = i+1; j < this._markers.length; j++) {
      if (
        this._markers[j].sc_visible[zoom] &&
        this._markers[i].getBounds(zoom).intersects(
          this._markers[j].getBounds(zoom))
      ) {
        bounds.extend(this._markers[j].getPosition());
        this._markers[j].sc_visible[zoom] = false;
        matches++;
      }
    }
 
    // If there was an intersection with this marker,
    // create a cluster icon on its position
    if (matches > 0) {
      bounds.extend(this._markers[i].getPosition());
      this._markers[i].sc_visible[zoom] = false;
      var mark = new SimpleMarker(this._map,
        this._markers[i].getPosition(),
        {
          image: '[long url]',
          title: (matches+1) + " Marker in this area"
        });
      mark.sc_bounds = bounds;

      // Default event for a cluster-marker: Zoom in
      // to the area where its clustered markers are
      // located
      google.maps.event.addListener(mark, 'click', function() {
        this.getMap().fitBounds(this.sc_bounds);
      }); 
   
      // Keep our marker for later usage
      this._clusters[zoom].push(mark);
    }
  }
}

A few notes on the fields in my class: every marker that is managed (disregarding whether visible or not) will be stored in an field set in SimpleCluster._markers. We add an additional attribute to the marker which stores for every zoom level if the marker is visible or not. This is the marker.sc_visible attribute. The cluster markers for every zoom level will be stored SimpleCluster._clusters. This caching improves performance when the user is switching between the levels a lot.

The calculation of clusters is only half of the story. The other half is displaying the clusters and/or markers depending on the area that is currently shown. This is done in the SimpleCluster.draw() method. This method should be called when the user changes the zoom level or drags the map around. I've added the listener in the constructor of the class so you don't have to add them manually to get the class working. Drawing itself is rather simple and is mainly composed out of this two for-loops:

// Iterate through possible markers
for(var i = 0; i < this._markers.length; i++) {
  if (
    this._markers[i].sc_visible[this._zoomVal] &&
    newBounds.contains(this._markers[i].getPosition())
  ) {
    // Display marker if was hidden before and is now in range
    if (!this._markers[i].getMap()) 
      this._markers[i].setMap(this._map);
  } else {
    // Hide marker if marker was shown and now is out of bounds
    this._markers[i].setMap(null);
  }
}
 
// Iterate through clusters and do the same as for markers
for(var i = 0; i < this._clusters[this._zoomVal].length; i++) {
  if (newBounds.contains(this._clusters[this._zoomVal][i].getPosition())) {
    // Display cluster if was hidden before and is now in range
    if (!this._clusters[this._zoomVal][i].getMap()) 
      this._clusters[this._zoomVal][i].setMap(this._map);
  } else {
    // Hide cluster if cluster was shown and now is out of bounds
    this._clusters[this._zoomVal][i].setMap(null);
  }
}

Here is not only checked if the markers shall be generally shown (or not - if they are in a cluster), but also if they are in a certain bound, called newBounds. These bounds contain the whole viewport of the user plus a small border around it. With that we ensure that no markers are drawn that are too far away from the users point of view and therefore useless anyway. This is generally an important step when improving websites for performance: don't render anything that the user won't need/use, as a complex DOM hierarchy (i.e. many DOM nodes) is one of the greatest performance killer in a web application.

Well, that's pretty much it. As you can see, clustering is not that magical as it might sound in the beginning. The only thing you have to make sure is to detect if two markers are close enough to be clustered or not. Everything else is simple showing and hiding of markers.

A working version can be found on demo.k621.de. The source code is attached at the end of this article. As said in the beginning, this clusterer is mainly for learning purposes, there might be others that work better.

Attachments