@SuppressLint("NewApi")
    public void run() {
      if (clusters.equals(DefaultClusterRenderer.this.mClusters)) {
        mCallback.run();
        return;
      }

      final MarkerModifier markerModifier = new MarkerModifier();

      final float zoom = mMapZoom;
      final boolean zoomingIn = zoom > mZoom;
      final float zoomDelta = zoom - mZoom;

      final Set<MarkerWithPosition> markersToRemove = mMarkers;
      final LatLngBounds visibleBounds = mProjection.getVisibleRegion().latLngBounds;
      // TODO: Add some padding, so that markers can animate in from off-screen.

      // Find all of the existing clusters that are on-screen. These are candidates for
      // markers to animate from.
      List<Point> existingClustersOnScreen = null;
      if (DefaultClusterRenderer.this.mClusters != null && SHOULD_ANIMATE) {
        existingClustersOnScreen = new ArrayList<Point>();
        for (Cluster<T> c : DefaultClusterRenderer.this.mClusters) {
          if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) {
            Point point = mSphericalMercatorProjection.toPoint(c.getPosition());
            existingClustersOnScreen.add(point);
          }
        }
      }

      // Create the new markers and animate them to their new positions.
      final Set<MarkerWithPosition> newMarkers = new HashSet<MarkerWithPosition>();
      for (Cluster<T> c : clusters) {
        boolean onScreen = visibleBounds.contains(c.getPosition());
        if (zoomingIn && onScreen && SHOULD_ANIMATE) {
          Point point = mSphericalMercatorProjection.toPoint(c.getPosition());
          Point closest = findClosestCluster(existingClustersOnScreen, point);
          if (closest != null) {
            LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest);
            markerModifier.add(true, new CreateMarkerTask(c, newMarkers, animateTo));
          } else {
            markerModifier.add(true, new CreateMarkerTask(c, newMarkers, null));
          }
        } else {
          markerModifier.add(onScreen, new CreateMarkerTask(c, newMarkers, null));
        }
      }

      // Wait for all markers to be added.
      markerModifier.waitUntilFree();

      // Don't remove any markers that were just added. This is basically anything that had
      // a hit in the MarkerCache.
      markersToRemove.removeAll(newMarkers);

      // Find all of the new clusters that were added on-screen. These are candidates for
      // markers to animate from.
      List<Point> newClustersOnScreen = null;
      if (SHOULD_ANIMATE) {
        newClustersOnScreen = new ArrayList<Point>();
        for (Cluster<T> c : clusters) {
          if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) {
            Point p = mSphericalMercatorProjection.toPoint(c.getPosition());
            newClustersOnScreen.add(p);
          }
        }
      }

      // Remove the old markers, animating them into clusters if zooming out.
      for (final MarkerWithPosition marker : markersToRemove) {
        boolean onScreen = visibleBounds.contains(marker.position);
        // Don't animate when zooming out more than 3 zoom levels.
        // TODO: drop animation based on speed of device & number of markers to animate.
        if (!zoomingIn && zoomDelta > -3 && onScreen && SHOULD_ANIMATE) {
          final Point point = mSphericalMercatorProjection.toPoint(marker.position);
          final Point closest = findClosestCluster(newClustersOnScreen, point);
          if (closest != null) {
            LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest);
            markerModifier.animateThenRemove(marker, marker.position, animateTo);
          } else {
            markerModifier.remove(true, marker.marker);
          }
        } else {
          markerModifier.remove(onScreen, marker.marker);
        }
      }

      markerModifier.waitUntilFree();

      mMarkers = newMarkers;
      DefaultClusterRenderer.this.mClusters = clusters;
      mZoom = zoom;

      mCallback.run();
    }