Dados de ponto de clustering no SDK do iOS (Visualização)
Nota
Aposentadoria do SDK do iOS do Azure Maps
O SDK nativo do Azure Maps para iOS foi preterido e será desativado em 31/03/25. Para evitar interrupções de serviço, migre para o SDK da Web do Azure Maps até 31/03/25. Para obter mais informações, consulte O guia de migração do SDK do iOS do Azure Maps.
Ao exibir muitos pontos de dados no mapa, os pontos de dados podem se sobrepor uns aos outros. A sobreposição pode fazer com que o mapa se torne ilegível e difícil de usar. Clustering point data é o processo de combinar dados de ponto que estão próximos uns dos outros e representá-los no mapa como um único ponto de dados clusterizado. À medida que o usuário amplia o mapa, os clusters se dividem em seus pontos de dados individuais. Quando você trabalha com um grande número de pontos de dados, use os processos de clustering para melhorar a experiência do usuário.
Internet of Things Show - Clustering point data no Azure Maps
Pré-requisitos
Certifique-se de concluir as etapas no Guia de início rápido: criar um documento de aplicativo iOS. Os blocos de código neste artigo podem ser inseridos na viewDidLoad
função de ViewController
.
Habilitando o clustering em uma fonte de dados
Habilite o DataSource
clustering na classe definindo a cluster
opção como true
. Defina clusterRadius
para selecionar pontos de dados próximos e combina-os em um cluster. O valor de clusterRadius
é em pontos. Use clusterMaxZoom
para especificar um nível de zoom no qual desabilitar a lógica de clustering. Veja um exemplo de como habilitar o clustering em uma fonte de dados.
// Create a data source and enable clustering.
let source = DataSource(options: [
//Tell the data source to cluster point data.
.cluster(true),
//The radius in points to cluster data points together.
.clusterRadius(45),
//The maximum zoom level in which clustering occurs.
//If you zoom in more than this, all points are rendered as symbols.
.clusterMaxZoom(15)
])
Atenção
O clustering só funciona com Point
recursos. Se a fonte de dados contiver recursos de outros tipos de geometria, como Polyline
ou Polygon
, ocorrerá um erro.
Gorjeta
Se dois pontos de dados estiverem próximos uns dos outros no chão, é possível que o cluster nunca se separe, não importa o quão próximo o usuário aumente o zoom. Para resolver isso, você pode definir a clusterMaxZoom
opção para desativar a lógica de clustering e simplesmente exibir tudo.
A DataSource
classe fornece os seguintes métodos relacionados ao clustering também.
Método | Tipo de retorno | Description |
---|---|---|
children(of cluster: Feature) |
[Feature] |
Recupera os filhos de determinado cluster no próximo nível de zoom. Estas crianças podem ser uma combinação de características e subagrupamentos. Os subclusters tornam-se recursos com propriedades correspondentes a ClusteredProperties. |
zoomLevel(forExpanding cluster: Feature) |
Double |
Calcula um nível de zoom no qual o cluster começa a se expandir ou se separar. |
leaves(of cluster: Feature, offset: UInt, limit: UInt) |
[Feature] |
Recupera todos os pontos em um cluster. Defina o limit para retornar um subconjunto dos pontos e use a offset página para através dos pontos. |
Exibir clusters usando uma camada de bolhas
Uma camada de bolhas é uma ótima maneira de renderizar pontos agrupados. Use expressões para dimensionar o raio e alterar a cor com base no número de pontos no cluster. Se você exibir clusters usando uma camada de bolhas, deverá usar uma camada separada para renderizar pontos de dados não clusterizados.
Para exibir o tamanho do cluster na parte superior da bolha, use uma camada de símbolo com texto e não use um ícone.
O código a seguir exibe pontos agrupados usando uma camada de bolhas e o número de pontos em cada cluster usando uma camada de símbolo. Uma segunda camada de símbolo é usada para exibir pontos individuais que não estão dentro de um cluster.
// Create a data source and enable clustering.
let source = DataSource(options: [
// Tell the data source to cluster point data.
.cluster(true),
// The radius in points to cluster data points together.
.clusterRadius(45),
// The maximum zoom level in which clustering occurs.
// If you zoom in more than this, all points are rendered as symbols.
.clusterMaxZoom(15)
])
// Import the geojson data and add it to the data source.
let url = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson")!
source.importData(fromURL: url)
// Add data source to the map.
map.sources.add(source)
// Create a bubble layer for rendering clustered data points.
map.layers.addLayer(
BubbleLayer(
source: source,
options: [
// Scale the size of the clustered bubble based on the number of points in the cluster.
.bubbleRadius(
from: NSExpression(
forAZMStepping: NSExpression(forKeyPath: "point_count"),
// Default of 20 point radius.
from: NSExpression(forConstantValue: 20),
stops: NSExpression(forConstantValue: [
// If point_count >= 100, radius is 30 points.
100: 30,
// If point_count >= 750, radius is 40 points.
750: 40
])
)
),
// Change the color of the cluster based on the value on the point_count property of the cluster.
.bubbleColor(
from: NSExpression(
forAZMStepping: NSExpression(forKeyPath: "point_count"),
// Default to green.
from: NSExpression(forConstantValue: UIColor.green),
stops: NSExpression(forConstantValue: [
// If the point_count >= 100, color is yellow.
100: UIColor.yellow,
// If the point_count >= 100, color is red.
750: UIColor.red
])
)
),
.bubbleStrokeWidth(0),
// Only rendered data points which have a point_count property, which clusters do.
.filter(from: NSPredicate(format: "point_count != NIL"))
]
)
)
// Create a symbol layer to render the count of locations in a cluster.
map.layers.addLayer(
SymbolLayer(
source: source,
options: [
// Hide the icon image.
.iconImage(nil),
// Display the point count as text.
.textField(from: NSExpression(forKeyPath: "point_count")),
.textOffset(CGVector(dx: 0, dy: 0.4)),
.textAllowOverlap(true),
// Allow clustered points in this layer.
.filter(from: NSPredicate(format: "point_count != NIL"))
]
)
)
// Create a layer to render the individual locations.
map.layers.addLayer(
SymbolLayer(
source: source,
options: [
// Filter out clustered points from this layer.
.filter(from: NSPredicate(format: "point_count = NIL"))
]
)
)
A imagem a seguir mostra os recursos de pontos agrupados de exibição de código acima em uma camada de bolhas, dimensionados e coloridos com base no número de pontos no cluster. Os pontos não agrupados são renderizados usando uma camada de símbolo.
Exibir clusters usando uma camada de símbolos
Ao exibir pontos de dados, a camada de símbolos oculta automaticamente os símbolos que se sobrepõem uns aos outros para garantir uma interface de usuário mais limpa. Esse comportamento padrão pode ser indesejável se você quiser mostrar a densidade de pontos de dados no mapa. No entanto, essas configurações podem ser alteradas. Para exibir todos os símbolos, defina a iconAllowOverlap
opção da camada Símbolo como true
.
Use o clustering para mostrar a densidade dos pontos de dados enquanto mantém uma interface de usuário limpa. O exemplo a seguir mostra como adicionar símbolos personalizados e representar clusters e pontos de dados individuais usando a camada de símbolos.
// Load all the custom image icons into the map resources.
map.images.add(UIImage(named: "earthquake_icon")!, withID: "earthquake_icon")
map.images.add(UIImage(named: "warning-triangle-icon")!, withID: "warning-triangle-icon")
// Create a data source and add it to the map.
let source = DataSource(options: [
// Tell the data source to cluster point data.
.cluster(true)
])
// Import the geojson data and add it to the data source.
let url = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson")!
source.importData(fromURL: url)
// Add data source to the map.
map.sources.add(source)
// Create a layer to render the individual locations.
map.layers.addLayer(
SymbolLayer(
source: source,
options: [
.iconImage("earthquake_icon"),
// Filter out clustered points from this layer.
.filter(from: NSPredicate(format: "point_count = NIL"))
]
)
)
// Create a symbol layer to render the clusters.
map.layers.addLayer(
SymbolLayer(
source: source,
options: [
.iconImage("warning-triangle-icon"),
.textField(from: NSExpression(forKeyPath: "point_count")),
.textOffset(CGVector(dx: 0, dy: -0.4)),
// Allow clustered points in this layer.
.filter(from: NSPredicate(format: "point_count != NIL"))
]
)
)
Para este exemplo, a imagem a seguir é carregada na pasta assets do aplicativo.
earthquake-icon.png | warning-triangle-icon.png |
A imagem a seguir mostra os recursos de ponto agrupados e não agrupados de renderização de código acima usando ícones personalizados.
Agrupamento e a camada de mapa de calor
Os mapas de calor são uma ótima maneira de exibir a densidade de dados no mapa. Esse método de visualização pode lidar com um grande número de pontos de dados por conta própria. Se os pontos de dados estiverem agrupados e o tamanho do cluster for usado como o peso do mapa de calor, o mapa de calor poderá lidar com ainda mais dados. Para conseguir essa opção, defina a heatmapWeight
opção da camada de mapa de calor como NSExpression(forKeyPath: "point_count")
. Quando o raio do cluster é pequeno, o mapa de calor parece quase idêntico a um mapa de calor usando os pontos de dados não agrupados, mas tem um desempenho melhor. No entanto, quanto menor o raio do cluster, mais preciso é o mapa de calor, mas com menos benefícios de desempenho.
// Create a data source and enable clustering.
let source = DataSource(options: [
// Tell the data source to cluster point data.
.cluster(true),
// The radius in points to cluster points together.
.clusterRadius(10)
])
// Import the geojson data and add it to the data source.
let url = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson")!
source.importData(fromURL: url)
// Add data source to the map.
map.sources.add(source)
// Create a heat map and add it to the map.
map.layers.insertLayer(
HeatMapLayer(
source: source,
options: [
// Set the weight to the point_count property of the data points.
.heatmapWeight(from: NSExpression(forKeyPath: "point_count")),
// Optionally adjust the radius of each heat point.
.heatmapRadius(20)
]
),
below: "labels"
)
A imagem a seguir mostra o código acima exibindo um mapa de calor otimizado usando recursos de ponto agrupado e a contagem de cluster como o peso no mapa de calor.
Toque em eventos em pontos de dados clusterizados
Quando os eventos tap ocorrem em uma camada que contém pontos de dados clusterizados, o ponto de dados clusterizado retorna ao evento como um objeto de recurso de ponto GeoJSON. Este recurso de ponto tem as seguintes propriedades:
Nome da propriedade | Tipo | Descrição |
---|---|---|
cluster |
boolean | Indica se o recurso representa um cluster. |
point_count |
Número | O número de pontos que o cluster contém. |
point_count_abbreviated |
string | Uma cadeia de caracteres que abrevia o point_count valor se ele for longo. (por exemplo, 4.000 torna-se 4K) |
Este exemplo usa uma camada de bolha que renderiza pontos de cluster e adiciona um evento tap. Quando o evento tap é acionado, o código calcula e amplia o mapa para o próximo nível de zoom, no qual o cluster se separa. Essa funcionalidade é implementada usando o zoomLevel(forExpanding:)
DataSource
método da classe.
// Create a data source and enable clustering.
let source = DataSource(options: [
// Tell the data source to cluster point data.
.cluster(true),
// The radius in points to cluster data points together.
.clusterRadius(45),
// The maximum zoom level in which clustering occurs.
// If you zoom in more than this, all points are rendered as symbols.
.clusterMaxZoom(15)
])
// Set data source to the class property to use in events handling later.
self.source = source
// Import the geojson data and add it to the data source.
let url = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson")!
source.importData(fromURL: url)
// Add data source to the map.
map.sources.add(source)
// Create a bubble layer for rendering clustered data points.
let clusterBubbleLayer = BubbleLayer(
source: source,
options: [
// Scale the size of the clustered bubble based on the number of points in the cluster.
.bubbleRadius(
from: NSExpression(
forAZMStepping: NSExpression(forKeyPath: "point_count"),
// Default of 20 point radius.
from: NSExpression(forConstantValue: 20),
stops: NSExpression(forConstantValue: [
// If point_count >= 100, radius is 30 points.
100: 30,
// If point_count >= 750, radius is 40 points.
750: 40
])
)
),
// Change the color of the cluster based on the value on the point_count property of the cluster.
.bubbleColor(
from: NSExpression(
forAZMStepping: NSExpression(forKeyPath: "point_count"),
// Default to green.
from: NSExpression(forConstantValue: UIColor.green),
stops: NSExpression(forConstantValue: [
// If the point_count >= 100, color is yellow.
100: UIColor.yellow,
// If the point_count >= 100, color is red.
750: UIColor.red
])
)
),
.bubbleStrokeWidth(0),
// Only rendered data points which have a point_count property, which clusters do.
.filter(from: NSPredicate(format: "point_count != NIL"))
]
)
// Add the clusterBubbleLayer to the map.
map.layers.addLayer(clusterBubbleLayer)
// Create a symbol layer to render the count of locations in a cluster.
map.layers.addLayer(
SymbolLayer(
source: source,
options: [
// Hide the icon image.
.iconImage(nil),
// Display the point count as text.
.textField(from: NSExpression(forKeyPath: "point_count_abbreviated")),
// Offset the text position so that it's centered nicely.
.textOffset(CGVector(dx: 0, dy: 0.4)),
// Allow text overlapping so text is visible anyway
.textAllowOverlap(true),
// Allow clustered points in this layer.
.filter(from: NSPredicate(format: "point_count != NIL"))
]
)
)
// Create a layer to render the individual locations.
map.layers.addLayer(
SymbolLayer(
source: source,
options: [
// Filter out clustered points from this layer.
.filter(from: NSPredicate(format: "point_count = NIL"))
]
)
)
// Add the delegate to handle taps on the clusterBubbleLayer only.
map.events.addDelegate(self, for: [clusterBubbleLayer.id])
func azureMap(_ map: AzureMap, didTapOn features: [Feature]) {
guard let source = source, let cluster = features.first else {
// Data source have been released or no features provided
return
}
// Get the cluster expansion zoom level. This is the zoom level at which the cluster starts to break apart.
let expansionZoom = source.zoomLevel(forExpanding: cluster)
// Update the map camera to be centered over the cluster.
map.setCameraOptions([
// Center the map over the cluster points location.
.center(cluster.coordinate),
// Zoom to the clusters expansion zoom level.
.zoom(expansionZoom),
// Animate the movement of the camera to the new position.
.animationType(.ease),
.animationDuration(200)
])
}
A imagem a seguir mostra o código acima exibindo pontos agrupados em um mapa que, quando tocado, amplia o próximo nível de zoom que um cluster começa a separar e expandir.
Exibir área do cluster
Os dados pontuais que um cluster representa estão espalhados por uma área. Neste exemplo, quando um cluster é tocado, ocorrem dois comportamentos principais. Primeiro, os pontos de dados individuais contidos no cluster usados para calcular um casco convexo. Em seguida, o casco convexo é exibido no mapa para mostrar uma área. Um casco convexo é um polígono que envolve um conjunto de pontos como uma banda elástica e pode ser calculado usando o convexHull(from:)
método. Todos os pontos contidos em um cluster podem ser recuperados da fonte de dados usando o leaves(of:offset:limit:)
método.
// Create a data source and enable clustering.
let source = DataSource(options: [
// Tell the data source to cluster point data.
.cluster(true)
])
// Set data source to the class property to use in events handling later.
self.source = source
// Import the geojson data and add it to the data source.
let url = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson")!
source.importData(fromURL: url)
// Add data source to the map.
map.sources.add(source)
// Create a data source for the convex hull polygon.
// Since this will be updated frequently it is more efficient to separate this into its own data source.
let polygonDataSource = DataSource()
// Set polygon data source to the class property to use in events handling later.
self.polygonDataSource = polygonDataSource
// Add data source to the map.
map.sources.add(polygonDataSource)
// Add a polygon layer and a line layer to display the convex hull.
map.layers.addLayer(PolygonLayer(source: polygonDataSource))
map.layers.addLayer(LineLayer(source: polygonDataSource))
// Load an icon into the image sprite of the map.
map.images.add(.azm_markerRed, withID: "marker-red")
// Create a symbol layer to render the clusters.
let clusterLayer = SymbolLayer(
source: source,
options: [
.iconImage("marker-red"),
.textField(from: NSExpression(forKeyPath: "point_count_abbreviated")),
.textOffset(CGVector(dx: 0, dy: -1.2)),
.textColor(.white),
.textSize(14),
// Only rendered data points which have a point_count property, which clusters do.
.filter(from: NSPredicate(format: "point_count != NIL"))
]
)
// Add the clusterLayer to the map.
map.layers.addLayer(clusterLayer)
// Create a layer to render the individual locations.
map.layers.addLayer(
SymbolLayer(
source: source,
options: [
// Filter out clustered points from this layer.
.filter(from: NSPredicate(format: "point_count = NIL"))
]
)
)
// Add the delegate to handle taps on the clusterLayer only
// and then calculate the convex hull of all the points within a cluster.
map.events.addDelegate(self, for: [clusterLayer.id])
func azureMap(_ map: AzureMap, didTapOn features: [Feature]) {
guard let source = source, let polygonDataSource = polygonDataSource, let cluster = features.first else {
// Data source have been released or no features provided
return
}
// Get all points in the cluster. Set the offset to 0 and the max int value to return all points.
let featureLeaves = source.leaves(of: cluster, offset: 0, limit: .max)
// When only two points in a cluster. Render a line.
if featureLeaves.count == 2 {
// Extract the locations from the feature leaves.
let locations = featureLeaves.map(\.coordinate)
// Create a line from the points.
polygonDataSource.set(geometry: Polyline(locations))
return
}
// When more than two points in a cluster. Render a polygon.
if let hullPolygon = Math.convexHull(from: featureLeaves) {
// Overwrite all data in the polygon data source with the newly calculated convex hull polygon.
polygonDataSource.set(geometry: hullPolygon)
}
}
A imagem a seguir mostra o código acima exibindo a área de todos os pontos dentro de um cluster tocado.
Agregando dados em clusters
Muitas vezes, os clusters são representados usando um símbolo com o número de pontos que estão dentro do cluster. Mas, às vezes, é desejável personalizar o estilo de clusters com métricas adicionais. Com as propriedades do cluster, as propriedades personalizadas podem ser criadas e iguais a um cálculo baseado nas propriedades dentro de cada ponto com um cluster. As propriedades do cluster podem ser definidas na clusterProperties
opção do DataSource
.
O código a seguir calcula uma contagem com base na propriedade de tipo de entidade de cada ponto de dados em um cluster. Quando um usuário toca em um cluster, um pop-up é exibido com informações adicionais sobre o cluster.
// Create a popup and add it to the map.
let popup = Popup()
map.popups.add(popup)
// Set popup to the class property to use in events handling later.
self.popup = popup
// Close the popup initially.
popup.close()
// Create a data source and enable clustering.
let source = DataSource(options: [
// Tell the data source to cluster point data.
.cluster(true),
// The radius in points to cluster data points together.
.clusterRadius(50),
// Calculate counts for each entity type in a cluster as custom aggregate properties.
.clusterProperties(self.entityTypes.map { entityType in
ClusterProperty(
name: entityType,
operator: NSExpression(
forFunction: "sum:",
arguments: [
NSExpression.featureAccumulatedAZMVariable,
NSExpression(forKeyPath: entityType)
]
),
map: NSExpression(
forConditional: NSPredicate(format: "EntityType = '\(entityType)'"),
trueExpression: NSExpression(forConstantValue: 1),
falseExpression: NSExpression(forConstantValue: 0)
)
)
})
])
// Import the geojson data and add it to the data source.
let url = URL(string: "https://samples.azuremaps.com/data/geojson/SamplePoiDataSet.json")!
source.importData(fromURL: url)
// Add data source to the map.
map.sources.add(source)
// Create a bubble layer for rendering clustered data points.
let clusterBubbleLayer = BubbleLayer(
source: source,
options: [
.bubbleRadius(20),
.bubbleColor(.purple),
.bubbleStrokeWidth(0),
// Only rendered data points which have a point_count property, which clusters do.
.filter(from: NSPredicate(format: "point_count != NIL"))
]
)
// Add the clusterBubbleLayer to the map.
map.layers.addLayer(clusterBubbleLayer)
// Create a symbol layer to render the count of locations in a cluster.
map.layers.addLayer(
SymbolLayer(
source: source,
options: [
// Hide the icon image.
.iconImage(nil),
// Display the 'point_count_abbreviated' property value.
.textField(from: NSExpression(forKeyPath: "point_count_abbreviated")),
.textColor(.white),
.textOffset(CGVector(dx: 0, dy: 0.4)),
// Allow text overlapping so text is visible anyway
.textAllowOverlap(true),
// Only rendered data points which have a point_count property, which clusters do.
.filter(from: NSPredicate(format: "point_count != NIL"))
]
)
)
// Create a layer to render the individual locations.
map.layers.addLayer(
SymbolLayer(
source: source,
options: [
// Filter out clustered points from this layer.
SymbolLayerOptions.filter(from: NSPredicate(format: "point_count = NIL"))
]
)
)
// Add the delegate to handle taps on the clusterBubbleLayer only
// and display the aggregate details of the cluster.
map.events.addDelegate(self, for: [clusterBubbleLayer.id])
func azureMap(_ map: AzureMap, didTapOn features: [Feature]) {
guard let popup = popup, let cluster = features.first else {
// Popup has been released or no features provided
return
}
// Create a number formatter that removes decimal places.
let nf = NumberFormatter()
nf.maximumFractionDigits = 0
// Create the popup's content.
var text = ""
let pointCount = cluster.properties["point_count"] as! Int
let pointCountString = nf.string(from: pointCount as NSNumber)!
text.append("Cluster size: \(pointCountString) entities\n")
entityTypes.forEach { entityType in
text.append("\n")
text.append("\(entityType): ")
// Get the aggregated entity type count from the properties of the cluster by name.
let aggregatedCount = cluster.properties[entityType] as! Int
let aggregatedCountString = nf.string(from: aggregatedCount as NSNumber)!
text.append(aggregatedCountString)
}
// Create the custom view for the popup.
let customView = PopupTextView()
// Set the text to the custom view.
customView.setText(text)
// Get the position of the cluster.
let position = Math.positions(from: cluster).first!
// Set the options on the popup.
popup.setOptions([
// Set the popups position.
.position(position),
// Set the anchor point of the popup content.
.anchor(.bottom),
// Set the content of the popup.
.content(customView)
])
// Open the popup.
popup.open()
}
O pop-up segue as etapas descritas na exibição de um documento pop-up .
A imagem a seguir mostra o código acima exibindo um pop-up com contagens agregadas de cada tipo de valor de entidade para todos os pontos no ponto clusterizado tocado.
Informações adicionais
Para adicionar mais dados ao seu mapa: