16

I have to place different markers on a GoogleMap from the Google Maps Android v2 API. The problem is that multiple markers are set to the same position (lat/lng), so the user only sees the "newest" marker.

Is there a possibility (in the best case: a library) that clusters different markers from the same area (in relation to the zoomlevel)?

I've already read about the MarkerClusterer, but this is designed for the JavaScript API.

Charles
  • 50,943
  • 13
  • 104
  • 142
Greeny
  • 1,931
  • 2
  • 17
  • 26

8 Answers8

24

Google has provided a utility to do this as part of their Google Maps Android API Utility Library:

https://developers.google.com/maps/documentation/android/utility/marker-clustering

with the source at:

https://github.com/googlemaps/android-maps-utils

Intrications
  • 16,782
  • 9
  • 50
  • 50
  • This is for Google Map v1. right? We cannot use v1 for a new app anymore. – cmcromance May 29 '14 at 02:28
  • @cmcromance It is for Maps v2. Why do you think it is for v1? – Intrications May 29 '14 at 18:09
  • When I visit the dev-google pages you wrote. And I click the link "Obtaining the API Key". They said "v1 Key is not available" So I thought that information is about only v1. So, I can adjust this solution to my v2, right? I will try it. Thank you for your information. – cmcromance Jun 02 '14 at 06:58
  • Sometimes google library place the cluster marker at wrong place – Bytecode Jul 10 '14 at 08:04
  • @Bytecode Have you reported this as an issue at https://github.com/googlemaps/android-maps-utils/issues?state=open? – Intrications Jul 10 '14 at 08:32
20

I haven't had a chance to try this out yet, but it looks very promising:

http://code.google.com/p/android-maps-extensions/

From this post

Here is another library with some pretty cool looking cluster animations:

https://github.com/twotoasters/clusterkraf

Community
  • 1
  • 1
DiscDev
  • 38,652
  • 20
  • 117
  • 133
  • Yes, I added the link to your post. As I said I haven't had time to try it yet, but I will pretty soon. I'll let you know when I do. – DiscDev Mar 19 '13 at 23:08
  • 1
    The problem with all of these clustering solutions such as map extensions and Clusterkraf is that they don't take into account the main problem the original question addressed - Markers with the exact same coordinates.. Imagine a map that displays movies. It might be that 10 movies are playing at a single movie theater. That movie theater has one address that geocodes to one coordinate. Clustering will do nothing for the 10 markers that all occupy the exact same spot. An expanding spider effect for these 10 markers would be just perfect. this has been addressed in Maps v3 with JS only. – Todd Painton Jun 27 '13 at 22:19
  • 2
    @ToddPainton There is nothing that stops you from adding such effect to these libraries. I made a [PoC in AME](https://github.com/mg6maciej/android-maps-extensions/blob/develop/android-maps-extensions-demo/src/pl/mg6/android/maps/extensions/demo/DeclusterificationExampleActivity.java#L70) to show not clustered `Marker`s after clicking cluster. See [the demo app](https://play.google.com/store/apps/details?id=pl.mg6.android.maps.extensions.demo). – MaciejGórski Aug 27 '13 at 21:20
7

I'm a little late, but Clusterkraf is a great library, check it out:

https://github.com/twotoasters/clusterkraf

Leo DroidCoder
  • 14,527
  • 4
  • 62
  • 54
Androiderson
  • 16,865
  • 6
  • 62
  • 72
7

For others who are looking for implementing their own clustering code. Please check my clustering algo very fast and works amazingly good.

I looked at various libraries and found them so complex couldn't understand a word so I decided to make my own clustering algorithm. Here goes my code in java.

    static int OFFSET = 268435456;
        static double RADIUS = 85445659.4471;
        static double pi = 3.1444;

    public static double lonToX(double lon) {
        return Math.round(OFFSET + RADIUS * lon * pi / 180);
    }

    public static double latToY(double lat) {
        return Math.round(OFFSET
                - RADIUS
                * Math.log((1 + Math.sin(lat * pi / 180))
                        / (1 - Math.sin(lat * pi / 180))) / 2);
    }

    public static int pixelDistance(double lat1, double lon1, double lat2,
            double lon2, int zoom) {
        double x1 = lonToX(lon1);
        double y1 = latToY(lat1);

        double x2 = lonToX(lon2);
        double y2 = latToY(lat2);

        return (int) (Math
                .sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2))) >> (21 - zoom);
    }

    static ArrayList<Cluster> cluster(ArrayList<Marker> markers, int zoom) {

        ArrayList<Cluster> clusterList = new ArrayList<Cluster>();

        ArrayList<Marker> originalListCopy = new ArrayList<Marker>();

        for (Marker marker : markers) {
            originalListCopy.add(marker);
        }

        /* Loop until all markers have been compared. */
        for (int i = 0; i < originalListCopy.size();) {

            /* Compare against all markers which are left. */

            ArrayList<Marker> markerList = new ArrayList<Marker>();
            for (int j = i + 1; j < markers.size();) {
                int pixelDistance = pixelDistance(markers.get(i).getLatitude(),
                        markers.get(i).getLongitude(), markers.get(j)
                                .getLatitude(), markers.get(j).getLongitude(),
                        zoom);

                if (pixelDistance < 40) {


                    markerList.add(markers.get(j));

                    markers.remove(j);

                    originalListCopy.remove(j);
                    j = i + 1;
                } else {
                    j++;
                }

            }

            if (markerList.size() > 0) {
 markerList.add(markers.get(i));                
Cluster cluster = new Cluster(clusterList.size(), markerList,
                        markerList.size() + 1, originalListCopy.get(i)
                                .getLatitude(), originalListCopy.get(i)
                                .getLongitude());
                clusterList.add(cluster);
                originalListCopy.remove(i);
                markers.remove(i);
                i = 0;

            } else {
                i++;
            }

            /* If a marker has been added to cluster, add also the one */
            /* we were comparing to and remove the original from array. */

        }
        return clusterList;
    }

Just pass in your array list here containing latitude and longitude then to display clusters. Here goes the function.


    @Override
    public void onTaskCompleted(ArrayList<FlatDetails> flatDetailsList) {

        LatLngBounds.Builder builder = new LatLngBounds.Builder();

        originalListCopy = new ArrayList<FlatDetails>();
        ArrayList<Marker> markersList = new ArrayList<Marker>();
        for (FlatDetails detailList : flatDetailsList) {

            markersList.add(new Marker(detailList.getLatitude(), detailList
                    .getLongitude(), detailList.getApartmentTypeString()));

            originalListCopy.add(detailList);

            builder.include(new LatLng(detailList.getLatitude(), detailList
                    .getLongitude()));

        }

        LatLngBounds bounds = builder.build();
        int padding = 0; // offset from edges of the map in pixels
        CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, padding);

        googleMap.moveCamera(cu);

        ArrayList<Cluster> clusterList = Utils.cluster(markersList,
                (int) googleMap.getCameraPosition().zoom);

        // Removes all markers, overlays, and polylines from the map.
        googleMap.clear();

        // Zoom in, animating the camera.
        googleMap.animateCamera(CameraUpdateFactory.zoomTo(previousZoomLevel),
                2000, null);

        CircleOptions circleOptions = new CircleOptions().center(point) //
                // setcenter
                .radius(3000) // set radius in meters
                .fillColor(Color.TRANSPARENT) // default
                .strokeColor(Color.BLUE).strokeWidth(5);

        googleMap.addCircle(circleOptions);

        for (Marker detail : markersList) {

            if (detail.getBhkTypeString().equalsIgnoreCase("1 BHK")) {
                googleMap.addMarker(new MarkerOptions()
                        .position(
                                new LatLng(detail.getLatitude(), detail
                                        .getLongitude()))
                        .snippet(String.valueOf(""))
                        .title("Flat" + flatDetailsList.indexOf(detail))
                        .icon(BitmapDescriptorFactory
                                .fromResource(R.drawable.bhk1)));
            } else if (detail.getBhkTypeString().equalsIgnoreCase("2 BHK")) {
                googleMap.addMarker(new MarkerOptions()
                        .position(
                                new LatLng(detail.getLatitude(), detail
                                        .getLongitude()))
                        .snippet(String.valueOf(""))
                        .title("Flat" + flatDetailsList.indexOf(detail))
                        .icon(BitmapDescriptorFactory
                                .fromResource(R.drawable.bhk_2)));

            }

            else if (detail.getBhkTypeString().equalsIgnoreCase("3 BHK")) {
                googleMap.addMarker(new MarkerOptions()
                        .position(
                                new LatLng(detail.getLatitude(), detail
                                        .getLongitude()))
                        .snippet(String.valueOf(""))
                        .title("Flat" + flatDetailsList.indexOf(detail))
                        .icon(BitmapDescriptorFactory
                                .fromResource(R.drawable.bhk_3)));

            } else if (detail.getBhkTypeString().equalsIgnoreCase("2.5 BHK")) {
                googleMap.addMarker(new MarkerOptions()
                        .position(
                                new LatLng(detail.getLatitude(), detail
                                        .getLongitude()))
                        .snippet(String.valueOf(""))
                        .title("Flat" + flatDetailsList.indexOf(detail))
                        .icon(BitmapDescriptorFactory
                                .fromResource(R.drawable.bhk2)));

            } else if (detail.getBhkTypeString().equalsIgnoreCase("4 BHK")) {
                googleMap.addMarker(new MarkerOptions()
                        .position(
                                new LatLng(detail.getLatitude(), detail
                                        .getLongitude()))
                        .snippet(String.valueOf(""))
                        .title("Flat" + flatDetailsList.indexOf(detail))
                        .icon(BitmapDescriptorFactory
                                .fromResource(R.drawable.bhk_4)));

            } else if (detail.getBhkTypeString().equalsIgnoreCase("5 BHK")) {
                googleMap.addMarker(new MarkerOptions()
                        .position(
                                new LatLng(detail.getLatitude(), detail
                                        .getLongitude()))
                        .snippet(String.valueOf(""))
                        .title("Flat" + flatDetailsList.indexOf(detail))
                        .icon(BitmapDescriptorFactory
                                .fromResource(R.drawable.bhk5)));

            } else if (detail.getBhkTypeString().equalsIgnoreCase("5+ BHK")) {
                googleMap.addMarker(new MarkerOptions()
                        .position(
                                new LatLng(detail.getLatitude(), detail
                                        .getLongitude()))
                        .snippet(String.valueOf(""))
                        .title("Flat" + flatDetailsList.indexOf(detail))
                        .icon(BitmapDescriptorFactory
                                .fromResource(R.drawable.bhk_5)));

            }

            else if (detail.getBhkTypeString().equalsIgnoreCase("2 BHK")) {
                googleMap.addMarker(new MarkerOptions()
                        .position(
                                new LatLng(detail.getLatitude(), detail
                                        .getLongitude()))
                        .snippet(String.valueOf(""))
                        .title("Flat" + flatDetailsList.indexOf(detail))
                        .icon(BitmapDescriptorFactory
                                .fromResource(R.drawable.bhk_2)));

            }
        }

        for (Cluster cluster : clusterList) {

            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inMutable = true;
            options.inPurgeable = true;
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.cluster_marker, options);

            Canvas canvas = new Canvas(bitmap);

            Paint paint = new Paint();
            paint.setColor(getResources().getColor(R.color.white));
            paint.setTextSize(30);

            canvas.drawText(String.valueOf(cluster.getMarkerList().size()), 10,
                    40, paint);

            googleMap.addMarker(new MarkerOptions()
                    .position(
                            new LatLng(cluster.getClusterLatitude(), cluster
                                    .getClusterLongitude()))
                    .snippet(String.valueOf(cluster.getMarkerList().size()))
                    .title("Cluster")
                    .icon(BitmapDescriptorFactory.fromBitmap(bitmap)));

        }

    }
Intrications
  • 16,782
  • 9
  • 50
  • 50
user1530779
  • 409
  • 5
  • 8
5

You need to follow these steps:

  1. We need to implement / instal Google Maps Android API utility library

Android studio / Gradle:

dependencies {
    compile 'com.google.maps.android:android-maps-utils:0.3+'
}
  1. Second you need to read official documentation from Google -> Google Maps Android Marker Clustering Utility

Add a simple marker clusterer

Follow the steps below to create a simple cluster of ten markers. The result will look like this, although the number of markers shown/clustered will change depending on the zoom level:

Add a simple marker clusterer

Here is a summary of the steps required:

  1. Implement ClusterItem to represent a marker on the map. The cluster item returns the position of the marker as a LatLng object.
  2. Add a new ClusterManager to group the cluster items (markers) based on zoom level.
  3. Set the map's OnCameraChangeListener() to the ClusterManager, since ClusterManager implements the listener.
  4. If you want to add specific functionality in response to a marker click event, set the map's OnMarkerClickListener() to the ClusterManager, since ClusterManager implements the listener.
  5. Feed the markers into the ClusterManager.

Looking at the steps in more detail: To create our simple cluster of ten markers, first create a MyItem class that implements ClusterItem.

public class MyItem implements ClusterItem {
    private final LatLng mPosition;

    public MyItem(double lat, double lng) {
        mPosition = new LatLng(lat, lng);
    }

    @Override
    public LatLng getPosition() {
        return mPosition;
    }
}

In your map activity, add the ClusterManager and feed it the cluster items. Note the type argument <MyItem>, which declares the ClusterManager to be of type MyItem.

private void setUpClusterer() {
    // Declare a variable for the cluster manager.
    private ClusterManager<MyItem> mClusterManager;

    // Position the map.
    getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(51.503186, -0.126446), 10));

    // Initialize the manager with the context and the map.
    // (Activity extends context, so we can pass 'this' in the constructor.)
    mClusterManager = new ClusterManager<MyItem>(this, getMap());

    // Point the map's listeners at the listeners implemented by the cluster
    // manager.
    getMap().setOnCameraChangeListener(mClusterManager);
    getMap().setOnMarkerClickListener(mClusterManager);

    // Add cluster items (markers) to the cluster manager.
    addItems();
}

private void addItems() {

    // Set some lat/lng coordinates to start with.
    double lat = 51.5145160;
    double lng = -0.1270060;

    // Add ten cluster items in close proximity, for purposes of this example.
    for (int i = 0; i < 10; i++) {
        double offset = i / 60d;
        lat = lat + offset;
        lng = lng + offset;
        MyItem offsetItem = new MyItem(lat, lng);
        mClusterManager.addItem(offsetItem);
    }
}

Demo

https://www.youtube.com/watch?v=5ZnVraO1mT4

Github

If you need more information / completed project please visit: https://github.com/dimitardanailov/googlemapsclustermarkers

If you have more questions, please contact me.

d.danailov
  • 9,594
  • 4
  • 51
  • 36
  • I don't quite get it. Do I have to add the markers to the map too? – qwertz Jun 20 '15 at 18:01
  • @qwertz I not understand your question, but you can add markers to google maps if you don't want clusters for them. If you want to have cluster markers you need to use `ClusterManager` – d.danailov Jun 20 '15 at 18:37
  • @d.danailov, I am getting error on .addItems(offsetItem) in android studio and it says me to cast offsetitem to Collection. Can you please suggest me what is the error ? – vivek Jan 27 '16 at 13:17
  • @vivek I want to apologize for slow response, but I was out of office. Can you share screenshot with error ? – d.danailov Feb 05 '16 at 17:49
4

For who is having troubles with Clusterkraf (I can't make it work!) and for who can't use android-maps-extension cause it's shipped with a custom build of Play Services lib, I want to suggest this other library, that is tiny, well written and works out of the box:

Clusterer: https://github.com/mrmans0n/clusterer

After a whole morning spent trying using the other two libs, this one saved my day!

lorenzo-s
  • 16,603
  • 15
  • 54
  • 86
  • What do you mean by saying AME is shipped with a custom build of GPServices? – MaciejGórski Nov 01 '13 at 08:35
  • @MaciejGórski AME has it's own implementation of Google Maps API, and so it comes with it's own *google-play-services.jar*. I had troubles referencing AME from my project that references the original Google Play Services lib for push notifications, for example... – lorenzo-s Nov 01 '13 at 14:30
  • AME uses unmodified google-play-services.jar. If you use other services, just remove google-play-services_lib project and use android-maps-extensions as a replacement. I guess that's bad if there was a library working the same way e.g. for game APIs, but I'm working on decoupling it. – MaciejGórski Nov 01 '13 at 17:43
  • 1
    I gave a chance to AME and Clusterer tool and must admit the Clusterer got my favorite at a glance, there was almost no set up time and the author uses same logic as myself for collision detection ( pixel range distance ). Only 3 classes with no dependencies, I will tune it more to fit to my use-case though and to make high performing. @MaciejGórski you did also a great job with AME but your lib would suit me best if you decided for toolkit-approach rather than to extend Map but you have lot of other interesting stuff apart from clustering on board and dynamic clustering is also must-have! – comeGetSome Dec 15 '13 at 15:33
2

if you are getting only the newest marker then you need to cluster all the markers

> private ClusterManager<MyItem> mClusterManager;

after addding your items

mClusterManager.addItem("your item");
mClusterManager.cluster();
Mohammad Adil
  • 503
  • 6
  • 13
0

For those of you who are having troubles with the performance of the Google Maps Android API Utility Library, we've created our own fast clustering library: https://github.com/sharewire/google-maps-clustering. It can easily handle 20000 markers on the map.

makovkastar
  • 5,000
  • 2
  • 30
  • 50