Custom Polygon Cluster

This example shows how to add Polygon Cluster in MapView

  • Add Cluster from GeoJson

  • Aggregate a large number of coordinate points

Custom Polygon Cluster

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

1
public 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.

© 2024 NextBillion.ai all rights reserved.