Skip to main content

Clustering

Point data dense enough to clutter the map can be clustered: spatially close points collapse into a single marker, per zoom level, usually labelled with how many it stands for. Clustering 100,000 points takes well under a second on a mid-range phone.

You control:

  • the style of the cluster object — including generating it from the count;
  • the minimum distance, in pixels, before two objects merge;
  • the minimum zoom clusters form at;
  • what a tap does — zoom in, or expand the cluster in place (useful up to about four objects).

The layer

ClusteredVectorLayer wraps a VectorDataSource and a cluster element builder. The source must hold all the elements, not just those in the current view — clustering is done over the whole set, not the visible one.

LocalVectorDataSource source = new LocalVectorDataSource(mapView.getOptions().getBaseProjection());
// … add every Marker to `source` …

ClusteredVectorLayer layer = new ClusteredVectorLayer(source, new CountClusterBuilder());
layer.setMinimumClusterDistance(20); // screen pixels
mapView.getLayers().add(layer);

The builder

buildClusterElement is handed the cluster's position and the elements inside it, and returns the one element that replaces them — a Marker, a Point, a BalloonPopup, anything.

Cache the styles. The builder is called for every cluster on every zoom change; rendering a bitmap per call is what makes clustering feel slow. One style per count is enough:

class CountClusterBuilder extends ClusterElementBuilder {

private final Map<Integer, MarkerStyle> styles = new HashMap<>();
private final android.graphics.Bitmap base;

@Override
public VectorElement buildClusterElement(MapPos pos, VectorElementVector elements) {
int count = (int) elements.size();
// A cluster of one is the original marker, with the original style.
if (count == 1) {
return new Marker(pos, ((Marker) elements.get(0)).getStyle());
}

MarkerStyle style = styles.get(count);
if (style == null) {
android.graphics.Bitmap canvasBitmap =
base.copy(android.graphics.Bitmap.Config.ARGB_8888, true);
android.graphics.Canvas canvas = new android.graphics.Canvas(canvasBitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(12);
canvas.drawText(Integer.toString(count), base.getWidth() / 2, base.getHeight() / 2 - 5, paint);

MarkerStyleBuilder builder = new MarkerStyleBuilder();
builder.setBitmap(BitmapUtils.createBitmapFromAndroidBitmap(canvasBitmap));
builder.setSize(30);
// Bigger clusters win a placement contest against smaller ones.
builder.setPlacementPriority(-count);
style = builder.buildStyle();
styles.put(count, style);
}
return new Marker(pos, style);
}
}

The builder runs on a background thread, so it must not touch UI state directly.

Surface API

ClusteredVectorLayer takes a builder callback, which a JSON spec cannot express. Construct it with the object API and adopt it:

MassifLayer clustered = Massif.adopt("layer", "sites", layer);
clustered.set("opacity", 0.9);

When not to cluster

Clustering exists to keep markers readable. For a large dataset that is really map data — tracks, a POI database, a route network — tile it through the GeoJSON vector tiling pyramid and style it with CartoCSS instead. That gets zoom-dependent filtering, label collision and the renderer's own batching, none of which a marker layer has.

Related: vector objects for the elements themselves, and performance for the spatial-index and simplification knobs.