Working with Google Maps in Android. Clustering of markers

Often, when drafting a project a situation arises where the assigned tasks are difficult to solve by existing standard development tools. Then third-party resources come to the aid in the form of libraries and other goodies. So it happened with our newly developed application MegaSOS for Android platform. But let me cover each thing in its turn.

Android is the fastest growing mobile operating system based on the Linux kernel, and having its own Java implementation from Google. It’s the brainchild of the search giant, which not only has released an open source platform, but also created the Open Handset Alliance, through which we have SDK at our disposal.

Google has adapted its many services under the mobile platform, and Google Maps being among them. Just what we will be talking about today.

Android Google Maps APIv2 is quite topical at the moment. I think it’s needless to talk about how to connect the service, get the keys and start to work - you can easily find such information on the Internet. So we'll go straight to the point.

When designing the project the task was to display the map markers, which are divided into several types (with help of Google Maps markers clustering). But, for example, nearby markers when zoomed out overlapped the way it all looked like one continuous blur. They were to be grouped - clustered - with regard to the type of each marker. Standard toolkit looked poorly for solving such problems. Of course, you could create your own class that inherits markers, and extending it at the same time. But why reinvent the wheel if a similar problem could have being solved before you. After a careful search we found two libraries designed if not completely, then to a great extent to simplify our lives without reading huge tutorial. This is unpretentious Android Maps Extensions and sometimes matchless Clusterkraft.

Android Maps Extensions

This library extends the service possibilities of GoogleMap and related facilities for Android API v2. Its main advantage is so necessary for us clustering of overlapping markers. It can be seen on the following screenshots:

Working with Google Maps in Android. Clustering of markers

The developers have indicated a whole list of classes and methods that can be used for the mentioned tasks:

  • Marker Clustering with GoogleMap.setClustering(ClusteringSettings)
  • ClusteringSettings.clusterOptionsProvider(ClusterOptionsProvider) is required for it all to work
  • ClusteringSettings.addMarkersDynamically when having too many markers
  • ClusteringSettings.clusterSize to control... cluster size
  • Object getData() and setData(Object) on Marker, Circle, GroundOverlay, Polygon, Polyline or TileOverlay
  • List GoogleMap.getMarkers() and
  • List GoogleMap.getCircles()
  • List GoogleMap.getGroundOverlays() etc.
  • boolean Circle.contains(LatLng)
  • Marker GoogleMap.getMarkerShowingInfoWindow()
  • List GoogleMap.getDisplayedMarkers()
  • float GoogleMap.getMinZoomLevelNotClustered(Marker)
  • Marker.animatePosition(LatLng target, AnimationSettings settings)
  • Marker.setClusterGroup(int)

At the time we were developing our project, this library was unable to refer to different groups of markers. This led to a clustering markers in Google Maps absolutely everywhere, so finally we decided not to use it. We reported the problem to developers of the library, which in turn do not leave us without attention, and now we have a wonderful and convenient method Marker.setClusterGroup(int).

The library is easy to use and for its integration you are just to import it as an external module. Then, using the main activity SupportMapFragment or MapView в xml-файле, in xml-file, cause getExtendedMap instead of getMap, which is used in the standard library.

Clusterkraft

This library was used in this project instead of Maps Extensions, and immediately proved to be a positive side. Firstly, its work turned to be much more stable, dynamic addition of markers worked much faster, and clustering was performed with a nice animation. Secondly, it showed the class that provides the opportunity to replace the icon of the marker cluster, depending on its content.

Working with Google Maps in Android. Clustering of markers

For integration the library was imported as a module.

Activity lay-out, which displays a map looks like this:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                tools:context=".Map">
    <FrameLayout
            android:id="@+id/frame_map"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            class="com.stfalcon.megasos.views.MySupportMapFragment">
    </FrameLayout>
    <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/transparent" />
</RelativeLayout>

In this case, for the card FrameLayout - frame_map was used. Empty FrameLayout with the transparent background acted as a «magic spell», which solved the problem with drawing the map on devices with versions prior to Ice Cream Sandwich (when opening / closing the pop-up menu, or slide a black area in the form of the window remained on the map).

onCreate method is as follows:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_map);
        SupportMapFragment mapFragment = SupportMapFragment.newInstance();
        getSupportFragmentManager().beginTransaction().add(R.id.frame_map, mapFragment, "MAP").commit();
    }

In onPostCreate method a card is initialized and we can put a marker:

@Override
    public void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        GoogleMap myMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentByTag("MAP")).getMap();
        myMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
        initMap();
        MarkerOptions options = new MarkerOptions()
                .anchor(0.5f, 0.5f)
                .position(new LatLng(latitude, longitude))
                .draggable(false)
                .icon(BitmapDescriptorFactory
                        .fromResource(marker_resource));
        myMap.addMarker(options);
        updateMap();
       initMap();
    }

Initialization of a card:

private void initMap() {
        if (map == null) {
            SupportMapFragment mapFragment = ((SupportMapFragment) getSupportFragmentManager().findFragmentByTag("MAP"));
            if (mapFragment != null) {
                map = mapFragment.getMap();
                if (map != null) {
                    UiSettings uiSettings = map.getUiSettings();
                    uiSettings.setAllGesturesEnabled(false);
                    uiSettings.setScrollGesturesEnabled(true);
                    uiSettings.setZoomGesturesEnabled(true);
                    map.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() {
                        @Override
                        public void onCameraChange(CameraPosition arg0) {
                            moveMapCameraToBoundsAndInitClusterkraf();
                            MapPointsProvider.getInstance().generate(mCallback, check_sos, check_company);
                            initMap();
                        }
                    });
                }
            }
        } else {
            moveMapCameraToBoundsAndInitClusterkraf();
        }
    }

Setting the camera to the previous position when the orientation was changed:

private void moveMapCameraToBoundsAndInitClusterkraf() {
        if (map != null && options != null && inputPoints != null) {
            try {
                if (restoreCameraPosition != null) {
                    /**
                     * if a restoreCameraPosition is available, move the camera
                     * there
                     */
                    map.moveCamera(CameraUpdateFactory.newCameraPosition(restoreCameraPosition));
                    restoreCameraPosition = null;
                }
                initClusterkraf();
            } catch (IllegalStateException ise) {
            }
        }
    }

Initialization of Clusterkraft (with the addition of each marker / marker groups you need to re-conduct clustering):

private void initClusterkraf() {
        if (map != null && inputPoints != null && inputPoints.size() > 0) {
            com.twotoasters.clusterkraf.Options options = new com.twotoasters.clusterkraf.Options();
            applyDemoOptionsToClusterkrafOptions(options);
            if (clusterkraf == null) {
                clusterkraf = new Clusterkraf(map, options, inputPoints);
            } else {
                clusterkraf.clear();
                //map.clear();
                clusterkraf.addAll(inputPoints);
            }
            if (!check_sos && !check_company) {
                clusterkraf.clear();
            }
        }
        if (map != null && inputPoints.size() == 0 && clusterkraf != null) {
            if (!check_sos && !check_company) {
                clusterkraf.clear();
            }
        }
    }

A class that describes the markers clustering rules uses for them the corresponding pin icons. If this is a cluster, then the class appends the number of elements in Pin with corresponding color:

public class MapMarkerOptionsChooser extends MarkerOptionsChooser {

    private final WeakReference contextRef;
    private final Paint clusterPaintLarge;
    private final Paint clusterPaintMedium;
    private final Paint clusterPaintSmall;

    public MapMarkerOptionsChooser(Context context) {
        this.contextRef = new WeakReference(context);

        Resources res = context.getResources();

        clusterPaintMedium = new Paint();
        clusterPaintMedium.setColor(Color.BLACK);
        clusterPaintMedium.setAlpha(255);
        clusterPaintMedium.setTextAlign(Paint.Align.CENTER);
        clusterPaintMedium.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL));
        clusterPaintMedium.setTextSize(res.getDimension(R.dimen.cluster_text_size_medium));

        clusterPaintSmall = new Paint(clusterPaintMedium);
        clusterPaintSmall.setTextSize(res.getDimension(R.dimen.cluster_text_size_small));

        clusterPaintLarge = new Paint(clusterPaintMedium);
        clusterPaintLarge.setTextSize(res.getDimension(R.dimen.cluster_text_size_large));
    }

    @Override
    public void choose(MarkerOptions markerOptions, ClusterPoint clusterPoint) {
        Context context = contextRef.get();
        if (context != null) {
            Resources res = context.getResources();
            boolean isCluster = clusterPoint.size() > 1;
            BitmapDescriptor icon;
            String title;
            if (isCluster) {
                int company = 0;
                int sos = 0;
                int clusterSize = clusterPoint.size();
                for (int i = 0; i < clusterSize; i++) {
                    if (((MarkerData) clusterPoint.getPointAtOffset(i).getTag()).getType() == MarkerData.SOS) {
                        sos++;
                    }
                    if (((MarkerData) clusterPoint.getPointAtOffset(i).getTag()).getType() == MarkerData.COMPANY) {
                        company++;
                    }
                }
                if (sos > 0 && company == 0) {
                    icon = BitmapDescriptorFactory.fromBitmap(getClusterBitmap(res, R.drawable.ic_map_group, clusterSize, Color.WHITE));
                } else if (sos == 0 && company > 0) {
                    icon = BitmapDescriptorFactory.fromBitmap(getClusterBitmap(res, R.drawable.ic_map_group_companies, clusterSize, Color.BLACK));
                } else {
                    icon = BitmapDescriptorFactory.fromBitmap(getClusterBitmap(res, R.drawable.ic_map_group_of_groups, clusterSize, Color.BLACK));
                }
            } else {
                MarkerData data = (MarkerData) clusterPoint.getPointAtOffset(0).getTag();
                if (data.getType() == MarkerData.SOS) {
                    icon = BitmapDescriptorFactory.fromResource(R.drawable.ic_map_sos_pin);
                } else {
                    icon = BitmapDescriptorFactory.fromResource(R.drawable.ic_map_company_pin);
                }
            }
            markerOptions.icon(icon);
            markerOptions.anchor(0.5f, 1.0f);
        }
    }

    @SuppressLint("NewApi")
    private Bitmap getClusterBitmap(Resources res, int resourceId, int clusterSize, int color) {
        BitmapFactory.Options options = new BitmapFactory.Options();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            options.inMutable = true;
        }
        Bitmap bitmap = BitmapFactory.decodeResource(res, resourceId, options);
        if (!bitmap.isMutable()) {
            bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
        }

        Canvas canvas = new Canvas(bitmap);

        Paint paint = null;
        float originY;
        if (clusterSize < 100) {
            paint = clusterPaintLarge;
            originY = bitmap.getHeight() * 0.64f;
        } else if (clusterSize < 1000) {
            paint = clusterPaintMedium;
            originY = bitmap.getHeight() * 0.6f;
        } else {
            paint = clusterPaintSmall;
            originY = bitmap.getHeight() * 0.56f;
        }

        paint.setColor(color);

        canvas.drawText(String.valueOf(clusterSize), bitmap.getWidth() * 0.5f, originY, paint);

        return bitmap;
    }
}

Still we couldn’t avoid some problems. When using the library the event onCameraChangePosition ceased to work. As a solution, we used View.OnTouchListener with some conditions.

And finally, here is a tip on standard info-windows («pop-ups» over markers). If you want to display any process in them or assign certain events to their individual elements, you can use pop-up for this, as the standard info window displays «not alive» View (i.e. like screenshot, that has no animation within the window)and processes only click on its entire surface.