package com.google.maps.android.clustering.algo; import com.google.android.gms.maps.model.LatLng; import com.google.maps.android.clustering.Cluster; import com.google.maps.android.clustering.ClusterItem; import com.google.maps.android.geometry.Bounds; import com.google.maps.android.geometry.Point; import com.google.maps.android.projection.SphericalMercatorProjection; import com.google.maps.android.quadtree.PointQuadTree; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not * hierarchical. * <p/> * High level algorithm:<br> * 1. Iterate over items in the order they were added (candidate clusters).<br> * 2. Create a cluster with the center of the item. <br> * 3. Add all items that are within a certain distance to the cluster. <br> * 4. Move any items out of an existing cluster if they are closer to another cluster. <br> * 5. Remove those items from the list of candidate clusters. * <p/> * Clusters have the center of the first element (not the centroid of the items within it). */ public class NonHierarchicalDistanceBasedAlgorithm<T extends ClusterItem> implements Algorithm<T> { public static final int MAX_DISTANCE_AT_ZOOM = 100; // essentially 100 dp. /** * Any modifications should be synchronized on mQuadTree. */ private final Collection<QuadItem<T>> mItems = new ArrayList<QuadItem<T>>(); /** * Any modifications should be synchronized on mQuadTree. */ private final PointQuadTree<QuadItem<T>> mQuadTree = new PointQuadTree<QuadItem<T>>(0, 1, 0, 1); private static final SphericalMercatorProjection PROJECTION = new SphericalMercatorProjection(1); @Override public void addItem(T item) { final QuadItem<T> quadItem = new QuadItem<T>(item); synchronized (mQuadTree) { mItems.add(quadItem); mQuadTree.add(quadItem); } } @Override public void addItems(Collection<T> items) { for (T item : items) { addItem(item); } } @Override public void clearItems() { synchronized (mQuadTree) { mItems.clear(); mQuadTree.clear(); } } @Override public void removeItem(T item) { // TODO: delegate QuadItem#hashCode and QuadItem#equals to its item. throw new UnsupportedOperationException("NonHierarchicalDistanceBasedAlgorithm.remove not implemented"); } @Override public Set<? extends Cluster<T>> getClusters(double zoom) { final int discreteZoom = (int) zoom; final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom) / 256; final Set<QuadItem<T>> visitedCandidates = new HashSet<QuadItem<T>>(); final Set<Cluster<T>> results = new HashSet<Cluster<T>>(); final Map<QuadItem<T>, Double> distanceToCluster = new HashMap<QuadItem<T>, Double>(); final Map<QuadItem<T>, StaticCluster<T>> itemToCluster = new HashMap<QuadItem<T>, StaticCluster<T>>(); for (QuadItem<T> candidate : mItems) { if (visitedCandidates.contains(candidate)) { // Candidate is already part of another cluster. continue; } Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan); Collection<QuadItem<T>> clusterItems; synchronized (mQuadTree) { clusterItems = mQuadTree.search(searchBounds); } if (clusterItems.size() == 1) { // Only the current marker is in range. Just add the single item to the results. results.add(candidate); visitedCandidates.add(candidate); distanceToCluster.put(candidate, 0d); continue; } StaticCluster<T> cluster = new StaticCluster<T>(candidate.mClusterItem.getPosition()); results.add(cluster); for (QuadItem<T> clusterItem : clusterItems) { Double existingDistance = distanceToCluster.get(clusterItem); double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint()); if (existingDistance != null) { // Item already belongs to another cluster. Check if it's closer to this cluster. if (existingDistance < distance) { continue; } // Move item to the closer cluster. itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem); } distanceToCluster.put(clusterItem, distance); cluster.add(clusterItem.mClusterItem); itemToCluster.put(clusterItem, cluster); } visitedCandidates.addAll(clusterItems); } return results; } @Override public Collection<T> getItems() { final List<T> items = new ArrayList<T>(); synchronized (mQuadTree) { for (QuadItem<T> quadItem : mItems) { items.add(quadItem.mClusterItem); } } return items; } private double distanceSquared(Point a, Point b) { return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); } private Bounds createBoundsFromSpan(Point p, double span) { // TODO: Use a span that takes into account the visual size of the marker, not just its // LatLng. double halfSpan = span / 2; return new Bounds( p.x - halfSpan, p.x + halfSpan, p.y - halfSpan, p.y + halfSpan); } private static class QuadItem<T extends ClusterItem> implements PointQuadTree.Item, Cluster<T> { private final T mClusterItem; private final Point mPoint; private final LatLng mPosition; private Set<T> singletonSet; private QuadItem(T item) { mClusterItem = item; mPosition = item.getPosition(); mPoint = PROJECTION.toPoint(mPosition); singletonSet = Collections.singleton(mClusterItem); } @Override public Point getPoint() { return mPoint; } @Override public LatLng getPosition() { return mPosition; } @Override public Set<T> getItems() { return singletonSet; } @Override public int getSize() { return 1; } } }