Skip to content

Commit 3ca965f

Browse files
barbeauarriolac
authored andcommitted
fix: Maintain insertion order in clusters
A clustering performance improvement was made in #368 by switching from using an ArrayList to a HashSet. However, ArrayLists maintain insertion order of the elements when iterating over them later, while HashSets do not. Because the cluster has the center of the first element, that center will change if the iteration order of the elements change, which results in clusters changing position when updating an item model and reclustering. This changes to using a LinkedHashSet, which should give the same constant time performance benefits as the Set for add/remove over the List, but still maintains insertion order. A unit test for insertion order is also added. Fixes #615 Refs: 615
1 parent 1f2a2dd commit 3ca965f

File tree

3 files changed

+38
-7
lines changed

3 files changed

+38
-7
lines changed

library/src/main/java/com/google/maps/android/clustering/algo/NonHierarchicalDistanceBasedAlgorithm.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,11 @@
2424
import com.google.maps.android.projection.SphericalMercatorProjection;
2525
import com.google.maps.android.quadtree.PointQuadTree;
2626

27-
import java.util.ArrayList;
2827
import java.util.Collection;
2928
import java.util.Collections;
3029
import java.util.HashMap;
3130
import java.util.HashSet;
32-
import java.util.List;
31+
import java.util.LinkedHashSet;
3332
import java.util.Map;
3433
import java.util.Set;
3534

@@ -54,7 +53,7 @@ public class NonHierarchicalDistanceBasedAlgorithm<T extends ClusterItem> extend
5453
/**
5554
* Any modifications should be synchronized on mQuadTree.
5655
*/
57-
private final Collection<QuadItem<T>> mItems = new HashSet<>();
56+
private final Collection<QuadItem<T>> mItems = new LinkedHashSet<>();
5857

5958
/**
6059
* Any modifications should be synchronized on mQuadTree.
@@ -169,7 +168,7 @@ protected Collection<QuadItem<T>> getClusteringItems(PointQuadTree<QuadItem<T>>
169168

170169
@Override
171170
public Collection<T> getItems() {
172-
final List<T> items = new ArrayList<T>();
171+
final Set<T> items = new LinkedHashSet<>();
173172
synchronized (mQuadTree) {
174173
for (QuadItem<T> quadItem : mItems) {
175174
items.add(quadItem.mClusterItem);

library/src/main/java/com/google/maps/android/quadtree/PointQuadTree.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import java.util.ArrayList;
2323
import java.util.Collection;
24-
import java.util.HashSet;
24+
import java.util.LinkedHashSet;
2525
import java.util.List;
2626
import java.util.Set;
2727

@@ -118,7 +118,7 @@ private void insert(double x, double y, T item) {
118118
return;
119119
}
120120
if (mItems == null) {
121-
mItems = new HashSet<>();
121+
mItems = new LinkedHashSet<>();
122122
}
123123
mItems.add(item);
124124
if (mItems.size() > MAX_ELEMENTS && mDepth < MAX_DEPTH) {

library/src/test/java/com/google/maps/android/clustering/QuadItemTest.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121

2222
import org.junit.Test;
2323

24+
import java.util.Collection;
25+
2426
import static org.junit.Assert.assertEquals;
2527
import static org.junit.Assert.assertFalse;
2628
import static org.junit.Assert.assertTrue;
2729

2830
public class QuadItemTest {
31+
2932
@Test
3033
public void testRemoval() {
3134
TestingItem item_1_5 = new TestingItem(0.1, 0.5);
@@ -46,10 +49,39 @@ public void testRemoval() {
4649
assertTrue(algo.getItems().contains(item_2_3));
4750
}
4851

52+
/**
53+
* Test if insertion order into the algorithm is the same as returned item order. This matters
54+
* because we want repeatable clustering behavior when updating model values and re-clustering.
55+
*/
56+
@Test
57+
public void testInsertionOrder() {
58+
NonHierarchicalDistanceBasedAlgorithm<ClusterItem> algo =
59+
new NonHierarchicalDistanceBasedAlgorithm<>();
60+
for (int i = 0; i < 100; i++) {
61+
algo.addItem(new TestingItem(Integer.toString(i), 0.0, 0.0));
62+
}
63+
64+
assertEquals(100, algo.getItems().size());
65+
66+
Collection<ClusterItem> items = algo.getItems();
67+
int counter = 0;
68+
for (ClusterItem item : items) {
69+
assertEquals(Integer.toString(counter), item.getTitle());
70+
counter++;
71+
}
72+
}
73+
4974
private class TestingItem implements ClusterItem {
5075
private final LatLng mPosition;
76+
private final String mTitle;
77+
78+
TestingItem(String title, double lat, double lng) {
79+
mTitle = title;
80+
mPosition = new LatLng(lat, lng);
81+
}
5182

5283
TestingItem(double lat, double lng) {
84+
mTitle = "";
5385
mPosition = new LatLng(lat, lng);
5486
}
5587

@@ -60,7 +92,7 @@ public LatLng getPosition() {
6092

6193
@Override
6294
public String getTitle() {
63-
return null;
95+
return mTitle;
6496
}
6597

6698
@Override

0 commit comments

Comments
 (0)