/* * Copyright 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.maps.android.clustering; import android.content.Context; import android.os.AsyncTask; import android.os.Build; 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.NonHierarchicalDistanceBasedAlgorithm; import com.google.maps.android.clustering.algo.PreCachingAlgorithmDecorator; import com.google.maps.android.clustering.view.ClusterRenderer; import com.google.maps.android.clustering.view.DefaultClusterRenderer; import java.util.Collection; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * 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, GoogleMap.OnInfoWindowClickListener { private final MarkerManager mMarkerManager; private final MarkerManager.Collection mMarkers; private final MarkerManager.Collection mClusterMarkers; private Algorithm<T> mAlgorithm; private final ReadWriteLock mAlgorithmLock = new ReentrantReadWriteLock(); private ClusterRenderer<T> mRenderer; private GoogleMap mMap; private CameraPosition mPreviousCameraPosition; private ClusterTask mClusterTask; private final ReadWriteLock mClusterTaskLock = new ReentrantReadWriteLock(); private OnClusterItemClickListener<T> mOnClusterItemClickListener; private OnClusterInfoWindowClickListener<T> mOnClusterInfoWindowClickListener; private OnClusterItemInfoWindowClickListener<T> mOnClusterItemInfoWindowClickListener; 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(); mRenderer.onAdd(); } public MarkerManager.Collection getMarkerCollection() { return mMarkers; } public MarkerManager.Collection getClusterMarkerCollection() { return mClusterMarkers; } public MarkerManager getMarkerManager() { return mMarkerManager; } public void setRenderer(ClusterRenderer<T> view) { mRenderer.setOnClusterClickListener(null); mRenderer.setOnClusterItemClickListener(null); mClusterMarkers.clear(); mMarkers.clear(); mRenderer.onRemove(); mRenderer = view; mRenderer.onAdd(); mRenderer.setOnClusterClickListener(mOnClusterClickListener); mRenderer.setOnClusterInfoWindowClickListener(mOnClusterInfoWindowClickListener); mRenderer.setOnClusterItemClickListener(mOnClusterItemClickListener); mRenderer.setOnClusterItemInfoWindowClickListener(mOnClusterItemInfoWindowClickListener); cluster(); } public void setAlgorithm(Algorithm<T> algorithm) { mAlgorithmLock.writeLock().lock(); try { if (mAlgorithm != null) { algorithm.addItems(mAlgorithm.getItems()); } mAlgorithm = new PreCachingAlgorithmDecorator<T>(algorithm); } finally { mAlgorithmLock.writeLock().unlock(); } cluster(); } public void clearItems() { mAlgorithmLock.writeLock().lock(); try { mAlgorithm.clearItems(); } finally { mAlgorithmLock.writeLock().unlock(); } } public void addItems(Collection<T> items) { mAlgorithmLock.writeLock().lock(); try { mAlgorithm.addItems(items); } finally { mAlgorithmLock.writeLock().unlock(); } } public void addItem(T myItem) { mAlgorithmLock.writeLock().lock(); try { mAlgorithm.addItem(myItem); } finally { mAlgorithmLock.writeLock().unlock(); } } public void removeItem(T item) { mAlgorithmLock.writeLock().lock(); try { mAlgorithm.removeItem(item); } finally { mAlgorithmLock.writeLock().unlock(); } } /** * Force a re-cluster. You may want to call this after adding new item(s). */ public void cluster() { mClusterTaskLock.writeLock().lock(); try { // Attempt to cancel the in-flight request. mClusterTask.cancel(true); mClusterTask = new ClusterTask(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { mClusterTask.execute(mMap.getCameraPosition().zoom); } else { mClusterTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mMap.getCameraPosition().zoom); } } finally { mClusterTaskLock.writeLock().unlock(); } } /** * 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); } @Override public void onInfoWindowClick(Marker marker) { getMarkerManager().onInfoWindowClick(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) { mAlgorithmLock.readLock().lock(); try { return mAlgorithm.getClusters(zoom[0]); } finally { mAlgorithmLock.readLock().unlock(); } } @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 a Cluster is tapped. Note: For this listener to function, * the ClusterManager must be added as a info window click listener to the map. */ public void setOnClusterInfoWindowClickListener(OnClusterInfoWindowClickListener<T> listener) { mOnClusterInfoWindowClickListener = listener; mRenderer.setOnClusterInfoWindowClickListener(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); } /** * Sets a callback that's invoked when an individual ClusterItem's Info Window is tapped. Note: For this * listener to function, the ClusterManager must be added as a info window click listener to the map. */ public void setOnClusterItemInfoWindowClickListener(OnClusterItemInfoWindowClickListener<T> listener) { mOnClusterItemInfoWindowClickListener = listener; mRenderer.setOnClusterItemInfoWindowClickListener(listener); } /** * Called when a Cluster is clicked. */ public interface OnClusterClickListener<T extends ClusterItem> { public boolean onClusterClick(Cluster<T> cluster); } /** * Called when a Cluster's Info Window is clicked. */ public interface OnClusterInfoWindowClickListener<T extends ClusterItem> { public void onClusterInfoWindowClick(Cluster<T> cluster); } /** * Called when an individual ClusterItem is clicked. */ public interface OnClusterItemClickListener<T extends ClusterItem> { public boolean onClusterItemClick(T item); } /** * Called when an individual ClusterItem's Info Window is clicked. */ public interface OnClusterItemInfoWindowClickListener<T extends ClusterItem> { public void onClusterItemInfoWindowClick(T item); } }