1
+ /*
2
+ * Copyright 2025 Google LLC
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 .annotation .SuppressLint ;
20
+ import android .graphics .Bitmap ;
21
+ import android .graphics .drawable .Drawable ;
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
+ import androidx .core .content .res .ResourcesCompat ;
30
+
31
+ import com .google .android .gms .maps .CameraUpdateFactory ;
32
+ import com .google .android .gms .maps .GoogleMap ;
33
+ import com .google .android .gms .maps .model .BitmapDescriptor ;
34
+ import com .google .android .gms .maps .model .BitmapDescriptorFactory ;
35
+ import com .google .android .gms .maps .model .LatLng ;
36
+ import com .google .android .gms .maps .model .LatLngBounds ;
37
+ import com .google .android .gms .maps .model .Marker ;
38
+ import com .google .android .gms .maps .model .MarkerOptions ;
39
+ import com .google .maps .android .clustering .Cluster ;
40
+ import com .google .maps .android .clustering .ClusterItem ;
41
+ import com .google .maps .android .clustering .ClusterManager ;
42
+ import com .google .maps .android .clustering .view .ClusterRendererMultipleItems ;
43
+ import com .google .maps .android .ui .IconGenerator ;
44
+ import com .google .maps .android .utils .demo .model .Person ;
45
+
46
+ import java .util .ArrayList ;
47
+ import java .util .List ;
48
+
49
+ /**
50
+ * Demonstrates how to apply a diff to the current Cluster
51
+ */
52
+ public class ClusteringDiffDemoActivity extends BaseDemoActivity implements ClusterManager .OnClusterClickListener <Person >, ClusterManager .OnClusterInfoWindowClickListener <Person >, ClusterManager .OnClusterItemClickListener <Person >, ClusterManager .OnClusterItemInfoWindowClickListener <Person > {
53
+ private ClusterManager <Person > mClusterManager ;
54
+ private Person itemtoUpdate = new Person (ENFIELD , "Teach" , R .drawable .teacher );
55
+
56
+ private static final LatLng ENFIELD = new LatLng (51.6524 , -0.0838 );
57
+ private static final LatLng ILFORD = new LatLng (51.5590 , -0.0815 );
58
+
59
+ private static final LatLng LONDON = new LatLng (51.5074 , -0.1278 );
60
+ LatLng midpoint = getMidpoint ();
61
+ private int currentLocationIndex = 0 ;
62
+
63
+ protected int getLayoutId () {
64
+ return R .layout .map_with_floating_button ;
65
+ }
66
+
67
+ @ Override
68
+ public void onMapReady (@ NonNull GoogleMap map ) {
69
+ super .onMapReady (map );
70
+ findViewById (R .id .fab_rotate_location ).setOnClickListener (v -> rotateLocation ());
71
+ getMap ().animateCamera (CameraUpdateFactory .newLatLngZoom (midpoint , 12 ));
72
+ }
73
+
74
+
75
+ private LatLng getMidpoint () {
76
+ double latitude = (ClusteringDiffDemoActivity .ENFIELD .latitude + ClusteringDiffDemoActivity .ILFORD .latitude + ClusteringDiffDemoActivity .LONDON .latitude ) / 3 ;
77
+ double longitude = (ClusteringDiffDemoActivity .ENFIELD .longitude + ClusteringDiffDemoActivity .ILFORD .longitude + ClusteringDiffDemoActivity .LONDON .longitude ) / 3 ;
78
+ return new LatLng (latitude , longitude );
79
+ }
80
+
81
+ /**
82
+ * Draws profile photos inside markers (using IconGenerator).
83
+ * When there are multiple people in the cluster, draw multiple photos (using MultiDrawable).
84
+ */
85
+ @ SuppressLint ("InflateParams" )
86
+ private class PersonRenderer extends ClusterRendererMultipleItems <Person > {
87
+ private final IconGenerator mIconGenerator = new IconGenerator (getApplicationContext ());
88
+ private final IconGenerator mClusterIconGenerator = new IconGenerator (getApplicationContext ());
89
+ private final ImageView mImageView ;
90
+ private final ImageView mClusterImageView ;
91
+ private final int mDimension ;
92
+
93
+ public PersonRenderer () {
94
+ super (getApplicationContext (), getMap (), mClusterManager );
95
+
96
+ View multiProfile = getLayoutInflater ().inflate (R .layout .multi_profile , null );
97
+ mClusterIconGenerator .setContentView (multiProfile );
98
+ mClusterImageView = multiProfile .findViewById (R .id .image );
99
+
100
+ mImageView = new ImageView (getApplicationContext ());
101
+ mDimension = (int ) getResources ().getDimension (R .dimen .custom_profile_image );
102
+ mImageView .setLayoutParams (new ViewGroup .LayoutParams (mDimension , mDimension ));
103
+ int padding = (int ) getResources ().getDimension (R .dimen .custom_profile_padding );
104
+ mImageView .setPadding (padding , padding , padding , padding );
105
+ mIconGenerator .setContentView (mImageView );
106
+ }
107
+
108
+ @ Override
109
+ protected void onBeforeClusterItemRendered (@ NonNull Person person , @ NonNull MarkerOptions markerOptions ) {
110
+ // Draw a single person - show their profile photo and set the info window to show their name
111
+ markerOptions
112
+ .icon (getItemIcon (person ))
113
+ .title (person .name );
114
+ }
115
+
116
+ @ Override
117
+ protected void onClusterItemUpdated (@ NonNull Person person , @ NonNull Marker marker ) {
118
+ // Same implementation as onBeforeClusterItemRendered() (to update cached markers)
119
+ marker .setIcon (getItemIcon (person ));
120
+ marker .setTitle (person .name );
121
+ }
122
+
123
+ /**
124
+ * Get a descriptor for a single person (i.e., a marker outside a cluster) from their
125
+ * profile photo to be used for a marker icon
126
+ *
127
+ * @param person person to return an BitmapDescriptor for
128
+ * @return the person's profile photo as a BitmapDescriptor
129
+ */
130
+ private BitmapDescriptor getItemIcon (Person person ) {
131
+ mImageView .setImageResource (person .profilePhoto );
132
+ Bitmap icon = mIconGenerator .makeIcon ();
133
+ return BitmapDescriptorFactory .fromBitmap (icon );
134
+ }
135
+
136
+ @ Override
137
+ protected void onBeforeClusterRendered (@ NonNull Cluster <Person > cluster , @ NonNull MarkerOptions markerOptions ) {
138
+ // Draw multiple people.
139
+ // Note: this method runs on the UI thread. Don't spend too much time in here (like in this example).
140
+ markerOptions .icon (getClusterIcon (cluster ));
141
+ }
142
+
143
+ @ Override
144
+ protected void onClusterUpdated (@ NonNull Cluster <Person > cluster , @ NonNull Marker marker ) {
145
+ // Same implementation as onBeforeClusterRendered() (to update cached markers)
146
+ marker .setIcon (getClusterIcon (cluster ));
147
+ }
148
+
149
+ /**
150
+ * Get a descriptor for multiple people (a cluster) to be used for a marker icon. Note: this
151
+ * method runs on the UI thread. Don't spend too much time in here (like in this example).
152
+ *
153
+ * @param cluster cluster to draw a BitmapDescriptor for
154
+ * @return a BitmapDescriptor representing a cluster
155
+ */
156
+ private BitmapDescriptor getClusterIcon (Cluster <Person > cluster ) {
157
+ List <Drawable > profilePhotos = new ArrayList <>(Math .min (4 , cluster .getSize ()));
158
+ int width = mDimension ;
159
+ int height = mDimension ;
160
+
161
+ for (Person p : cluster .getItems ()) {
162
+ // Draw 4 at most.
163
+ if (profilePhotos .size () == 4 ) break ;
164
+ Drawable drawable = ResourcesCompat .getDrawable (getBaseContext ().getResources (), p .profilePhoto , null );
165
+ if (drawable != null ) {
166
+ drawable .setBounds (0 , 0 , width , height );
167
+ }
168
+ profilePhotos .add (drawable );
169
+ }
170
+ MultiDrawable multiDrawable = new MultiDrawable (profilePhotos );
171
+ multiDrawable .setBounds (0 , 0 , width , height );
172
+
173
+ mClusterImageView .setImageDrawable (multiDrawable );
174
+ Bitmap icon = mClusterIconGenerator .makeIcon (String .valueOf (cluster .getSize ()));
175
+ return BitmapDescriptorFactory .fromBitmap (icon );
176
+ }
177
+
178
+ @ Override
179
+ protected boolean shouldRenderAsCluster (@ NonNull Cluster <Person > cluster ) {
180
+ return cluster .getSize () >= 2 ;
181
+ }
182
+ }
183
+
184
+
185
+ @ Override
186
+ public boolean onClusterClick (Cluster <Person > cluster ) {
187
+ // Show a toast with some info when the cluster is clicked.
188
+ String firstName = cluster .getItems ().iterator ().next ().name ;
189
+ Toast .makeText (this , cluster .getSize () + " (including " + firstName + ")" , Toast .LENGTH_SHORT ).show ();
190
+
191
+ // Zoom in the cluster. Need to create LatLngBounds and including all the cluster items
192
+ // inside of bounds, then animate to center of the bounds.
193
+
194
+ // Create the builder to collect all essential cluster items for the bounds.
195
+ LatLngBounds .Builder builder = LatLngBounds .builder ();
196
+ for (ClusterItem item : cluster .getItems ()) {
197
+ builder .include (item .getPosition ());
198
+ }
199
+ // Get the LatLngBounds
200
+ final LatLngBounds bounds = builder .build ();
201
+
202
+ // Animate camera to the bounds
203
+ try {
204
+ getMap ().animateCamera (CameraUpdateFactory .newLatLngBounds (bounds , 100 ));
205
+ } catch (Exception e ) {
206
+ e .printStackTrace ();
207
+ }
208
+
209
+ return true ;
210
+ }
211
+
212
+ @ Override
213
+ public void onClusterInfoWindowClick (Cluster <Person > cluster ) {
214
+ // Does nothing, but you could go to a list of the users.
215
+ }
216
+
217
+ @ Override
218
+ public boolean onClusterItemClick (Person item ) {
219
+ // Does nothing, but you could go into the user's profile page, for example.
220
+ return false ;
221
+ }
222
+
223
+ @ Override
224
+ public void onClusterItemInfoWindowClick (Person item ) {
225
+ // Does nothing, but you could go into the user's profile page, for example.
226
+ }
227
+
228
+ @ Override
229
+ protected void startDemo (boolean isRestore ) {
230
+ if (!isRestore ) {
231
+ getMap ().moveCamera (CameraUpdateFactory .newLatLngZoom (new LatLng (51.503186 , -0.126446 ), 6 ));
232
+ }
233
+
234
+ mClusterManager = new ClusterManager <>(this , getMap ());
235
+ mClusterManager .setRenderer (new PersonRenderer ());
236
+ getMap ().setOnCameraIdleListener (mClusterManager );
237
+ getMap ().setOnMarkerClickListener (mClusterManager );
238
+ getMap ().setOnInfoWindowClickListener (mClusterManager );
239
+ mClusterManager .setOnClusterClickListener (this );
240
+ mClusterManager .setOnClusterInfoWindowClickListener (this );
241
+ mClusterManager .setOnClusterItemClickListener (this );
242
+ mClusterManager .setOnClusterItemInfoWindowClickListener (this );
243
+
244
+ addItems ();
245
+ mClusterManager .cluster ();
246
+ }
247
+
248
+ private void addItems () {
249
+ // Marker in Enfield
250
+ mClusterManager .addItem (new Person (ENFIELD , "John" , R .drawable .john ));
251
+
252
+ // Marker in the center of London
253
+ itemtoUpdate = new Person (LONDON , "Teach" , R .drawable .teacher );
254
+ mClusterManager .addItem (itemtoUpdate );
255
+ }
256
+
257
+ private void rotateLocation () {
258
+ // Update the current index to cycle through locations (0 = Enfield, 1 = Olford, 2 = London)
259
+ currentLocationIndex = (currentLocationIndex + 1 ) % 3 ;
260
+
261
+
262
+ LatLng newLocation = switch (currentLocationIndex ) {
263
+ case 0 -> ENFIELD ;
264
+ case 1 -> ILFORD ;
265
+ default -> LONDON ;
266
+ };
267
+
268
+ String cityName = getCityName (newLocation );
269
+
270
+ Log .d ("ClusterTest" , "Item rotated to: " + newLocation .toString () + ", City: " + cityName );
271
+
272
+ if (itemtoUpdate != null ) {
273
+ itemtoUpdate = new Person (newLocation , "Teach" , R .drawable .teacher );
274
+ mClusterManager .updateItem (itemtoUpdate ); // Update the marker
275
+ mClusterManager .cluster ();
276
+ }
277
+ }
278
+
279
+ // Method to map LatLng to city name
280
+ private String getCityName (LatLng location ) {
281
+ if (areLocationsEqual (location , ENFIELD )) {
282
+ return "Enfield" ;
283
+ } else if (areLocationsEqual (location , ILFORD )) {
284
+ return "Ilford" ;
285
+ } else if (areLocationsEqual (location , LONDON )) {
286
+ return "London" ;
287
+ } else {
288
+ return "Unknown City" ; // Default case if location is not recognized
289
+ }
290
+ }
291
+
292
+ // Method to compare LatLng objects with a tolerance
293
+ private boolean areLocationsEqual (LatLng loc1 , LatLng loc2 ) {
294
+ return Math .abs (loc1 .latitude - loc2 .latitude ) < 1E-5 &&
295
+ Math .abs (loc1 .longitude - loc2 .longitude ) < 1E-5 ;
296
+ }
297
+ }
0 commit comments