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

Fix #384 - Show clustered markers in exact same location #413

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<activity android:name=".HeatmapsPlacesDemoActivity"/>
<activity android:name=".GeoJsonDemoActivity"/>
<activity android:name=".KmlDemoActivity"/>
<activity android:name=".ClusteringSameLocationActivity"/>

</application>

Expand Down
7 changes: 7 additions & 0 deletions demo/res/raw/markers_same_location.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{ "lat" : 51.503186, "lng" : -0.126446, "title" : "Marker 1", "snippet": "Marker 1"},
{ "lat" : 51.503186, "lng" : -0.126446, "title" : "Marker 2", "snippet" : "Marker 2"},
{ "lat" : 51.503186, "lng" : -0.126446, "title" : "Marker 3", "snippet": "Marker 3"},
{ "lat" : 51.503186, "lng" : -0.126446, "title" : "Marker 4", "snippet": "Marker 4" },
{ "lat" : 51.503186, "lng" : -0.126446, "title" : "Marker 5", "snippet": "Marker 5"}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.google.maps.android.utils.demo;

import android.util.Log;
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.utils.demo.model.MyItem;

import org.json.JSONException;

import java.io.InputStream;
import java.util.List;

public class ClusteringSameLocationActivity extends BaseDemoActivity {

private ClusterManager<MyItem> mClusterManager;

@Override
protected void startDemo() {
getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(51.503186, -0.126446), 10));

mClusterManager = new ClusterManager<>(this, getMap());

getMap().setOnMarkerClickListener(mClusterManager);
getMap().setOnCameraMoveListener(mClusterManager);

try {
readItems();

mClusterManager.cluster();
} 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.markers_same_location);
List<MyItem> items = new MyItemReader().read(inputStream);

mClusterManager.addItems(items);
}
}
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: Markers in same location", ClusteringSameLocationActivity.class);
addDemo("PolyUtil.decode", PolyDecodeDemoActivity.class);
addDemo("PolyUtil.simplify", PolySimplifyDemoActivity.class);
addDemo("IconGenerator", IconGeneratorDemoActivity.class);
Expand Down
9 changes: 9 additions & 0 deletions demo/src/com/google/maps/android/utils/demo/model/MyItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ public LatLng getPosition() {
@Override
public String getSnippet() { return mSnippet; }

@Override
public ClusterItem copy(double lat, double lng) {
MyItem item = new MyItem(lat, lng);
item.setSnippet(getSnippet());
item.setTitle(getTitle());

return item;
}

/**
* Set the title of the marker
* @param title string to be set as title
Expand Down
5 changes: 5 additions & 0 deletions demo/src/com/google/maps/android/utils/demo/model/Person.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,9 @@ public String getTitle() {
public String getSnippet() {
return null;
}

@Override
public ClusterItem copy(double lat, double lng) {
return new Person(new LatLng(lat, lng), name, profilePhoto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,11 @@ public interface ClusterItem {
* The description of this marker.
*/
String getSnippet();

/**
* Produces a copy of the same object but setting the given location.
*
* @return The new object copied.
*/
ClusterItem copy(double lat, double lng);
}
78 changes: 78 additions & 0 deletions library/src/com/google/maps/android/clustering/ClusterManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@
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.ClusterItemsDistributor;
import com.google.maps.android.clustering.view.ClusterRenderer;
import com.google.maps.android.clustering.view.DefaultClusterItemsDistributor;
import com.google.maps.android.clustering.view.DefaultClusterRenderer;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
Expand All @@ -43,6 +47,7 @@
*/
public class ClusterManager<T extends ClusterItem> implements
GoogleMap.OnCameraIdleListener,
GoogleMap.OnCameraMoveListener,
GoogleMap.OnMarkerClickListener,
GoogleMap.OnInfoWindowClickListener {

Expand All @@ -53,6 +58,7 @@ public class ClusterManager<T extends ClusterItem> implements
private Algorithm<T> mAlgorithm;
private final ReadWriteLock mAlgorithmLock = new ReentrantReadWriteLock();
private ClusterRenderer<T> mRenderer;
private ClusterItemsDistributor<T> mClusterItemsDistributor;

private GoogleMap mMap;
private CameraPosition mPreviousCameraPosition;
Expand All @@ -76,7 +82,16 @@ public ClusterManager(Context context, GoogleMap map, MarkerManager markerManage
mRenderer = new DefaultClusterRenderer<T>(context, map, this);
mAlgorithm = new PreCachingAlgorithmDecorator<T>(new NonHierarchicalDistanceBasedAlgorithm<T>());
mClusterTask = new ClusterTask();
mClusterItemsDistributor = new DefaultClusterItemsDistributor<>(this);
mRenderer.onAdd();

setOnClusterClickListener(new ClusterManager.OnClusterClickListener<T>() {

@Override
public boolean onClusterClick(Cluster<T> cluster) {
return handleClusterClick(cluster);
}
});
}

public MarkerManager.Collection getMarkerCollection() {
Expand Down Expand Up @@ -123,6 +138,10 @@ public void setAnimation(boolean animate) {
mRenderer.setAnimation(animate);
}

public void setClusterItemsDistributor(ClusterItemsDistributor<T> clusterItemsDistributor) {
this.mClusterItemsDistributor = clusterItemsDistributor;
}

public ClusterRenderer<T> getRenderer() {
return mRenderer;
}
Expand Down Expand Up @@ -159,6 +178,13 @@ public void addItem(T myItem) {
}
}

public void removeItems(List<T> items) {

for (T item : items) {
removeItem(item);
}
}

public void removeItem(T item) {
mAlgorithmLock.writeLock().lock();
try {
Expand Down Expand Up @@ -206,6 +232,15 @@ public void onCameraIdle() {
cluster();
}

@Override
public void onCameraMove() {

// collect markers to the original position if they were relocated
if (mMap.getCameraPosition().zoom < mMap.getMaxZoomLevel()) {
mClusterItemsDistributor.collect();
}
}

@Override
public boolean onMarkerClick(Marker marker) {
return getMarkerManager().onMarkerClick(marker);
Expand All @@ -216,6 +251,49 @@ public void onInfoWindowClick(Marker marker) {
getMarkerManager().onInfoWindowClick(marker);
}

public boolean itemsInSameLocation(Cluster<T> cluster) {
Collection<T> items = cluster.getItems();

if (items.size() < 2) {
return false;
}

Iterator<T> iterator = items.iterator();
T item = iterator.next();

double longitude = item.getPosition().longitude;
double latitude = item.getPosition().latitude;

while (iterator.hasNext()) {
T t = iterator.next();

if (Double.compare(longitude, t.getPosition().longitude) != 0 && Double.compare(latitude, t.getPosition().latitude) != 0) {
return false;
}
}

return true;
}

public boolean handleClusterClick(Cluster<T> cluster) {
float maxZoomLevel = mMap.getMaxZoomLevel();
float currentZoomLevel = mMap.getCameraPosition().zoom;

// only show markers if users is in the max zoom level
if (currentZoomLevel != maxZoomLevel) {
return false;
}

if (!itemsInSameLocation(cluster)) {
return false;
}

// relocate the markers as defined in the distributor
mClusterItemsDistributor.distribute(cluster);

return true;
}

/**
* Runs the clustering algorithm in a background thread, then re-paints when results come back.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.google.maps.android.clustering.view;

import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;

/**
* It distributes the items in a cluster.
*/
public interface ClusterItemsDistributor<T extends ClusterItem> {

/**
* Proceed with the distribution of the items in a cluster.
*/
void distribute(Cluster<T> cluster);

/**
* Proceed to collect the items back to their previous state.
*/
void collect();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.google.maps.android.clustering.view;

import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;
import com.google.maps.android.clustering.ClusterManager;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* The default distributor of items included in a cluster. It distributes the items around the original lat/lng in a given radius.
*
* @param <T> Cluster item type.
*/
public class DefaultClusterItemsDistributor<T extends ClusterItem> implements ClusterItemsDistributor<T> {

private static final double DEFAULT_RADIUS = 0.00003;

private static final String DEFAULT_DELETE_LIST = "itemsDeleted";

private static final String DEFAULT_ADDED_LIST = "itemsAdded";

private double mDistributionRadius;

private ClusterManager<T> mClusterManager;

private Map<String, List<T>> mItemsCache;

public DefaultClusterItemsDistributor(ClusterManager<T> clusterManager) {
this(clusterManager, DEFAULT_RADIUS);
}

public DefaultClusterItemsDistributor(ClusterManager<T> clusterManager, double distributionRadius) {
mClusterManager = clusterManager;
mDistributionRadius = distributionRadius;
mItemsCache = new HashMap<>();
mItemsCache.put(DEFAULT_ADDED_LIST, new ArrayList<T>());
mItemsCache.put(DEFAULT_DELETE_LIST, new ArrayList<T>());
}

@Override
public void distribute(Cluster<T> cluster) {
// relocate the markers around the original markers position
int counter = 0;
float rotateFactor = (360 / cluster.getItems().size());

for (T item : cluster.getItems()) {
double lat = item.getPosition().latitude + (mDistributionRadius * Math.cos(++counter * rotateFactor));
double lng = item.getPosition().longitude + (mDistributionRadius * Math.sin(counter * rotateFactor));
T copy = (T) item.copy(lat, lng);

mClusterManager.removeItem(item);
mClusterManager.addItem(copy);
mClusterManager.cluster();

mItemsCache.get(DEFAULT_ADDED_LIST).add(copy);
mItemsCache.get(DEFAULT_DELETE_LIST).add(item);
}
}

public void collect() {
// collect the items
mClusterManager.removeItems(mItemsCache.get(DEFAULT_ADDED_LIST));
mClusterManager.addItems(mItemsCache.get(DEFAULT_DELETE_LIST));
mClusterManager.cluster();

mItemsCache.get(DEFAULT_ADDED_LIST).clear();
mItemsCache.get(DEFAULT_DELETE_LIST).clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public String getTitle() {
public String getSnippet() {
return null;
}

@Override
public TestingItem copy(double lat, double lng) {
return new TestingItem(lat, lng);
}
}

public void setUp() {
Expand Down