-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Render only visible items on the map #217
Changes from 4 commits
1cf9f4e
7e91e4f
0ce7080
ae6c896
9db3838
8694da3
4a34125
2e288b8
42e8df3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package com.google.maps.android.utils.demo; | ||
|
||
import android.util.DisplayMetrics; | ||
import android.widget.Toast; | ||
|
||
import com.google.android.gms.maps.CameraUpdateFactory; | ||
import com.google.android.gms.maps.model.LatLng; | ||
import com.google.maps.android.clustering.ClusterManager; | ||
import com.google.maps.android.clustering.algo.VisibleNonHierarchicalDistanceBasedAlgorithm; | ||
import com.google.maps.android.utils.demo.model.MyItem; | ||
|
||
import org.json.JSONException; | ||
|
||
import java.io.InputStream; | ||
import java.util.List; | ||
|
||
public class VisibleClusteringDemoActivity extends BaseDemoActivity { | ||
private ClusterManager<MyItem> mClusterManager; | ||
|
||
@Override | ||
protected void startDemo() { | ||
DisplayMetrics metrics = new DisplayMetrics(); | ||
getWindowManager().getDefaultDisplay().getMetrics(metrics); | ||
|
||
getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(51.503186, -0.126446), 10)); | ||
|
||
mClusterManager = new ClusterManager<MyItem>(this, getMap()); | ||
mClusterManager.setClusterOnlyVisibleArea(true); | ||
mClusterManager.setAlgorithm(new VisibleNonHierarchicalDistanceBasedAlgorithm<MyItem>(metrics.widthPixels, metrics.heightPixels)); | ||
|
||
getMap().setOnCameraChangeListener(mClusterManager); | ||
|
||
try { | ||
readItems(); | ||
} catch (JSONException e) { | ||
Toast.makeText(this, "Problem reading list of markers.", Toast.LENGTH_LONG).show(); | ||
} | ||
} | ||
|
||
private void readItems() throws JSONException { | ||
InputStream inputStream = getResources().openRawResource(R.raw.radar_search); | ||
List<MyItem> items = new MyItemReader().read(inputStream); | ||
for (int i = 0; i < 100; i++) { | ||
double offset = i / 60d; | ||
for (MyItem item : items) { | ||
LatLng position = item.getPosition(); | ||
double lat = position.latitude + offset; | ||
double lng = position.longitude + offset; | ||
MyItem offsetItem = new MyItem(lat, lng); | ||
mClusterManager.addItem(offsetItem); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
package com.google.maps.android.clustering.algo; | ||
|
||
import com.google.android.gms.maps.GoogleMap; | ||
import com.google.android.gms.maps.model.CameraPosition; | ||
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.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
/** | ||
* A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not | ||
* hierarchical. This algorithm will compute clusters only in visible area. | ||
* <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 VisibleNonHierarchicalDistanceBasedAlgorithm<T extends ClusterItem> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a lot of code duplicated here from NonHierarchicalDistanceBasedAlgorithm that you could save if you extended it. Can you do that? Feel free to loosen up the member privacy as needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I didn't wanted to change initial algorithm. |
||
implements Algorithm<T>, GoogleMap.OnCameraChangeListener { | ||
|
||
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); | ||
private final int mScreenWidth; | ||
private final int mScreenHeight; | ||
private LatLng mMapCenter; | ||
|
||
public VisibleNonHierarchicalDistanceBasedAlgorithm(int screenWidth, int screenHeight) { | ||
mScreenWidth = screenWidth; | ||
mScreenHeight = screenHeight; | ||
} | ||
|
||
@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("VisibleNonHierarchicalDistanceBasedAlgorithm.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>>(); | ||
|
||
synchronized (mQuadTree) { | ||
|
||
Bounds visibleBounds = getVisibleBounds(discreteZoom); | ||
|
||
Collection<QuadItem<T>> items = mQuadTree.search(visibleBounds); | ||
|
||
for (QuadItem<T> candidate : items) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As this block is straight from the NonHierarchicalDistanceBasedAlgorithm, it'd be great to pull it out into a separate method, on the parent class, when you subclass it. |
||
if (visitedCandidates.contains(candidate)) { | ||
// Candidate is already part of another cluster. | ||
continue; | ||
} | ||
|
||
Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan); | ||
Collection<QuadItem<T>> clusterItems; | ||
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 Bounds getVisibleBounds(int zoom) { | ||
if (mMapCenter == null) { | ||
return new Bounds(0, 0, 0, 0); | ||
} | ||
|
||
Point p = PROJECTION.toPoint(mMapCenter); | ||
|
||
final double halfWidthSpan = mScreenWidth / Math.pow(2, zoom) / 256 / 2; | ||
final double halfHeightSpan = mScreenHeight / Math.pow(2, zoom) / 256 / 2; | ||
|
||
return new Bounds( | ||
p.x - halfWidthSpan, p.x + halfWidthSpan, | ||
p.y - halfHeightSpan, p.y + halfHeightSpan); | ||
} | ||
|
||
@Override | ||
public void onCameraChange(CameraPosition cameraPosition) { | ||
mMapCenter = cameraPosition.target; | ||
} | ||
|
||
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; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that a user should have to provide an algorithm as well as specifying this property. Can you refactor such that this property is defined on the Algorithm implementation? e.g. add a method to Algorithm like
boolean shouldReclusterOnMapMovement()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about back compatibility?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we could use Java 8 we could add a default method to the interface, but alas that's not an option.
How about extending Algorithm (e.g.
ViewBasedAlgorithm
) and use that to determine instead. You may even be able to move the onCameraChangeListener code in there, too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this variant too, but in this case I will have to use instanceof to check, if this algorythm is ViewBasedAlgorithm, but if this algorithm will be decorated(like you do with PreCachingAlgorithmDecorator), it will not work at all. That's why I removed this decoration by default
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, good point. My main concern is that the behaviour of using
setClusterOnlyVisibleArea
on anything other than the new Algorithm you've introduced is undefined.The only other option available is to break the Algorithm interface by adding the method suggested above. I've thought about this a bit and I think that it's the right thing to do but I (or whoever releases the next version to Maven) will need to make sure we bump the version number appropriately so it's clear what's happening.
What do you think about that? Reasonable? Or too drastic?