s## Clustering
CARTO Mobile SDK can dynamically cluster point data if large amounts of points need to be shown without cluttering the MapView. Clusters are formed based on the map zoom level and spatially close points are placed into the same cluster.
Clusters are usually markers which display a location of several objects, and typically indicate the number of markers within each object.
CARTO Mobile SDK has built-in cluster feature, which is highly customizable. You can define the following options in your app code:
Styling the cluster objects
Dynamically generate cluster object styles. For example, automatically display the number of objects in each cluster
Define the minimum zoom level for clusters
Set the minimum distance between objects, before it becomes a cluster
Indicate the action when clicking on marker. For example, zoom in, or expand the cluster without zooming
Tip: The cluster expand feature is useful for small clusters (containing up to four objects inside)
Depending on the device, the Mobile SDK can cluster 100,000 points in less than a second.
Implementing clustering
Clusters are generated dynamically, based on VectorDataSource
data that loads the map layer. If using an API, it works as a unique layer with the ClusteredVectorLayer
method, and includes the following parameters in the a hierarchal order:
Select the layer DataSource
In most cases, the LocalVectorDataSource
function contains all the elements to request the data. It is important that the DataSource displays all elements in a layer, and does not limit it to the current map visualization bbox (bounding box)
ClusterElementBuilder
defines a single method buildClusterElement
// 1. Initialize a local vector data source
LocalVectorDataSource vectorDataSource1 = new LocalVectorDataSource ( mapView . getOptions (). getBaseProjection ());
// 2. Create Marker objects and add them to vectorDataSource
// **Note:** This depends on the _app type_ of your mobile app settings. See AdvancedMap for samples with JSON loading and random point generation
// 3. Initialize a vector layer with the previous data source
ClusteredVectorLayer vectorLayer1 = new ClusteredVectorLayer ( vectorDataSource1 , new MyClusterElementBuilder ( this . getApplication ()));
vectorLayer1 . setMinimumClusterDistance ( 20 );
// 4. Add the previous vector layer to the map
mapView . getLayers (). add ( vectorLayer1 );
// 1. Create overlay layer for markers
var dataSource = new LocalVectorDataSource(MapView.Options.BaseProjection);
// 2. Create Marker objects and add them to vectorDataSource.
// **Note:** This depends on the _app type_ of your mobile app settings. See samples with JSON loading
// 3. Initialize a vector layer with the previous data source
var layer = new ClusteredVectorLayer(dataSource, new MyClusterElementBuilder());
layer.MinimumClusterDistance = 20; // in pixels
MapView.Layers.Add(layer);
// 1. Initialize a local vector data source
NTProjection * proj = [[ mapView getOptions ] getBaseProjection ];
NTLocalVectorDataSource * vectorDataSource = [[ NTLocalVectorDataSource alloc ] initWithProjection : proj ];
// 2. Create Marker objects and add them to vectorDataSource.
// **Note:** This depends on the _app type_ of your mobile app settings. See AdvancedMap for samples with JSON loading and random point generation
// 3. Create element builder
MyMarkerClusterElementBuilder * clusterElementBuilder = [[ MyMarkerClusterElementBuilder alloc ] init ];
// 4. Initialize a vector layer with the previous data source
NTClusteredVectorLayer * vectorLayer = [[ NTClusteredVectorLayer alloc ] initWithDataSource : vectorDataSource clusterElementBuilder : clusterElementBuilder ];
[[ mapView getLayers ] add : vectorLayer ];
// 1. Initialize a local vector data source
let vectorDataSource1 = NTLocalVectorDataSource ( projection : mapView ? . getOptions () . getBaseProjection ())
// 2. Create Marker objects and add them to vectorDataSource
// **Note:** This depends on the _app type_ of your mobile app settings.
// See AdvancedMap for samples with JSON loading and random point generation
// 3. Initialize a vector layer with the previous data source
let builder = MyClusterElementBuilder ( imageUrl : "marker_black.png" )
let vectorLayer1 = NTClusteredVectorLayer ( dataSource : vectorDataSource1 , clusterElementBuilder : builder )
vectorLayer1 ? . setMinimumClusterDistance ( 20 )
// 4. Add the previous vector layer to the map
mapView ? . getLayers ()? . add ( vectorLayer1 )
// 1. Initialize a local vector data source
val vectorDataSource1 = LocalVectorDataSource ( mapView ?. options ?. baseProjection )
// 2. Create Marker objects and add them to vectorDataSource
// **Note:** This depends on the _app type_ of your mobile app settings.
// See AdvancedMap for samples with JSON loading and random point generation
// 3. Initialize a vector layer with the previous data source
val vectorLayer1 = ClusteredVectorLayer ( vectorDataSource1 , MyClusterElementBuilder ( this . application ))
vectorLayer1 . minimumClusterDistance = 20f
// 4. Add the previous vector layer to the map
mapView ?. layers ?. add ( vectorLayer1 )
Define ClusterElementBuilder
The Cluster Element Builder takes set of original markers (map objects) as input, and returns one object (or another VectorElement
, such as a Point or BalloonPopup) which dynamically replaces the original marker.
Note: It is highly recommended to reuse and cache styles to reduce memory usage. For example, a marker style with a specific number is only created once. Android and iOS samples use platform-specific graphic APIs to generate the bitmap for the marker. .NET example only uses BalloonPopup, which is slower but works the same across all platforms.
private class MyClusterElementBuilder extends ClusterElementBuilder {
@SuppressLint ( "UseSparseArrays" )
private Map < Integer , MarkerStyle > markerStyles = new HashMap < Integer , MarkerStyle >();
private android . graphics . Bitmap markerBitmap ;
MyClusterElementBuilder ( Application context ) {
markerBitmap = android . graphics . Bitmap . createBitmap ( BitmapFactory . decodeResource ( context . getResources (), R . drawable . marker_black ));
}
@Override
public VectorElement buildClusterElement ( MapPos pos , VectorElementVector elements ) {
// 1. Reuse existing marker styles
MarkerStyle style = markerStyles . get (( int ) elements . size ());
if ( elements . size () == 1 ) {
style = (( Marker ) elements . get ( 0 )). getStyle ();
}
if ( style == null ) {
android . graphics . Bitmap canvasBitmap = markerBitmap . copy ( android . graphics . Bitmap . Config . ARGB_8888 , true );
android . graphics . Canvas canvas = new android . graphics . Canvas ( canvasBitmap );
android . graphics . Paint paint = new android . graphics . Paint ( android . graphics . Paint . ANTI_ALIAS_FLAG );
paint . setTextAlign ( Paint . Align . CENTER );
paint . setTextSize ( 12 );
paint . setColor ( android . graphics . Color . argb ( 255 , 0 , 0 , 0 ));
canvas . drawText ( Integer . toString (( int ) elements . size ()), markerBitmap . getWidth () / 2 , markerBitmap . getHeight () / 2 - 5 , paint );
MarkerStyleBuilder styleBuilder = new MarkerStyleBuilder ();
styleBuilder . setBitmap ( BitmapUtils . createBitmapFromAndroidBitmap ( canvasBitmap ));
styleBuilder . setSize ( 30 );
styleBuilder . setPlacementPriority (( int )- elements . size ());
style = styleBuilder . buildStyle ();
markerStyles . put (( int ) elements . size (), style );
}
// 2. Create marker for the cluster
Marker marker = new Marker ( pos , style );
return marker ;
}
}
public class MyClusterElementBuilder : ClusterElementBuilder
{
BalloonPopupStyleBuilder balloonPopupStyleBuilder;
public MyClusterElementBuilder()
{
balloonPopupStyleBuilder = new BalloonPopupStyleBuilder();
balloonPopupStyleBuilder.CornerRadius = 3;
balloonPopupStyleBuilder.TitleMargins = new BalloonPopupMargins(6, 6, 6, 6);
balloonPopupStyleBuilder.LeftColor = new Color(240, 230, 140, 255);
}
public override VectorElement BuildClusterElement(MapPos pos, VectorElementVector elements)
{
BalloonPopupStyle style = balloonPopupStyleBuilder.BuildStyle();
var popup = new BalloonPopup(pos, style, elements.Count.ToString(), "");
return popup;
}
}
// .h
@interface MyMarkerClusterElementBuilder : NTClusterElementBuilder
@property NSMutableDictionary * markerStyles ;
@end
// .m
@implementation MyMarkerClusterElementBuilder
- ( NTVectorElement * ) buildClusterElement :( NTMapPos * ) mapPos elements :( NTVectorElementVector * ) elements
{
if ( ! self . markerStyles ) {
self . markerStyles = [ NSMutableDictionary new ];
}
NSString * styleKey = [ NSString stringWithFormat : @"%d" ,( int )[ elements size ]];
if ([ elements size ] > 1000 ) {
styleKey = @">1K" ;
}
NTMarkerStyle * markerStyle = [ self . markerStyles valueForKey : styleKey ];
if ([ elements size ] == 1 ) {
markerStyle = [( NTMarker * )[ elements get : 0 ] getStyle ];
}
if ( ! markerStyle ) {
UIImage * image = [ UIImage imageNamed : @"marker_black.png" ];
UIGraphicsBeginImageContext ( image . size );
[ image drawAtPoint : CGPointMake ( 0 , 0 )];
CGRect rect = CGRectMake ( 0 , 15 , image . size . width , image . size . height );
[[ UIColor blackColor ] set ];
NSMutableParagraphStyle * style = [[ NSParagraphStyle defaultParagraphStyle ] mutableCopy ];
[ style setAlignment : NSTextAlignmentCenter ];
NSDictionary * attr = [ NSDictionary dictionaryWithObject : style forKey : NSParagraphStyleAttributeName ];
[ styleKey drawInRect : CGRectIntegral ( rect ) withAttributes : attr ];
UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext ();
UIGraphicsEndImageContext ();
NTBitmap * markerBitmap = [ NTBitmapUtils createBitmapFromUIImage : newImage ];
NTMarkerStyleBuilder * markerStyleBuilder = [[ NTMarkerStyleBuilder alloc ] init ];
[ markerStyleBuilder setBitmap : markerBitmap ];
[ markerStyleBuilder setSize : 30 ];
[ markerStyleBuilder setPlacementPriority : - ( int )[ elements size ]];
markerStyle = [ markerStyleBuilder buildStyle ];
[ self . markerStyles setValue : markerStyle forKey : styleKey ];
}
NTMarker * marker = [[ NTMarker alloc ] initWithPos : mapPos style : markerStyle ];
NTVariant * variant = [[ NTVariant alloc ] initWithString :[ @ ([ elements size ]) stringValue ]];
[ marker setMetaDataElement : @"elements" element : variant ];
return marker ;
}
public class MyClusterElementBuilder : NTClusterElementBuilder {
let markerStyles = NSMutableDictionary ()
var imageUrl : String ?
convenience init ( imageUrl : String ) {
self . init ()
self . imageUrl = imageUrl
}
override public func buildClusterElement ( _ mapPos : NTMapPos ! , elements : NTVectorElementVector ! ) -> NTVectorElement ! {
var styleKey = String ( elements . size ())
if ( elements . size () > 1000 ) {
styleKey = ">1K"
}
var markerStyle = self . markerStyles . value ( forKeyPath : styleKey )
if ( elements . size () == 1 ) {
markerStyle = ( elements . get ( 0 ) as! NTMarker ) . getStyle ()
}
if ( markerStyle == nil ) {
let image = UIImage ( named : imageUrl ! )
UIGraphicsBeginImageContext (( image ? . size ) ! )
image ? . draw ( at : CGPoint ( x : 0 , y : 0 ));
let rect = CGRect ( x : 0 , y : 15 , width : ( image ? . size . width ) ! , height : ( image ? . size . height ) ! )
UIColor . black . set ()
image ? . draw ( in : rect . integral , blendMode : CGBlendMode . color , alpha : 1.0 )
let newImage = UIGraphicsGetImageFromCurrentImageContext ()
UIGraphicsEndImageContext ()
let marker = NTBitmapUtils . createBitmap ( from : newImage )
let builder = NTMarkerStyleBuilder ()
builder ? . setBitmap ( marker )
builder ? . setSize ( 30 )
builder ? . setPlacementPriority ( - ( Int32 ( elements . size () as UInt32 )))
markerStyle = builder ? . buildStyle ()
self . markerStyles . setValue ( markerStyle , forKey : styleKey )
}
let marker = NTMarker ( pos : mapPos , style : markerStyle as! NTMarkerStyle ! )
let variant = NTVariant ( string : String ( elements . size ()))
marker ? . setMetaData ( "elements" , element : variant )
return marker
}
}
private inner class MyClusterElementBuilder internal constructor ( context : Context ) : ClusterElementBuilder () {
val markerStyles = HashMap < Int , MarkerStyle >()
val markerBitmap : android . graphics . Bitmap
init {
val resource = BitmapFactory . decodeResource ( context . resources , R . drawable . marker_black )
markerBitmap = android . graphics . Bitmap . createBitmap ( resource )
}
override fun buildClusterElement ( pos : MapPos , elements : VectorElementVector ): VectorElement {
// 1. Reuse existing marker styles
var style : MarkerStyle ? = markerStyles [ elements . size (). toInt ()]
if ( elements . size (). toInt () == 1 ) {
style = ( elements . get ( 0 ) as Marker ). style
}
if ( style == null ) {
val canvasBitmap = markerBitmap . copy ( android . graphics . Bitmap . Config . ARGB_8888 , true )
val canvas = android . graphics . Canvas ( canvasBitmap )
val paint = android . graphics . Paint ( android . graphics . Paint . ANTI_ALIAS_FLAG )
paint . textAlign = Paint . Align . CENTER
paint . textSize = 12f
paint . color = android . graphics . Color . argb ( 255 , 0 , 0 , 0 )
val text = Integer . toString ( elements . size (). toInt ())
val x = ( markerBitmap . width / 2 ). toFloat ()
val y = ( markerBitmap . height / 2 - 5 ). toFloat ()
canvas . drawText ( text , x , y , paint )
val styleBuilder = MarkerStyleBuilder ()
styleBuilder . bitmap = BitmapUtils . createBitmapFromAndroidBitmap ( canvasBitmap )
styleBuilder . size = 30f
styleBuilder . placementPriority = (- elements . size ()). toInt ()
style = styleBuilder . buildStyle ()
markerStyles . put ( elements . size (). toInt (), style )
}
// 2. Create marker for the cluster
val marker = Marker ( pos , style )
return marker
}
}