Condividi tramite


Clustering dei dati dei punti in iOS SDK (anteprima)

Nota

ritiro di iOS SDK Mappe di Azure

Il Mappe di Azure Native SDK per iOS è ora deprecato e verrà ritirato il 3/31/25. Per evitare interruzioni del servizio, eseguire la migrazione all'SDK Web di Mappe di Azure entro il 3/31/25. Per altre informazioni, vedere La guida alla migrazione Mappe di Azure di iOS SDK per iOS.

Quando si visualizzano molti punti dati sulla mappa, i punti dati possono sovrapporsi tra loro. La sovrapposizione può rendere la mappa illeggibile e difficile da utilizzare. Il clustering dei dati dei punti è la combinazione di dati dei punti vicini tra loro e rappresentati sulla mappa come singolo punto dati in cluster. Quando l'utente ingrandisce la mappa, i cluster si scompongono nei singoli punti dati. Quando si lavora con un numero elevato di punti dati, è possibile implementare i processi di clustering per migliorare l'esperienza utente.

Internet of Things Show - Clustering point data in Mappe di Azure

Prerequisiti

Assicurarsi di completare i passaggi descritti nel documento Avvio rapido: Creare un'app iOS. I blocchi di codice in questo articolo possono essere inseriti nella viewDidLoad funzione di ViewController.

Abilitazione del clustering su un'origine dati

Abilitare il clustering nella DataSource classe impostando l'opzione cluster su true. Impostare clusterRadius per selezionare i punti dati nelle vicinanze e combinarli in un cluster. Il valore di clusterRadius è espresso in punti. Usare clusterMaxZoom per specificare un livello di zoom in base al quale disabilitare la logica di clustering. Ecco un esempio di come abilitare il clustering in un'origine dati.

// 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)
])

Attenzione

Il clustering funziona solo con Point le funzionalità. Se l'origine dati contiene funzionalità di altri tipi di geometria, ad esempio Polyline o Polygon, si verificherà un errore.

Suggerimento

Se due punti dati sono vicini, è possibile che il cluster non si scomponga mai, indipendentemente dal livello di ingrandimento selezionato dall'utente. Per risolvere questo problema, è possibile impostare l'opzione clusterMaxZoom sulla disabilitazione della logica di clustering per fare in modo che vengano visualizzati tutti gli elementi.

La DataSource classe fornisce anche i metodi seguenti correlati al clustering.

metodo Tipo restituito Descrizione
children(of cluster: Feature) [Feature] Recupera gli elementi figlio del cluster specificato al livello di zoom successivo. Questi elementi figlio possono essere una combinazione di caratteristiche e sottocluster. I sottocluster diventano funzionalità con proprietà corrispondenti a ClusteredProperties.
zoomLevel(forExpanding cluster: Feature) Double Calcola un livello di zoom in base al quale il cluster inizia a espandersi o suddividersi.
leaves(of cluster: Feature, offset: UInt, limit: UInt) [Feature] Recupera tutti i punti in un cluster. Impostare limit per restituire un subset dei punti e usare offset per spostarsi attraverso i punti.

Visualizzare i cluster con un livello bolle

Il livello bolle è particolarmente indicato per il rendering dei punti in cluster. Usare le espressioni per ridimensionare il raggio e modificare il colore in base al numero di punti nel cluster. Se si sceglie di visualizzare i cluster tramite un livello bolle, è necessario usare un livello separato per eseguire il rendering dei punti dati non in cluster.

Per visualizzare le dimensioni del cluster nella parte superiore della bolla, usare un livello simbolo con testo, senza nessuna icona.

Nel codice seguente vengono visualizzati i punti raggruppati usando un livello bolla e il numero di punti in ogni cluster usando un livello simbolo. Un secondo livello simbolo viene usato per visualizzare singoli punti che non si trovano all'interno di un 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"))
        ]
    )
)

L'immagine seguente mostra le caratteristiche del punto cluster nel codice precedente in un livello a bolle, ridimensionate e colorate in base al numero di punti nel cluster. Il rendering dei punti non cluster viene eseguito usando un livello simbolo.

Mappare le posizioni raggruppate separandosi durante lo zoom avanti della mappa.

Visualizzare i cluster con un livello simbolo

Quando si visualizzano punti dati, il livello simbolo nasconde automaticamente i simboli che si sovrappongono tra loro per garantire un'interfaccia utente più pulita. Questo comportamento predefinito potrebbe non essere desiderato se si intende visualizzare la densità dei punti dati sulla mappa. Tuttavia, è possibile modificare queste impostazioni. Per visualizzare tutti i simboli, impostare l'opzione iconAllowOverlap Livello simbolo su true.

Usare il clustering per mostrare la densità dei punti dati mantenendo al contempo un'interfaccia utente pulita. L'esempio seguente illustra come aggiungere simboli personalizzati e rappresentare cluster e singoli punti dati usando il livello simbolo.

// 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"))
        ]
    )
)

Per questo esempio, l'immagine seguente viene caricata nella cartella assets dell'app.

Immagine dell'icona terremoto Immagine dell'icona meteo delle piogge
earthquake-icon.png warning-triangle-icon.png

L'immagine seguente mostra il rendering del codice riportato sopra con le funzionalità dei punti cluster e non cluster usando icone personalizzate.

Mappa dei punti raggruppati di cui viene eseguito il rendering usando un livello simbolo.

Clustering e livello mappa termica

Le mappe termiche rappresentano un ottimo modo per visualizzare la densità dei dati sulla mappa. Questo metodo di visualizzazione può gestire autonomamente un numero elevato di punti dati. Se i punti dati sono raggruppati nel cluster e le dimensioni del cluster vengono usate come peso della mappa termica, quest'ultima può gestire ancora più dati. A questo scopo, impostare l'opzione heatmapWeight del livello mappa termica su NSExpression(forKeyPath: "point_count"). Quando il raggio del cluster è piccolo, la mappa termica è quasi identica a una mappa termica usando i punti dati non cluster, ma offre prestazioni migliori. Tuttavia, più piccolo è il raggio del cluster, maggiore è l'accuratezza della mappa termica, ma con un minor numero di vantaggi in termini di prestazioni.

// 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"
)

L'immagine seguente mostra il codice precedente che mostra una mappa termica ottimizzata usando le funzionalità dei punti cluster e il numero di cluster come peso nella mappa termica.

Mappa di una mappa termica ottimizzata usando punti cluster come peso.

Toccare gli eventi nei punti dati in cluster

Quando si verificano eventi di tocco su un livello che contiene punti dati cluster, il punto dati cluster torna all'evento come oggetto funzionalità punto GeoJSON. Questa funzionalità punto presenta le proprietà seguenti:

Nome proprietà Type Descrizione
cluster boolean Indica se la funzionalità rappresenta un cluster.
point_count number Numero di punti contenuti nel cluster.
point_count_abbreviated string Stringa che abbrevia il valore point_count se necessario. Ad esempio, 4.000 diventa 4K.

Questo esempio accetta un livello bolla che esegue il rendering dei punti del cluster e aggiunge un evento di tocco. Quando viene attivato l'evento tap, il codice calcola e esegue lo zoom della mappa al livello di zoom successivo, in corrispondenza del quale il cluster si interrompe. Questa funzionalità viene implementata usando il zoomLevel(forExpanding:) metodo della DataSource 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)
    ])
}

L'immagine seguente mostra il codice precedente che mostra i punti raggruppati su una mappa che, quando toccata, esegue lo zoom al livello di zoom successivo che un cluster inizia a suddividere ed espandere.

Mappa delle funzionalità in cluster che si ingrandisce e si separano quando viene toccato.

Visualizzare l'area del cluster

I dati dei punti rappresentati da un cluster sono distribuiti in un'area. In questo esempio quando viene toccato un cluster, si verificano due comportamenti principali. In primo luogo, i singoli punti dati contenuti nel cluster usato per calcolare uno scafo convesso. Quindi, lo scafo convesso viene visualizzato sulla mappa per mostrare un'area. Un hull convesso è un poligono che esegue il wrapping di un set di punti come se fosse una banda elastica e può essere calcolato usando il metodo convexHull(from:). Tutti i punti contenuti in un cluster possono essere recuperati dall'origine dati tramite il metodo 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)
    }
}

L'immagine seguente mostra il codice precedente che mostra l'area di tutti i punti all'interno di un cluster toccato.

Mappa che mostra il poligono dello scafo convesso di tutti i punti all'interno di un cluster toccato.

Aggregazione dei dati nei cluster

Spesso i cluster sono rappresentati da un simbolo in cui è indicato il numero di punti che contengono. Tuttavia, a volte è opportuno personalizzare lo stile dei cluster con metriche aggiuntive. Con le proprietà del cluster, le proprietà personalizzate possono essere create ed uguali a un calcolo in base alle proprietà all'interno di ogni punto con un cluster. Le proprietà del cluster possono essere definite nell'opzione clusterProperties di DataSource.

Il codice seguente calcola un conteggio basato sulla proprietà del tipo di entità di ogni punto dati in un cluster. Quando un utente tocca un cluster, viene visualizzato un popup con informazioni aggiuntive sul 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()
}

Il popup segue i passaggi descritti nella visualizzazione di un documento popup .

L'immagine seguente mostra il codice precedente che mostra un popup con conteggi aggregati di ogni tipo di valore di entità per tutti i punti nel punto toccato cluster.

Mappa che mostra la finestra popup dei conteggi aggregati dei tipi di entità di tutti i punti in un cluster.

Informazioni aggiuntive

Per aggiungere altri dati alla mappa: