package com.google.maps.android.clustering; import android.content.Context; import android.os.AsyncTask; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.Marker; import com.google.maps.android.MarkerManager; import com.google.maps.android.clustering.algo.Algorithm; import com.google.maps.android.clustering.algo.PreCachingAlgorithmDecorator; import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm; import com.google.maps.android.clustering.view.ClusterRenderer; import com.google.maps.android.clustering.view.DefaultClusterRenderer; import java.util.Collection; import java.util.Set; /** * Groups many items on a map based on zoom level. * <p/> * ClusterManager should be added to the map as an: * <ul> * <li>{@link com.google.android.gms.maps.GoogleMap.OnCameraChangeListener}</li> * <li>{@link com.google.android.gms.maps.GoogleMap.OnMarkerClickListener}</li> * </ul> */ public class ClusterManager<T extends ClusterItem> implements GoogleMap.OnCameraChangeListener, GoogleMap.OnMarkerClickListener { private static final String TAG = ClusterManager.class.getName(); private final MarkerManager mMarkerManager; private final MarkerManager.Collection mMarkers; private final MarkerManager.Collection mClusterMarkers; private Algorithm<T> mAlgorithm; private ClusterRenderer<T> mRenderer; private GoogleMap mMap; private CameraPosition mPreviousCameraPosition; private ClusterTask mClusterTask; private OnClusterItemClickListener<T> mOnClusterItemClickListener; private OnClusterClickListener<T> mOnClusterClickListener; public ClusterManager(Context context, GoogleMap map) { this(context, map, new MarkerManager(map)); } public ClusterManager(Context context, GoogleMap map, MarkerManager markerManager) { mMap = map; mMarkerManager = markerManager; mClusterMarkers = markerManager.newCollection(); mMarkers = markerManager.newCollection(); mRenderer = new DefaultClusterRenderer<T>(context, map, this); mAlgorithm = new PreCachingAlgorithmDecorator<T>(new NonHierarchicalDistanceBasedAlgorithm<T>()); mClusterTask = new ClusterTask(); } public MarkerManager.Collection getMarkerCollection() { return mMarkers; } public MarkerManager.Collection getClusterMarkerCollection() { return mClusterMarkers; } public MarkerManager getMarkerManager() { return mMarkerManager; } public void setRenderer(ClusterRenderer<T> view) { view.setOnClusterClickListener(null); view.setOnClusterItemClickListener(null); mClusterMarkers.clear(); mMarkers.clear(); mRenderer.onRemove(); mRenderer = view; mRenderer.onAdd(); mRenderer.setOnClusterClickListener(mOnClusterClickListener); mRenderer.setOnClusterItemClickListener(mOnClusterItemClickListener); cluster(); } public void setAlgorithm(Algorithm<T> algorithm) { Collection<T> items = null; if (mAlgorithm != null) { items = mAlgorithm.getItems(); mAlgorithm.clearItems(); } mAlgorithm = new PreCachingAlgorithmDecorator<T>(algorithm); if (items != null) { mAlgorithm.addItems(items); } cluster(); } public void clearItems() { mAlgorithm.clearItems(); } public void addItems(Collection<T> items) { mAlgorithm.addItems(items); } public void addItem(T myItem) { mAlgorithm.addItem(myItem); } public void removeItem(T item) { mAlgorithm.removeItem(item); } /** * Force a re-cluster. You may want to call this after adding new item(s). */ public void cluster() { // Attempt to cancel the in-flight request. mClusterTask.cancel(true); mClusterTask = new ClusterTask(); mClusterTask.execute(mMap.getCameraPosition().zoom); } /** * Might re-cluster. * * @param cameraPosition */ @Override public void onCameraChange(CameraPosition cameraPosition) { if (mRenderer instanceof GoogleMap.OnCameraChangeListener) { ((GoogleMap.OnCameraChangeListener) mRenderer).onCameraChange(cameraPosition); } // Don't re-compute clusters if the map has just been panned/tilted/rotated. CameraPosition position = mMap.getCameraPosition(); if (mPreviousCameraPosition != null && mPreviousCameraPosition.zoom == position.zoom) { return; } mPreviousCameraPosition = mMap.getCameraPosition(); cluster(); } @Override public boolean onMarkerClick(Marker marker) { return getMarkerManager().onMarkerClick(marker); } /** * Runs the clustering algorithm in a background thread, then re-paints when results come * back. */ private class ClusterTask extends AsyncTask<Float, Void, Set<? extends Cluster<T>>> { @Override protected Set<? extends Cluster<T>> doInBackground(Float... zoom) { return mAlgorithm.getClusters(zoom[0]); } @Override protected void onPostExecute(Set<? extends Cluster<T>> clusters) { mRenderer.onClustersChanged(clusters); } } /** * Sets a callback that's invoked when a Cluster is tapped. * Note: For this listener to function, the ClusterManager must be added as a click listener to the map. */ public void setOnClusterClickListener(OnClusterClickListener<T> listener) { mOnClusterClickListener = listener; mRenderer.setOnClusterClickListener(listener); } /** * Sets a callback that's invoked when an individual ClusterItem is tapped. * Note: For this listener to function, the ClusterManager must be added as a click listener to the map. */ public void setOnClusterItemClickListener(OnClusterItemClickListener<T> listener) { mOnClusterItemClickListener = listener; mRenderer.setOnClusterItemClickListener(listener); } /** * Called when a Cluster is clicked. */ public interface OnClusterClickListener<T extends ClusterItem> { public boolean onClusterClick(Cluster<T> cluster); } /** * Called when an individual ClusterItem is clicked. */ public interface OnClusterItemClickListener<T extends ClusterItem> { public boolean onClusterItemClick(T item); } }