Custom Polygon Cluster
This example shows how to add Polygon Cluster in MapView
-
Add Cluster from GeoJson
-
Aggregate a large number of coordinate points

For all code examples, refer to Android Maps SDK Code Examples
activity_polygon_cluster.xml view source
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:orientation="vertical"
8 tools:context=".PolygonActivity">
9
10 <ai.nextbillion.maps.core.MapView
11 android:id="@id/map_view"
12 android:layout_width="match_parent"
13 android:layout_height="match_parent"
14 app:nbmap_uiAttribution="false"
15 app:nbmap_cameraTargetLat="1.3383500934590808"
16 app:nbmap_cameraTargetLng="103.80586766146754"
17 app:nbmap_cameraZoom="11" />
18
19 <com.google.android.material.floatingactionbutton.FloatingActionButton
20 android:id="@+id/fab_route"
21 android:layout_width="wrap_content"
22 android:layout_height="wrap_content"
23 android:layout_alignParentBottom="true"
24 android:layout_alignParentEnd="true"
25 android:layout_alignParentRight="true"
26 android:layout_marginBottom="@dimen/fab_margin"
27 android:layout_marginRight="@dimen/fab_margin"
28 android:layout_marginEnd="@dimen/fab_margin"
29 android:src="@drawable/ic_directions_bus_black"
30 app:backgroundTint="@android:color/white" />
31
32 <com.google.android.material.floatingactionbutton.FloatingActionButton
33 android:id="@+id/fab_style"
34 android:layout_width="wrap_content"
35 android:layout_height="wrap_content"
36 android:layout_above="@id/fab_route"
37 android:layout_alignParentEnd="true"
38 android:layout_alignParentRight="true"
39 android:layout_marginBottom="@dimen/fab_margin"
40 android:layout_marginRight="@dimen/fab_margin"
41 android:src="@drawable/ic_layers"
42 android:layout_marginEnd="@dimen/fab_margin"
43 app:backgroundTint="@color/purple_200"
44 android:visibility="gone"/>
45
46 <ImageView
47 android:id="@+id/back_ib"
48 android:layout_width="40dp"
49 android:layout_height="40dp"
50 android:layout_marginLeft="12dp"
51 android:layout_marginTop="30dp"
52 android:background="@drawable/circle_white_bg"
53 android:src="@drawable/icon_back"
54 app:tint="@color/color_back_icon"/>
55
56</RelativeLayout>
PolygonClusterActivity view source
1public class PolygonClusterActivity extends AppCompatActivity implements OnMapReadyCallback, NextbillionMap.OnMapClickListener, View.OnClickListener {
2
3 private static final String TAG = "PolygonClusterActivity";
4 public static final String SOURCE_ID = "bus_stop";
5 public static final String SOURCE_ID_CLUSTER = "bus_stop_cluster";
6 public static final String URL_BUS_ROUTES = "https://raw.githubusercontent.com/cheeaun/busrouter-sg/master/data/2/bus-stops.geojson";
7 public static final String LAYER_ID = "stops_layer";
8 private static final String TAXI = "taxi";
9 private ImageView backBtn;
10 private MapView mapView;
11 private NextbillionMap nextbillionMap;
12
13 private FloatingActionButton styleFab;
14 private FloatingActionButton routeFab;
15
16 private CircleLayer layer;
17 private GeoJsonSource source;
18
19 private int currentStyleIndex = 0;
20 private boolean isLoadingStyle = true;
21
22 @Override
23 protected void onCreate(Bundle savedInstanceState) {
24 super.onCreate(savedInstanceState);
25 setContentView(R.layout.activity_polygon_cluster);
26 mapView = findViewById(R.id.map_view);
27 backBtn = findViewById(R.id.back_ib);
28 mapView.onCreate(savedInstanceState);
29 mapView.getMapAsync(this);
30
31 backBtn.setOnClickListener(new View.OnClickListener() {
32 @Override
33 public void onClick(View v) {
34 finish();
35 }
36 });
37 }
38
39 @Override
40 public void onMapReady(@NonNull NextbillionMap nbMap) {
41 this.nextbillionMap = nbMap;
42 mapView.addOnDidFinishLoadingStyleListener(() -> {
43 Style style = nextbillionMap.getStyle();
44 style.addImage(TAXI,(BitmapDrawable)getResources().getDrawable(R.mipmap.beat_taxi));
45 addBusStopSource(style);
46 addBusStopCircleLayer(style);
47 initFloatingActionButtons();
48 isLoadingStyle = false;
49 });
50 }
51
52 @Override
53 public boolean onMapClick(@NonNull LatLng latLng) {
54
55 return false;
56 }
57
58
59 private void addBusStopSource(Style style) {
60 try {
61 source = new GeoJsonSource(SOURCE_ID, new URI(URL_BUS_ROUTES));
62 } catch (URISyntaxException exception) {
63 Log.e(TAG, "That's not an url... ");
64 }
65 style.addSource(source);
66 }
67
68 private void addBusStopCircleLayer(Style style) {
69 layer = new CircleLayer(LAYER_ID, SOURCE_ID);
70 layer.setProperties(
71 circleColor(Color.parseColor("#FF0000")),
72 circleRadius(2.0f)
73 );
74 style.addLayerBelow(layer, "waterway-label");
75 }
76
77 private void initFloatingActionButtons() {
78 routeFab = findViewById(R.id.fab_route);
79 routeFab.setColorFilter(ContextCompat.getColor(PolygonClusterActivity.this, R.color.purple_200));
80 routeFab.setOnClickListener(PolygonClusterActivity.this);
81
82 styleFab = findViewById(R.id.fab_style);
83 styleFab.setOnClickListener(PolygonClusterActivity.this);
84 }
85
86 @Override
87 public void onClick(View view) {
88 if (isLoadingStyle) {
89 return;
90 }
91
92 if (view.getId() == R.id.fab_route) {
93 showBusCluster();
94 } else if (view.getId() == R.id.fab_style) {
95 changeMapStyle();
96 }
97 }
98
99 private void showBusCluster() {
100 removeFabs();
101 removeOldSource();
102 addClusteredSource();
103 }
104
105 private void removeOldSource() {
106 nextbillionMap.getStyle().removeSource(SOURCE_ID);
107 nextbillionMap.getStyle().removeLayer(LAYER_ID);
108 }
109
110 private void addClusteredSource() {
111 try {
112 nextbillionMap.getStyle().addSource(
113 new GeoJsonSource(SOURCE_ID_CLUSTER,
114 new URI(URL_BUS_ROUTES),
115 new GeoJsonOptions()
116 .withCluster(true)
117 .withClusterMaxZoom(14)
118 .withClusterRadius(50)
119 )
120 );
121 } catch (URISyntaxException malformedUrlException) {
122 Log.e(TAG, "That's not an url... ");
123 }
124
125 // Add unclustered layer
126 int[][] layers = new int[][]{
127 new int[]{150, ResourcesCompat.getColor(getResources(), R.color.purple_200, getTheme())},
128 new int[]{20, ResourcesCompat.getColor(getResources(), R.color.colorAccent, getTheme())},
129 new int[]{0, ResourcesCompat.getColor(getResources(), R.color.color_4158ce, getTheme())}
130 };
131
132 SymbolLayer unclustered = new SymbolLayer("unclustered-points", SOURCE_ID_CLUSTER);
133 unclustered.setProperties(
134 iconImage(TAXI)
135 );
136
137 nextbillionMap.getStyle().addLayer(unclustered);
138
139 for (int i = 0; i < layers.length; i++) {
140 // Add some nice circles
141 CircleLayer circles = new CircleLayer("cluster-" + i, SOURCE_ID_CLUSTER);
142 circles.setProperties(
143 circleColor(layers[i][1]),
144 circleRadius(18f)
145 );
146
147 Expression pointCount = toNumber(get("point_count"));
148 circles.setFilter(
149 i == 0
150 ? all(has("point_count"),
151 gte(pointCount, literal(layers[i][0]))
152 ) : all(has("point_count"),
153 gt(pointCount, literal(layers[i][0])),
154 lt(pointCount, literal(layers[i - 1][0]))
155 )
156 );
157 nextbillionMap.getStyle().addLayer(circles);
158 }
159
160 // Add the count labels
161 SymbolLayer count = new SymbolLayer("count", SOURCE_ID_CLUSTER);
162 count.setProperties(
163 textField(Expression.toString(get("point_count"))),
164 textSize(12f),
165 textColor(Color.WHITE),
166 textIgnorePlacement(true),
167 textAllowOverlap(true)
168 );
169 nextbillionMap.getStyle().addLayer(count);
170 }
171
172 private void removeFabs() {
173 routeFab.setVisibility(View.GONE);
174 styleFab.setVisibility(View.GONE);
175 }
176
177 private void changeMapStyle() {
178 isLoadingStyle = true;
179 removeBusStop();
180 loadNewStyle();
181 }
182
183 private void removeBusStop() {
184 nextbillionMap.getStyle().removeLayer(layer);
185 nextbillionMap.getStyle().removeSource(source);
186 }
187
188 private void loadNewStyle() {
189 nextbillionMap.setStyle(new Style.Builder().fromUri(getNextStyle()));
190 }
191
192 private void addBusStop() {
193 nextbillionMap.getStyle().addLayer(layer);
194 nextbillionMap.getStyle().addSource(source);
195 }
196
197 private String getNextStyle() {
198 currentStyleIndex++;
199 if (currentStyleIndex == Data.STYLES.length) {
200 currentStyleIndex = 0;
201 }
202 return Data.STYLES[currentStyleIndex];
203 }
204
205
206 ///////////////////////////////////////////////////////////////////////////
207 // Lifecycle
208 ///////////////////////////////////////////////////////////////////////////
209
210 @Override
211 protected void onStart() {
212 super.onStart();
213 mapView.onStart();
214 }
215
216 @Override
217 protected void onResume() {
218 super.onResume();
219 mapView.onResume();
220 }
221
222 @Override
223 protected void onPause() {
224 super.onPause();
225 mapView.onPause();
226 }
227
228 @Override
229 protected void onStop() {
230 super.onStop();
231 mapView.onStop();
232 }
233
234 @Override
235 protected void onSaveInstanceState(@NonNull Bundle outState) {
236 super.onSaveInstanceState(outState);
237 mapView.onSaveInstanceState(outState);
238 }
239
240 @Override
241 protected void onDestroy() {
242 super.onDestroy();
243 mapView.onDestroy();
244 }
245
246 @Override
247 public void onLowMemory() {
248 super.onLowMemory();
249 mapView.onLowMemory();
250 }
251
252 private static class Data {
253 private static final String[] STYLES = new String[]{
254 Style.NBMAP_STREETS,
255 Style.OUTDOORS,
256 Style.LIGHT,
257 Style.DARK,
258 Style.SATELLITE,
259 Style.SATELLITE_STREETS
260 };
261 }
262}
-
onMapReady: This method is called when the map is ready to be used. It initializes the NextbillionMap object and adds listeners to the map-style loading event. Once the style finishes loading, it adds the bus stop source, bus stop circle layer and sets up the floating action buttons.
-
addBusStopSource: This method adds a GeoJsonSource to the map style, representing the bus stop data source. It creates a GeoJsonSource object with the provided URL to a GeoJSON file containing bus stop information and adds it to the map style.
-
addBusStopCircleLayer: This method adds a CircleLayer to the map style, representing the bus stop markers on the map. It creates a CircleLayer object with the specified layer ID and source ID, sets properties for the circle color and radius and adds the layer to the map style.
-
addClusteredSource: This method adds a clustered source to the map style, representing a clustering of bus stops. It creates a GeoJsonSource object with the provided URL to a GeoJSON file, enables clustering with specified cluster options and adds the source to the map style. It also adds clustered and unclustered layers to represent the clusters and individual bus stops on the map.
-
showBusCluster: This method is called when the user clicks on the "Route" floating action button. It removes the existing floating action buttons, removes the old bus stop source and layer from the map style, and adds a clustered source with clustered and unclustered layers to represent the bus stop clusters on the map.