Skip to content
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

Merged
merged 9 commits into from
Apr 17, 2018
1 change: 1 addition & 0 deletions demo/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<activity android:name=".DistanceDemoActivity"/>
<activity android:name=".ClusteringDemoActivity"/>
<activity android:name=".BigClusteringDemoActivity"/>
<activity android:name=".VisibleClusteringDemoActivity"/>
<activity android:name=".CustomMarkerClusteringDemoActivity"/>
<activity android:name=".TileProviderAndProjectionDemo"/>
<activity android:name=".HeatmapsDemoActivity"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ protected void onCreate(Bundle savedInstanceState) {
addDemo("Clustering", ClusteringDemoActivity.class);
addDemo("Clustering: Custom Look", CustomMarkerClusteringDemoActivity.class);
addDemo("Clustering: 2K markers", BigClusteringDemoActivity.class);
addDemo("Clustering: 20k only visible markers", VisibleClusteringDemoActivity.class);
addDemo("PolyUtil.decode", PolyDecodeDemoActivity.class);
addDemo("PolyUtil.simplify", PolySimplifyDemoActivity.class);
addDemo("IconGenerator", IconGeneratorDemoActivity.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2016 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.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.NonHierarchicalViewBasedAlgorithm;
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.setAlgorithm(new NonHierarchicalViewBasedAlgorithm<MyItem>(
metrics.widthPixels, metrics.heightPixels));

getMap().setOnCameraIdleListener(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);
}
}
}
}
39 changes: 30 additions & 9 deletions library/src/com/google/maps/android/clustering/ClusterManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
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.algo.ScreenBasedAlgorithm;
import com.google.maps.android.clustering.algo.ScreenBasedAlgorithmAdapter;
import com.google.maps.android.clustering.view.ClusterRenderer;
import com.google.maps.android.clustering.view.DefaultClusterRenderer;

Expand All @@ -50,7 +52,7 @@ public class ClusterManager<T extends ClusterItem> implements
private final MarkerManager.Collection mMarkers;
private final MarkerManager.Collection mClusterMarkers;

private Algorithm<T> mAlgorithm;
private ScreenBasedAlgorithm<T> mAlgorithm;
private final ReadWriteLock mAlgorithmLock = new ReentrantReadWriteLock();
private ClusterRenderer<T> mRenderer;

Expand All @@ -74,7 +76,9 @@ public ClusterManager(Context context, GoogleMap map, MarkerManager markerManage
mClusterMarkers = markerManager.newCollection();
mMarkers = markerManager.newCollection();
mRenderer = new DefaultClusterRenderer<T>(context, map, this);
mAlgorithm = new PreCachingAlgorithmDecorator<T>(new NonHierarchicalDistanceBasedAlgorithm<T>());
mAlgorithm = new ScreenBasedAlgorithmAdapter<T>(new PreCachingAlgorithmDecorator<T>(
new NonHierarchicalDistanceBasedAlgorithm<T>()));

mClusterTask = new ClusterTask();
mRenderer.onAdd();
}
Expand Down Expand Up @@ -107,15 +111,29 @@ public void setRenderer(ClusterRenderer<T> view) {
}

public void setAlgorithm(Algorithm<T> algorithm) {
if (algorithm instanceof ScreenBasedAlgorithm) {
setAlgorithm((ScreenBasedAlgorithm) algorithm);
} else {
setAlgorithm(new ScreenBasedAlgorithmAdapter<T>(algorithm));
}
}

public void setAlgorithm(ScreenBasedAlgorithm<T> algorithm) {
mAlgorithmLock.writeLock().lock();
try {
if (mAlgorithm != null) {
algorithm.addItems(mAlgorithm.getItems());
}
mAlgorithm = new PreCachingAlgorithmDecorator<T>(algorithm);

mAlgorithm = algorithm;
} finally {
mAlgorithmLock.writeLock().unlock();
}

if (mAlgorithm.shouldReclusterOnMapMovement()) {
mAlgorithm.onCameraChange(mMap.getCameraPosition());
}

cluster();
}

Expand Down Expand Up @@ -192,14 +210,17 @@ public void onCameraIdle() {
((GoogleMap.OnCameraIdleListener) mRenderer).onCameraIdle();
}

mAlgorithm.onCameraChange(mMap.getCameraPosition());

// delegate clustering to the algorithm
if (mAlgorithm.shouldReclusterOnMapMovement()) {
cluster();

// 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;
} else if (mPreviousCameraPosition == null || mPreviousCameraPosition.zoom != mMap.getCameraPosition().zoom) {
mPreviousCameraPosition = mMap.getCameraPosition();
cluster();
}
mPreviousCameraPosition = mMap.getCameraPosition();

cluster();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public Set<? extends Cluster<T>> getClusters(double zoom) {
final Map<QuadItem<T>, StaticCluster<T>> itemToCluster = new HashMap<QuadItem<T>, StaticCluster<T>>();

synchronized (mQuadTree) {
for (QuadItem<T> candidate : mItems) {
for (QuadItem<T> candidate : getClusteringItems(mQuadTree, discreteZoom)) {
if (visitedCandidates.contains(candidate)) {
// Candidate is already part of another cluster.
continue;
Expand Down Expand Up @@ -148,6 +148,10 @@ public Set<? extends Cluster<T>> getClusters(double zoom) {
return results;
}

protected Collection<QuadItem<T>> getClusteringItems(PointQuadTree<QuadItem<T>> quadTree, int discreteZoom) {
return mItems;
}

@Override
public Collection<T> getItems() {
final List<T> items = new ArrayList<T>();
Expand All @@ -172,7 +176,7 @@ private Bounds createBoundsFromSpan(Point p, double span) {
p.y - halfSpan, p.y + halfSpan);
}

private static class QuadItem<T extends ClusterItem> implements PointQuadTree.Item, Cluster<T> {
static class QuadItem<T extends ClusterItem> implements PointQuadTree.Item, Cluster<T> {
private final T mClusterItem;
private final Point mPoint;
private final LatLng mPosition;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2016 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.algo;

import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.maps.android.clustering.ClusterItem;
import com.google.maps.android.geometry.Bounds;
import com.google.maps.android.projection.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import com.google.maps.android.quadtree.PointQuadTree;

import java.util.Collection;

/**
* This algorithm works the same way as {@link NonHierarchicalDistanceBasedAlgorithm} but works, only in
* visible area. It requires to be reclustered on camera movement because clustering is done only for visible area.
* @param <T>
*/
public class NonHierarchicalViewBasedAlgorithm<T extends ClusterItem>
extends NonHierarchicalDistanceBasedAlgorithm<T> implements ScreenBasedAlgorithm<T> {

private static final SphericalMercatorProjection PROJECTION = new SphericalMercatorProjection(1);

private int mViewWidth;
private int mViewHeight;

private LatLng mMapCenter;

public NonHierarchicalViewBasedAlgorithm(int screenWidth, int screenHeight) {
mViewWidth = screenWidth;
mViewHeight = screenHeight;
}

@Override
public void onCameraChange(CameraPosition cameraPosition) {
mMapCenter = cameraPosition.target;
}

@Override
protected Collection<QuadItem<T>> getClusteringItems(PointQuadTree<QuadItem<T>> quadTree, int discreteZoom) {
return quadTree.search(getVisibleBounds(discreteZoom));
}

@Override
public boolean shouldReclusterOnMapMovement() {
return true;
}

/**
* Update view width and height in case map size was changed.
* You need to recluster all the clusters, to update view state after view size changes.
* @param width map width
* @param height map height
*/
public void updateViewSize(int width, int height) {
mViewWidth = width;
mViewHeight = height;
}

private Bounds getVisibleBounds(int zoom) {
if (mMapCenter == null) {
return new Bounds(0, 0, 0, 0);
}

Point p = PROJECTION.toPoint(mMapCenter);

final double halfWidthSpan = mViewWidth / Math.pow(2, zoom) / 256 / 2;
final double halfHeightSpan = mViewHeight / Math.pow(2, zoom) / 256 / 2;

return new Bounds(
p.x - halfWidthSpan, p.x + halfWidthSpan,
p.y - halfHeightSpan, p.y + halfHeightSpan);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2016 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.algo;

import com.google.android.gms.maps.GoogleMap;
import com.google.maps.android.clustering.ClusterItem;

/**
*
* This algorithm uses map position for clustering, and should be reclustered on map movement
* @param <T>
*/

public interface ScreenBasedAlgorithm<T extends ClusterItem> extends Algorithm<T>, GoogleMap.OnCameraChangeListener {

boolean shouldReclusterOnMapMovement();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2016 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.algo;

import com.google.android.gms.maps.model.CameraPosition;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;

import java.util.Collection;
import java.util.Set;

public class ScreenBasedAlgorithmAdapter<T extends ClusterItem> implements ScreenBasedAlgorithm<T> {

private Algorithm<T> mAlgorithm;

public ScreenBasedAlgorithmAdapter(Algorithm<T> algorithm) {
mAlgorithm = algorithm;
}

@Override
public boolean shouldReclusterOnMapMovement() {
return false;
}

@Override
public void addItem(T item) {
mAlgorithm.addItem(item);
}

@Override
public void addItems(Collection<T> items) {
mAlgorithm.addItems(items);
}

@Override
public void clearItems() {
mAlgorithm.clearItems();
}

@Override
public void removeItem(T item) {
mAlgorithm.removeItem(item);
}

@Override
public Set<? extends Cluster<T>> getClusters(double zoom) {
return mAlgorithm.getClusters(zoom);
}

@Override
public Collection<T> getItems() {
return mAlgorithm.getItems();
}

@Override
public void onCameraChange(CameraPosition cameraPosition) {
// stub
}
}