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);
}
}