Skip to content

Commit 1878230

Browse files
committed
feat: implementation of diff
1 parent d915323 commit 1878230

File tree

6 files changed

+1529
-1
lines changed

6 files changed

+1529
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ project.properties
1111
.DS_Store
1212
.java-version
1313
secrets.properties
14+
.kotlin
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/*
2+
* Copyright 2013 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.maps.android.utils.demo;
18+
19+
import android.graphics.Bitmap;
20+
import android.graphics.drawable.Drawable;
21+
import android.os.Handler;
22+
import android.util.Log;
23+
import android.view.View;
24+
import android.view.ViewGroup;
25+
import android.widget.ImageView;
26+
import android.widget.Toast;
27+
28+
import androidx.annotation.NonNull;
29+
30+
import com.google.android.gms.maps.CameraUpdateFactory;
31+
import com.google.android.gms.maps.GoogleMap;
32+
import com.google.android.gms.maps.model.BitmapDescriptor;
33+
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
34+
import com.google.android.gms.maps.model.LatLng;
35+
import com.google.android.gms.maps.model.LatLngBounds;
36+
import com.google.android.gms.maps.model.Marker;
37+
import com.google.android.gms.maps.model.MarkerOptions;
38+
import com.google.maps.android.clustering.Cluster;
39+
import com.google.maps.android.clustering.ClusterItem;
40+
import com.google.maps.android.clustering.ClusterManager;
41+
import com.google.maps.android.clustering.view.ClusterRendererMultipleItems;
42+
import com.google.maps.android.ui.IconGenerator;
43+
import com.google.maps.android.utils.demo.model.Person;
44+
45+
import java.util.ArrayList;
46+
import java.util.Collections;
47+
import java.util.List;
48+
import java.util.Random;
49+
50+
/**
51+
* Demonstrates how to apply a diff to the current Cluster
52+
*/
53+
public class ClusteringDiffDemoActivity extends BaseDemoActivity implements ClusterManager.OnClusterClickListener<Person>, ClusterManager.OnClusterInfoWindowClickListener<Person>, ClusterManager.OnClusterItemClickListener<Person>, ClusterManager.OnClusterItemInfoWindowClickListener<Person> {
54+
private ClusterManager<Person> mClusterManager;
55+
private final Random mRandom = new Random(1984);
56+
private Person itemtoUpdate = new Person(position(), "Teach", R.drawable.teacher);
57+
58+
private final Random random = new Random();
59+
private final Handler handler = new Handler();
60+
61+
@Override
62+
63+
public void onMapReady(@NonNull GoogleMap map) {
64+
super.onMapReady(map);
65+
startRandomCalls();
66+
}
67+
68+
69+
/**
70+
* Draws profile photos inside markers (using IconGenerator).
71+
* When there are multiple people in the cluster, draw multiple photos (using MultiDrawable).
72+
*/
73+
private class PersonRenderer extends ClusterRendererMultipleItems<Person> {
74+
private final IconGenerator mIconGenerator = new IconGenerator(getApplicationContext());
75+
private final IconGenerator mClusterIconGenerator = new IconGenerator(getApplicationContext());
76+
private final ImageView mImageView;
77+
private final ImageView mClusterImageView;
78+
private final int mDimension;
79+
80+
public PersonRenderer() {
81+
super(getApplicationContext(), getMap(), mClusterManager);
82+
83+
View multiProfile = getLayoutInflater().inflate(R.layout.multi_profile, null);
84+
mClusterIconGenerator.setContentView(multiProfile);
85+
mClusterImageView = multiProfile.findViewById(R.id.image);
86+
87+
mImageView = new ImageView(getApplicationContext());
88+
mDimension = (int) getResources().getDimension(R.dimen.custom_profile_image);
89+
mImageView.setLayoutParams(new ViewGroup.LayoutParams(mDimension, mDimension));
90+
int padding = (int) getResources().getDimension(R.dimen.custom_profile_padding);
91+
mImageView.setPadding(padding, padding, padding, padding);
92+
mIconGenerator.setContentView(mImageView);
93+
}
94+
95+
public void setUpdateMarker(Person person) {
96+
Marker marker = getMarker(person);
97+
if (marker != null) {
98+
marker.setIcon(getItemIcon(person));
99+
}
100+
}
101+
102+
@Override
103+
protected void onBeforeClusterItemRendered(@NonNull Person person, @NonNull MarkerOptions markerOptions) {
104+
// Draw a single person - show their profile photo and set the info window to show their name
105+
markerOptions
106+
.icon(getItemIcon(person))
107+
.title(person.name);
108+
}
109+
110+
@Override
111+
protected void onClusterItemUpdated(@NonNull Person person, @NonNull Marker marker) {
112+
// Same implementation as onBeforeClusterItemRendered() (to update cached markers)
113+
marker.setIcon(getItemIcon(person));
114+
marker.setTitle(person.name);
115+
}
116+
117+
/**
118+
* Get a descriptor for a single person (i.e., a marker outside a cluster) from their
119+
* profile photo to be used for a marker icon
120+
*
121+
* @param person person to return an BitmapDescriptor for
122+
* @return the person's profile photo as a BitmapDescriptor
123+
*/
124+
private BitmapDescriptor getItemIcon(Person person) {
125+
mImageView.setImageResource(person.profilePhoto);
126+
Bitmap icon = mIconGenerator.makeIcon();
127+
return BitmapDescriptorFactory.fromBitmap(icon);
128+
}
129+
130+
@Override
131+
protected void onBeforeClusterRendered(@NonNull Cluster<Person> cluster, @NonNull MarkerOptions markerOptions) {
132+
// Draw multiple people.
133+
// Note: this method runs on the UI thread. Don't spend too much time in here (like in this example).
134+
markerOptions.icon(getClusterIcon(cluster));
135+
}
136+
137+
@Override
138+
protected void onClusterUpdated(@NonNull Cluster<Person> cluster, Marker marker) {
139+
// Same implementation as onBeforeClusterRendered() (to update cached markers)
140+
marker.setIcon(getClusterIcon(cluster));
141+
}
142+
143+
/**
144+
* Get a descriptor for multiple people (a cluster) to be used for a marker icon. Note: this
145+
* method runs on the UI thread. Don't spend too much time in here (like in this example).
146+
*
147+
* @param cluster cluster to draw a BitmapDescriptor for
148+
* @return a BitmapDescriptor representing a cluster
149+
*/
150+
private BitmapDescriptor getClusterIcon(Cluster<Person> cluster) {
151+
List<Drawable> profilePhotos = new ArrayList<>(Math.min(4, cluster.getSize()));
152+
int width = mDimension;
153+
int height = mDimension;
154+
155+
for (Person p : cluster.getItems()) {
156+
// Draw 4 at most.
157+
if (profilePhotos.size() == 4) break;
158+
Drawable drawable = getResources().getDrawable(p.profilePhoto);
159+
drawable.setBounds(0, 0, width, height);
160+
profilePhotos.add(drawable);
161+
}
162+
MultiDrawable multiDrawable = new MultiDrawable(profilePhotos);
163+
multiDrawable.setBounds(0, 0, width, height);
164+
165+
mClusterImageView.setImageDrawable(multiDrawable);
166+
Bitmap icon = mClusterIconGenerator.makeIcon(String.valueOf(cluster.getSize()));
167+
return BitmapDescriptorFactory.fromBitmap(icon);
168+
}
169+
170+
@Override
171+
protected boolean shouldRenderAsCluster(@NonNull Cluster<Person> cluster) {
172+
// Always render clusters.
173+
return cluster.getSize() >= 1;
174+
}
175+
}
176+
177+
private void startRandomCalls() {
178+
// Initial call to the random update.
179+
callUpdateRandom();
180+
}
181+
182+
private void callUpdateRandom() {
183+
// Generate a random delay between 1 and 5 seconds
184+
int delay = random.nextInt(5000) + 1000; // Random delay in milliseconds (1000ms to 5000ms)
185+
186+
// Post the next call with the random delay
187+
handler.postDelayed(this::callUpdateRandom, delay);
188+
updateRandom();
189+
}
190+
191+
@Override
192+
public boolean onClusterClick(Cluster<Person> cluster) {
193+
// Show a toast with some info when the cluster is clicked.
194+
String firstName = cluster.getItems().iterator().next().name;
195+
Toast.makeText(this, cluster.getSize() + " (including " + firstName + ")", Toast.LENGTH_SHORT).show();
196+
197+
// Zoom in the cluster. Need to create LatLngBounds and including all the cluster items
198+
// inside of bounds, then animate to center of the bounds.
199+
200+
// Create the builder to collect all essential cluster items for the bounds.
201+
LatLngBounds.Builder builder = LatLngBounds.builder();
202+
for (ClusterItem item : cluster.getItems()) {
203+
builder.include(item.getPosition());
204+
}
205+
// Get the LatLngBounds
206+
final LatLngBounds bounds = builder.build();
207+
208+
// Animate camera to the bounds
209+
try {
210+
getMap().animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100));
211+
} catch (Exception e) {
212+
e.printStackTrace();
213+
}
214+
215+
return true;
216+
}
217+
218+
@Override
219+
public void onClusterInfoWindowClick(Cluster<Person> cluster) {
220+
// Does nothing, but you could go to a list of the users.
221+
}
222+
223+
@Override
224+
public boolean onClusterItemClick(Person item) {
225+
// Does nothing, but you could go into the user's profile page, for example.
226+
return false;
227+
}
228+
229+
@Override
230+
public void onClusterItemInfoWindowClick(Person item) {
231+
// Does nothing, but you could go into the user's profile page, for example.
232+
}
233+
234+
@Override
235+
protected void startDemo(boolean isRestore) {
236+
if (!isRestore) {
237+
getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(51.503186, -0.126446), 6));
238+
}
239+
240+
mClusterManager = new ClusterManager<>(this, getMap());
241+
mClusterManager.setRenderer(new PersonRenderer());
242+
getMap().setOnCameraIdleListener(mClusterManager);
243+
getMap().setOnMarkerClickListener(mClusterManager);
244+
getMap().setOnInfoWindowClickListener(mClusterManager);
245+
mClusterManager.setOnClusterClickListener(this);
246+
mClusterManager.setOnClusterInfoWindowClickListener(this);
247+
mClusterManager.setOnClusterItemClickListener(this);
248+
mClusterManager.setOnClusterItemInfoWindowClickListener(this);
249+
250+
addItems();
251+
mClusterManager.cluster();
252+
}
253+
254+
private void addItems() {
255+
256+
// http://www.flickr.com/photos/sdasmarchives/5036231225/
257+
mClusterManager.addItem(new Person(position(), "John", R.drawable.john));
258+
259+
260+
// http://www.flickr.com/photos/usnationalarchives/4726892651/
261+
itemtoUpdate = new Person(position(), "Teach", R.drawable.teacher);
262+
mClusterManager.addItem(itemtoUpdate);
263+
}
264+
265+
266+
private void updateRandom() {
267+
itemtoUpdate = new Person(position(), "Teach", R.drawable.teacher);
268+
Log.d("ClusterTest", "We start updating the item. New position: " + itemtoUpdate.getPosition().toString());
269+
270+
mClusterManager.updateItem(this.itemtoUpdate);
271+
272+
//We could also call the diff() method to add, remove and update at once.
273+
mClusterManager.diff(null, null, new ArrayList<>(Collections.singleton(this.itemtoUpdate)));
274+
mClusterManager.setAnimation(true);
275+
276+
// Cluster needs to be called, to force an update of the cluster.
277+
mClusterManager.cluster();
278+
}
279+
280+
private LatLng position() {
281+
return new LatLng(random(51.6723432, 51.38494009999999), random(0.148271, -0.3514683));
282+
}
283+
284+
private double random(double min, double max) {
285+
return mRandom.nextDouble() * (max - min) + min;
286+
}
287+
}

demo/src/main/java/com/google/maps/android/utils/demo/model/Person.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import androidx.annotation.NonNull;
2323
import androidx.annotation.Nullable;
2424

25+
import java.util.Objects;
26+
27+
2528
public class Person implements ClusterItem {
2629
public final String name;
2730
public final int profilePhoto;
@@ -54,4 +57,18 @@ public String getSnippet() {
5457
public Float getZIndex() {
5558
return null;
5659
}
60+
61+
@Override
62+
public int hashCode() {
63+
return Objects.hashCode(name);
64+
}
65+
66+
// If we use the diff() operation, we need to implement an equals operation, to determine what
67+
// makes each ClusterItem unique (which is probably not the position)
68+
@Override
69+
public boolean equals(@Nullable Object obj) {
70+
if (obj != null && getClass() != obj.getClass()) return false;
71+
Person myObj = (Person) obj;
72+
return this.name.equals(myObj.name);
73+
}
5774
}

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ play-services-maps = "19.0.0"
1414
core-ktx = "1.15.0"
1515
robolectric = "4.12.2"
1616
kxml2 = "2.3.0"
17-
mockk = "1.13.11"
17+
mockk = "1.13.13"
1818
lint = "31.7.3"
1919

2020
[libraries]

library/src/main/java/com/google/maps/android/clustering/ClusterManager.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import android.content.Context;
2020
import android.os.AsyncTask;
21+
import android.util.Log;
2122

2223
import com.google.android.gms.maps.GoogleMap;
2324
import com.google.android.gms.maps.model.CameraPosition;
@@ -37,6 +38,7 @@
3738
import java.util.concurrent.locks.ReentrantReadWriteLock;
3839

3940
import androidx.annotation.NonNull;
41+
import androidx.annotation.Nullable;
4042

4143
/**
4244
* Groups many items on a map based on zoom level.
@@ -205,6 +207,31 @@ public boolean addItem(T myItem) {
205207
}
206208
}
207209

210+
public void diff(@Nullable Collection<T> add, @Nullable Collection<T> remove, @Nullable Collection<T> modify) {
211+
final Algorithm<T> algorithm = getAlgorithm();
212+
algorithm.lock();
213+
try {
214+
// Add items
215+
if (add != null) {
216+
for (T item : add) {
217+
algorithm.addItem(item);
218+
}
219+
}
220+
221+
// Remove items
222+
algorithm.removeItems(remove);
223+
224+
// Modify items
225+
if (modify != null) {
226+
for (T item : modify) {
227+
updateItem(item);
228+
}
229+
}
230+
} finally {
231+
algorithm.unlock();
232+
}
233+
}
234+
208235
/**
209236
* Removes items from clusters. After calling this method you must invoke {@link #cluster()} for
210237
* the state of the clusters to be updated on the map.

0 commit comments

Comments
 (0)