Dynamically loading thousands of markers in Google Maps

UPDATE (September 17, 2011): If you want to effectively handle many thousands of points in a Google Map then the only real solution is using Google Fusion Tables. I hit on that as a solution a few months ago and it’s been working perfectly for The Broadband Map and it’s over 400,000 loadable points. Blog post with details coming soon.


When you’re making a Google Map with more than about a hundred markers, there are two established solutions: MarkerManager and MarkerClusterer.

MarkerManager:

  • Loads/unloads markers in a map as you pan the viewport
  • Allows adding sets of markers to specific zoom ranges

The restriction of markers to zoom levels means that you can use MarkerManager to roll your own clustering. That’s great for the end user because you serve up a completely customized experience. The downside is that you have to completely customize the experience. That means you’ll have to have some metric by which to split out your points into zoom levels. Say you have five thousand points: you’ll need to split them into zones so that no more than a few hundred (at the most) are visible at once.

MarkerClusterer

  • Automatically clusters markers

That point is a big deal. Clustering isn’t cake, and marker cluster performs the tricky task with aplomb. The icons and styling is a little eh out of the box, but it gets you to the point where you actually care about icons and styling immediately.

The problem with both

Both solutions really want to work off a fixed array of markers that you’ve already pulled in. Feed either solution up to a couple thousand points of data and they’ll handle it (either automatically or with some work on your part) and you can move on.

But neither solution really deals well with loading almost a hundred thousand points of data. Obviously we won’t feed every map every point all at once, and neither MarkerManager nor MarkerClusterer handles dynamically adding and removing points that well since they are both doing a fair bit of calculation that requires knowing about every point.

My solution

My solution, so far, is to:

  • dynamically load markers until we reach a threshold
  • keep a hashtable of markers that have already been added
  • after the threshold has been reached, remove markers that aren’t currently within the viewport
  • remove all markers from the map when the user has zoomed out, and don’t load any markers until the user zooms back to a reasonable level

In my case, the point data is really only useful at the city level. This allows me to completely ignore the higher zoom levels.

Here’s the essentials from the code I have so far:

function requestMarkers() {
  // no loading markers when zoomed way out
  if (map.getZoom() < 11) { return };
  // start dropping markers before the browser bogs down
  if (markers.length > 50) {
    dropSuperfluousMarkers();
  }
  $.ajax({
    // [snip typical ajax data request]
    success: populateMarkers,
  });
}

// add the markers from pointsData if they haven't already been placed
function populateMarkers(pointsData, status, xhr) {
  for (var i = 0, ii = pointsData.length; i < ii; i++)  {
    var lat = pointData.geom.x;
    var lng = pointData.geom.y;
    var marker = new google.maps.Marker({
      position: new google.maps.LatLng(lat, lng),
      draggable: false,
      animation: google.maps.Animation.DROP, // whee!
      flat: true, // disables some styling DOM elements for a faster marker
    });

    // hash the marker position
    coordHash = calculateCoordinateHash(marker);

    /*
    if we haven't seen this hash, add the marker and mark as seen

    without this, the markers array quickly grows unwieldy as duplicate points
    are loaded
    */
    if(seenCoordinates[coordHash] == null) {
      seenCoordinates[coordHash] = 1;
      markers.push(marker);
      marker.setMap(map);
    }
  }
}

// turn marker coordinates into a hash key
function calculateCoordinateHash(marker) {
  var coordinatesHash = [ marker.getPosition().lat(),
                          marker.getPosition().lng() ].join('');
  return coordinatesHash.replace(".","").replace(",", "").replace("-","");
}

// remove markers that aren't currently visible
function dropSuperfluousMarkers() {
  mapBounds = map.getBounds();
  for (var i = 0, ii = markers.length; i < ii; i++)  {
    if (!markers[i]) {continue};
    if (!mapBounds.contains(markers[i].getPosition())) {
      // remove from the map
      markers[i].setMap(null);

      // remove from the record of seen markers
      coordHash = calculateCoordinateHash(markers[i]);
      if(seenCoordinates[coordHash]) {
        seenCoordinates[coordHash] = null;
      }

      // remove from the markers array
      markers.splice(i, 1);
    }
  }
}

// clear all markers from the map, empty the markers array and seen markers
function clearMarkers() {
  for (var i = 0, ii = markers.length; i < ii; i++)  {
    markers[i].setMap(null);
  }
  markers = [];
  seenCoordinates = {};
}