Skip to content

Commit

Permalink
feat: implementation of diff
Browse files Browse the repository at this point in the history
  • Loading branch information
kikoso committed Dec 10, 2024
1 parent d915323 commit 1878230
Show file tree
Hide file tree
Showing 6 changed files with 1,529 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ project.properties
.DS_Store
.java-version
secrets.properties
.kotlin
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.maps.android.utils.demo;

import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Toast;

import androidx.annotation.NonNull;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;
import com.google.maps.android.clustering.ClusterManager;
import com.google.maps.android.clustering.view.ClusterRendererMultipleItems;
import com.google.maps.android.ui.IconGenerator;
import com.google.maps.android.utils.demo.model.Person;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

/**
* Demonstrates how to apply a diff to the current Cluster
*/
public class ClusteringDiffDemoActivity extends BaseDemoActivity implements ClusterManager.OnClusterClickListener<Person>, ClusterManager.OnClusterInfoWindowClickListener<Person>, ClusterManager.OnClusterItemClickListener<Person>, ClusterManager.OnClusterItemInfoWindowClickListener<Person> {
private ClusterManager<Person> mClusterManager;
private final Random mRandom = new Random(1984);
private Person itemtoUpdate = new Person(position(), "Teach", R.drawable.teacher);

private final Random random = new Random();
private final Handler handler = new Handler();

@Override

public void onMapReady(@NonNull GoogleMap map) {
super.onMapReady(map);
startRandomCalls();
}


/**
* Draws profile photos inside markers (using IconGenerator).
* When there are multiple people in the cluster, draw multiple photos (using MultiDrawable).
*/
private class PersonRenderer extends ClusterRendererMultipleItems<Person> {
private final IconGenerator mIconGenerator = new IconGenerator(getApplicationContext());
private final IconGenerator mClusterIconGenerator = new IconGenerator(getApplicationContext());
private final ImageView mImageView;
private final ImageView mClusterImageView;
private final int mDimension;

public PersonRenderer() {
super(getApplicationContext(), getMap(), mClusterManager);

View multiProfile = getLayoutInflater().inflate(R.layout.multi_profile, null);
mClusterIconGenerator.setContentView(multiProfile);
mClusterImageView = multiProfile.findViewById(R.id.image);

mImageView = new ImageView(getApplicationContext());
mDimension = (int) getResources().getDimension(R.dimen.custom_profile_image);
mImageView.setLayoutParams(new ViewGroup.LayoutParams(mDimension, mDimension));
int padding = (int) getResources().getDimension(R.dimen.custom_profile_padding);
mImageView.setPadding(padding, padding, padding, padding);
mIconGenerator.setContentView(mImageView);
}

public void setUpdateMarker(Person person) {
Marker marker = getMarker(person);
if (marker != null) {
marker.setIcon(getItemIcon(person));
}
}

@Override
protected void onBeforeClusterItemRendered(@NonNull Person person, @NonNull MarkerOptions markerOptions) {
// Draw a single person - show their profile photo and set the info window to show their name
markerOptions
.icon(getItemIcon(person))
.title(person.name);
}

@Override
protected void onClusterItemUpdated(@NonNull Person person, @NonNull Marker marker) {
// Same implementation as onBeforeClusterItemRendered() (to update cached markers)
marker.setIcon(getItemIcon(person));
marker.setTitle(person.name);
}

/**
* Get a descriptor for a single person (i.e., a marker outside a cluster) from their
* profile photo to be used for a marker icon
*
* @param person person to return an BitmapDescriptor for
* @return the person's profile photo as a BitmapDescriptor
*/
private BitmapDescriptor getItemIcon(Person person) {
mImageView.setImageResource(person.profilePhoto);
Bitmap icon = mIconGenerator.makeIcon();
return BitmapDescriptorFactory.fromBitmap(icon);
}

@Override
protected void onBeforeClusterRendered(@NonNull Cluster<Person> cluster, @NonNull MarkerOptions markerOptions) {
// Draw multiple people.
// Note: this method runs on the UI thread. Don't spend too much time in here (like in this example).
markerOptions.icon(getClusterIcon(cluster));
}

@Override
protected void onClusterUpdated(@NonNull Cluster<Person> cluster, Marker marker) {
// Same implementation as onBeforeClusterRendered() (to update cached markers)
marker.setIcon(getClusterIcon(cluster));
}

/**
* Get a descriptor for multiple people (a cluster) to be used for a marker icon. Note: this
* method runs on the UI thread. Don't spend too much time in here (like in this example).
*
* @param cluster cluster to draw a BitmapDescriptor for
* @return a BitmapDescriptor representing a cluster
*/
private BitmapDescriptor getClusterIcon(Cluster<Person> cluster) {
List<Drawable> profilePhotos = new ArrayList<>(Math.min(4, cluster.getSize()));
int width = mDimension;
int height = mDimension;

for (Person p : cluster.getItems()) {
// Draw 4 at most.
if (profilePhotos.size() == 4) break;
Drawable drawable = getResources().getDrawable(p.profilePhoto);
drawable.setBounds(0, 0, width, height);
profilePhotos.add(drawable);
}
MultiDrawable multiDrawable = new MultiDrawable(profilePhotos);
multiDrawable.setBounds(0, 0, width, height);

mClusterImageView.setImageDrawable(multiDrawable);
Bitmap icon = mClusterIconGenerator.makeIcon(String.valueOf(cluster.getSize()));
return BitmapDescriptorFactory.fromBitmap(icon);
}

@Override
protected boolean shouldRenderAsCluster(@NonNull Cluster<Person> cluster) {
// Always render clusters.
return cluster.getSize() >= 1;
}
}

private void startRandomCalls() {
// Initial call to the random update.
callUpdateRandom();
}

private void callUpdateRandom() {
// Generate a random delay between 1 and 5 seconds
int delay = random.nextInt(5000) + 1000; // Random delay in milliseconds (1000ms to 5000ms)

// Post the next call with the random delay
handler.postDelayed(this::callUpdateRandom, delay);
updateRandom();
}

@Override
public boolean onClusterClick(Cluster<Person> cluster) {
// Show a toast with some info when the cluster is clicked.
String firstName = cluster.getItems().iterator().next().name;
Toast.makeText(this, cluster.getSize() + " (including " + firstName + ")", Toast.LENGTH_SHORT).show();

// Zoom in the cluster. Need to create LatLngBounds and including all the cluster items
// inside of bounds, then animate to center of the bounds.

// Create the builder to collect all essential cluster items for the bounds.
LatLngBounds.Builder builder = LatLngBounds.builder();
for (ClusterItem item : cluster.getItems()) {
builder.include(item.getPosition());
}
// Get the LatLngBounds
final LatLngBounds bounds = builder.build();

// Animate camera to the bounds
try {
getMap().animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100));
} catch (Exception e) {
e.printStackTrace();
}

return true;
}

@Override
public void onClusterInfoWindowClick(Cluster<Person> cluster) {
// Does nothing, but you could go to a list of the users.
}

@Override
public boolean onClusterItemClick(Person item) {
// Does nothing, but you could go into the user's profile page, for example.
return false;
}

@Override
public void onClusterItemInfoWindowClick(Person item) {
// Does nothing, but you could go into the user's profile page, for example.
}

@Override
protected void startDemo(boolean isRestore) {
if (!isRestore) {
getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(51.503186, -0.126446), 6));
}

mClusterManager = new ClusterManager<>(this, getMap());
mClusterManager.setRenderer(new PersonRenderer());
getMap().setOnCameraIdleListener(mClusterManager);
getMap().setOnMarkerClickListener(mClusterManager);
getMap().setOnInfoWindowClickListener(mClusterManager);
mClusterManager.setOnClusterClickListener(this);
mClusterManager.setOnClusterInfoWindowClickListener(this);
mClusterManager.setOnClusterItemClickListener(this);
mClusterManager.setOnClusterItemInfoWindowClickListener(this);

addItems();
mClusterManager.cluster();
}

private void addItems() {

// http://www.flickr.com/photos/sdasmarchives/5036231225/
mClusterManager.addItem(new Person(position(), "John", R.drawable.john));


// http://www.flickr.com/photos/usnationalarchives/4726892651/
itemtoUpdate = new Person(position(), "Teach", R.drawable.teacher);
mClusterManager.addItem(itemtoUpdate);
}


private void updateRandom() {
itemtoUpdate = new Person(position(), "Teach", R.drawable.teacher);
Log.d("ClusterTest", "We start updating the item. New position: " + itemtoUpdate.getPosition().toString());

mClusterManager.updateItem(this.itemtoUpdate);

//We could also call the diff() method to add, remove and update at once.
mClusterManager.diff(null, null, new ArrayList<>(Collections.singleton(this.itemtoUpdate)));
mClusterManager.setAnimation(true);

// Cluster needs to be called, to force an update of the cluster.
mClusterManager.cluster();
}

private LatLng position() {
return new LatLng(random(51.6723432, 51.38494009999999), random(0.148271, -0.3514683));
}

private double random(double min, double max) {
return mRandom.nextDouble() * (max - min) + min;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.Objects;


public class Person implements ClusterItem {
public final String name;
public final int profilePhoto;
Expand Down Expand Up @@ -54,4 +57,18 @@ public String getSnippet() {
public Float getZIndex() {
return null;
}

@Override
public int hashCode() {
return Objects.hashCode(name);
}

// If we use the diff() operation, we need to implement an equals operation, to determine what
// makes each ClusterItem unique (which is probably not the position)
@Override
public boolean equals(@Nullable Object obj) {
if (obj != null && getClass() != obj.getClass()) return false;
Person myObj = (Person) obj;
return this.name.equals(myObj.name);
}
}
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ play-services-maps = "19.0.0"
core-ktx = "1.15.0"
robolectric = "4.12.2"
kxml2 = "2.3.0"
mockk = "1.13.11"
mockk = "1.13.13"
lint = "31.7.3"

[libraries]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;

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

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
* Groups many items on a map based on zoom level.
Expand Down Expand Up @@ -205,6 +207,31 @@ public boolean addItem(T myItem) {
}
}

public void diff(@Nullable Collection<T> add, @Nullable Collection<T> remove, @Nullable Collection<T> modify) {
final Algorithm<T> algorithm = getAlgorithm();
algorithm.lock();
try {
// Add items
if (add != null) {
for (T item : add) {
algorithm.addItem(item);
}
}

// Remove items
algorithm.removeItems(remove);

// Modify items
if (modify != null) {
for (T item : modify) {
updateItem(item);
}
}
} finally {
algorithm.unlock();
}
}

/**
* Removes items from clusters. After calling this method you must invoke {@link #cluster()} for
* the state of the clusters to be updated on the map.
Expand Down
Loading

0 comments on commit 1878230

Please sign in to comment.