Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implementation of diff #1438

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ project.properties
.DS_Store
.java-version
secrets.properties
.kotlin
.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);
Fixed Show fixed Hide fixed
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);
Fixed Show fixed Hide fixed
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);
Dismissed Show dismissed Hide dismissed
getMap().setOnInfoWindowClickListener(mClusterManager);
Dismissed Show dismissed Hide dismissed
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"
org-jacoco-core = "0.8.11"

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
Loading