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