Como criar um cluster de dados de ponto no SDK para iOS (versão prévia)
Observação
Desativação do SDK do iOS no Azure Mapas
O SDK Nativo do Azure Mapas para iOS já foi preterido e será desativado em 31/03/25. Para evitar interrupções de serviço, migre para o SDK da Web do Azure Mapas até 31/03/25. Para obter mais informações, confira O guia de migração do SDK do iOS no Azure Mapas.
Ao visualizar muitos pontos de dados no mapa, eles poderão se sobrepor uns aos outros. A sobreposição pode fazer com que o mapa se torne ilegível e difícil de usar. O clustering de dados de ponto são o processo de combinar dados de ponto que estão próximos uns dos outros e os representar no mapa como um ponto de dados clusterizado individual. À medida que o usuário aplica zoom ao 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 – Criar um cluster de dados de ponto nos Azure Mapas
Pré-requisitos
Conclua as etapas no artigo Guia de Início Rápido: Criar um documento de aplicativo iOS . Os blocos de código deste artigo podem ser inseridos na função viewDidLoad
de ViewController
.
Habilitar o clustering na fonte de dados
Habilite o clustering na classe DataSource
definindo a opção cluster
como true
. Defina clusterRadius
para selecionar pontos de dados próximos e combiná-los em um cluster. O valor de clusterRadius
está em pontos. Use clusterMaxZoom
para especificar um nível de ampliação para o qual deseja desabilitar a lógica de clustering. Aqui está 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)
])
Cuidado
O clustering só funciona com recursos Point
. Se a fonte de dados contiver recursos de outros tipos de geometria, como Polyline
ou Polygon
, ocorrerá um erro.
Dica
Se dois pontos de dados estiverem próximos no solo, possivelmente o cluster nunca será dividido, independentemente de quanto o usuário ampliar. Para resolver isso, você pode definir a opção clusterMaxZoom
para desabilitar a lógica de clustering e simplesmente exibir tudo.
A classe DataSource
também fornece os seguintes métodos relacionados ao clustering.
Método | Tipo de retorno | Descrição |
---|---|---|
children(of cluster: Feature) |
[Feature] |
Recupera os filhos do cluster fornecido no próximo nível de zoom. Esses filhos podem ser uma combinação de recursos e subclusters. 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çará 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 offset para paginar os pontos. |
Exibir clusters usando uma camada de bolha
Uma camada de bolha é uma ótima maneira de renderizar os pontos clusterizados. Use as 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 bolha, 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 os pontos clusterizados 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 o código acima exibir recursos de ponto clusterizado em uma camada de bolha, escalada e colorida com base no número de pontos no cluster. Os pontos não clusterizados são renderizados por meio de uma camada de símbolo.
Exibir clusters usando uma camada de símbolo
Ao visualizar pontos de dados, a camada de símbolo oculta automaticamente os símbolos que se sobrepõem para garantir uma interface do usuário mais limpa. Esse comportamento padrão poderá ser indesejável se você quiser mostrar a densidade dos pontos de dados no mapa. No entanto, a maioria dessas configurações pode ser alterada. Para ver todos os símbolos, defina a opção iconAllowOverlap
da camadas de Símbolo como true
.
Use o clustering para mostrar a densidade dos pontos de dados enquanto mantém uma interface do 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ímbolo.
// 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"))
]
)
)
Neste exemplo, a imagem a seguir foi carregada na pasta de ativos do aplicativo.
earthquake-icon.png | warning-triangle-icon.png |
A imagem a seguir mostra o código acima renderizando os recursos de ponto clusterizado e não clusterizado usando ícones personalizados.
Clustering e a camada de mapas de calor
Mapas de calor são uma ótima maneira de exibir a densidade dos dados no mapa. Esse método de visualização processa um grande número de pontos de dados por conta própria. Se os pontos de dados forem clusterizados 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 obter essa opção, defina a opção heatmapWeight
da camada do mapa de calor como NSExpression(forKeyPath: "point_count")
. Quando o raio do cluster for pequeno, o mapa de calor será praticamente idêntico a um mapa de calor que usa os pontos de dados não clusterizados, mas ele terá um desempenho muito melhor. No entanto, quanto menor for o raio do cluster, mais preciso será 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 que é otimizado por meio de recursos de ponto clusterizado e a contagem de clusters como o peso no mapa de calor.
Eventos de toque em pontos de dados clisterizados
Quando ocorrem eventos de toque em uma camada que contém pontos de dados clusterizados, o ponto de dados cauterizado retorna ao evento como um objeto de recurso de ponto GeoJSON. O recurso de ponto terá as seguintes propriedades:
Nome da propriedade | Type | Descrição |
---|---|---|
cluster |
booleano | 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 abreviará o valor point_count , 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 de toque. Quando o evento de toque é disparado, 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 por meio do método zoomLevel(forExpanding:)
da classe DataSource
.
// 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 clusterizados em um mapa que, quando recebe um toque, amplia no próximo nível de zoom em que um cluster começa a se separar e se expandir.
Exibir a área do cluster
Os dados de ponto que um cluster representa são distribuídos por uma área. Neste exemplo, quando um cluster é tocado, ocorrem dois comportamentos principais. Primeiro, os pontos de dados individuais contidos no cluster serão usados para calcular uma envoltória convexa. Em seguida, a envoltória convexa será exibida no mapa para mostrar uma área. Uma envoltória convexa é um polígono que encapsula um conjunto de pontos como uma banda elástica e pode ser calculada usando o método convexHull(from:)
. Todos os pontos contidos em um cluster podem ser recuperados da fonte de dados usando o método leaves(of:offset:limit:)
.
// 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 em um cluster que recebeu um toque.
Agregando dados em clusters
Geralmente, 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 dos clusters com métricas adicionais. Com as propriedades do cluster, as propriedades personalizadas podem ser criadas e iguais a um cálculo com base nas propriedades de cada ponto com um cluster. As agregações do cluster podem ser definidas na opção clusterProperties
da 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 no documento Exibir um pop-up.
A imagem a seguir mostra o código acima exibindo um pop-up com as contagens agregadas de cada tipo de valor de entidade para todos os pontos no ponto clusterizado que recebeu um toque.
Informações adicionais
Para adicionar mais dados ao seu mapa: