Display a web map with a point feature layer that has feature reduction enabled to aggregate points into clusters.
Use case
Feature clustering can be used to dynamically aggregate groups of points that are within proximity of each other in order to represent each group with a single symbol. Such grouping allows you to see patterns in the data that are difficult to visualize when a layer contains hundreds or thousands of points that overlap and cover each other.
How to use the sample
Pan and zoom the map to view how clustering is dynamically updated. Toggle clustering off to view the original point features that make up the clustered elements. When clustering is "On", you can tap on a clustered geoelement to view a list of contained geoelements.
How it works
- Create a map from a web map
PortalItem
. - Get the cluster enabled layer from the map's operational layers.
- Get the
FeatureReduction
from the feature layer and set theenabled
bool to enable or disable clustering on the feature layer. - When the user taps on the map view, call
identifyLayer()
, passing in the layer, map tap location and tolerance. - Select the
AggregateGeoElement
from the resultingIdentifyLayerResult
and callgetGeoElements()
to retrieve the containingGeoElement
objects. - Display the list of contained
GeoElement
objects in a dialog.
Relevant API
- AggregateGeoElement
- FeatureLayer
- FeatureReduction
- GeoElement
- IdentifyLayerResult
About the data
This sample uses a web map that displays the Esri Global Power Plants feature layer with feature reduction enabled. When enabled, the aggregate features symbology shows the color of the most common power plant type, and a size relative to the average plant capacity of the cluster.
Additional information
Graphics in a graphics overlay can also be aggregated into clusters. To do this, set the FeatureReduction
property on the GraphicsOverlay
to a new ClusteringFeatureReduction
.
Tags
aggregate, bin, cluster, group, merge, normalize, reduce, summarize
Sample Code
//
// Copyright 2024 Esri
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import 'package:arcgis_maps/arcgis_maps.dart';
import 'package:flutter/material.dart';
import '../../utils/sample_state_support.dart';
class DisplayClusters extends StatefulWidget {
const DisplayClusters({super.key});
@override
State<DisplayClusters> createState() => _DisplayClustersState();
}
class _DisplayClustersState extends State<DisplayClusters>
with SampleStateSupport {
// Create a map view controller.
final _mapViewController = ArcGISMapView.createController();
late ArcGISMap _map;
late FeatureLayer _featureLayer;
// A flag to track whether feature clustering is enabled to display in the UI.
var _featureReductionEnabled = false;
// A flag for when the map view is ready and controls can be used.
var _ready = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
child: Stack(
children: [
Column(
children: [
Expanded(
// Add a map view to the widget tree and set a controller.
child: ArcGISMapView(
controllerProvider: () => _mapViewController,
onMapViewReady: onMapViewReady,
onTap: onTap,
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Add a button to toggle feature clustering.
ElevatedButton(
onPressed: toggleFeatureClustering,
child: const Text('Toggle feature clustering'),
),
const SizedBox(
height: 10,
),
// Display the current feature reduction state.
Text(
_featureReductionEnabled
? 'Feature Reduction: On'
: 'Feature Reduction: Off',
),
],
),
],
),
// Display a progress indicator and prevent interaction until state is ready.
Visibility(
visible: !_ready,
child: SizedBox.expand(
child: Container(
color: Colors.white30,
child: const Center(child: CircularProgressIndicator()),
),
),
),
],
),
),
);
}
void onMapViewReady() async {
// Get the power plants web map from the default portal.
final portal = Portal.arcGISOnline();
final portalItem = PortalItem.withPortalAndItemId(
portal: portal,
itemId: '8916d50c44c746c1aafae001552bad23',
);
// Load the portal item.
await portalItem.load();
// Create a map from the portal item.
_map = ArcGISMap.withItem(portalItem);
// Set the map to the map view controller.
_mapViewController.arcGISMap = _map;
// Load the map.
await _map.load();
// Get the power plant feature layer once the map has finished loading.
if (_map.operationalLayers.isNotEmpty &&
_map.operationalLayers.first is FeatureLayer) {
// Get the first layer from the web map a feature layer.
_featureLayer = _map.operationalLayers.first as FeatureLayer;
if (_featureLayer.featureReduction != null) {
// Set the ready state variable to true to enable the sample UI.
// Set the feature reduction flag to the current state of the feature
setState(() {
_ready = true;
_featureReductionEnabled = _featureLayer.featureReduction!.enabled;
});
} else {
showWarningDialog(
'Feature layer does not have feature reduction enabled.',
);
}
} else {
showWarningDialog('Unable to access a feature layer on the web map.');
}
}
void onTap(Offset localPosition) async {
// Clear any existing selected features.
_featureLayer.clearSelection();
// Perform an identify result on the map view controller, using the feature layer and tapped location.
final identifyLayerResult = await _mapViewController.identifyLayer(
_featureLayer,
screenPoint: localPosition,
tolerance: 12.0,
);
// Get the aggregate geoelements from the identify result.
final aggregateGeoElements =
identifyLayerResult.geoElements.whereType<AggregateGeoElement>();
if (aggregateGeoElements.isEmpty) return;
// Select the first aggregate geoelement.
final aggregateGeoElement = aggregateGeoElements.first;
aggregateGeoElement.isSelected = true;
// Get the list of geoelements associated with the aggregate geoelement.
final geoElements = await aggregateGeoElement.getGeoElements();
// Display a dialog with information about the geoelements.
showResultsDialog(geoElements);
}
void showResultsDialog(List<GeoElement> geoElements) {
// Create a dialog that lists the count and names of the provided list of geoelements.
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Contained GeoElements'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total GeoElements: ${geoElements.length}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 10),
SizedBox(
height: 200,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: geoElements
.map(
(geoElement) => Text(
geoElement.attributes['name'] ??
'Geoelement: ${geoElements.indexOf(geoElement)}}',
),
)
.toList(),
),
),
),
],
),
);
},
);
}
void toggleFeatureClustering() {
if (_featureLayer.featureReduction != null) {
// Toggle the feature reduction.
final featureReduction = _featureLayer.featureReduction!;
featureReduction.enabled = !featureReduction.enabled;
setState(() => _featureReductionEnabled = featureReduction.enabled);
}
}
void showWarningDialog(String message) {
// Show a dialog with the provided message.
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Warning'),
content: Text(
'$message Could not load sample.',
),
);
},
);
}
}