Sdílet prostřednictvím


Data bodu clusteringu v sadě iOS SDK (Preview)

Poznámka:

Vyřazení sady Azure Mapy iOS SDK

Sada Azure Mapy Native SDK pro iOS je teď zastaralá a bude vyřazena 31. 31. 25. Pokud se chcete vyhnout přerušení služeb, proveďte migraci do sady Azure Mapy Web SDK do 31. 31. 25. Další informace najdete v průvodci migrací sady Azure Mapy iOS SDK.

Při zobrazení mnoha datových bodů na mapě se datové body můžou překrývat mezi sebou. Překrytí může způsobit, že mapa může být nečitelná a obtížně použitelná. Data bodu clusteringu jsou proces kombinování dat bodů, která jsou blízko sebe a představují je na mapě jako jeden skupinový datový bod. Když se uživatel přiblíží k mapě, clustery se rozdělí do jednotlivých datových bodů. Při práci s velkým počtem datových bodů můžete pomocí procesů clusteringu zlepšit uživatelské prostředí.

Zobrazení Internetu věcí – data clusteringového bodu v Azure Mapy

Požadavky

Nezapomeňte dokončit kroky v rychlém startu : Vytvoření dokumentu aplikace pro iOS. Bloky kódu v tomto článku lze vložit do viewDidLoad funkce ViewController.

Povolení clusteringu ve zdroji dat

Povolte clustering ve DataSource třídě nastavením cluster možnosti truena . Nastavte clusterRadius výběr blízkých datových bodů a zkombinujte je do clusteru. Hodnota clusterRadius je v bodech. Slouží clusterMaxZoom k určení úrovně přiblížení, na které chcete zakázat logiku clusteringu. Tady je příklad povolení clusteringu ve zdroji dat.

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

Upozornění

Clustering funguje jenom s funkcemi Point . Pokud zdroj dat obsahuje funkce jiných typů geometrie, například Polyline nebo Polygon, dojde k chybě.

Tip

Pokud jsou dva datové body na zemi blízko, je možné, že se cluster nikdy nerozčlení, bez ohledu na to, jak blízko se uživatel přibližuje. Pokud to chcete vyřešit, můžete nastavit clusterMaxZoom možnost zakázat logiku clusteringu a jednoduše zobrazit vše.

Třída DataSource poskytuje také následující metody související s clusteringem.

Metoda Návratový typ Popis
children(of cluster: Feature) [Feature] Načte podřízené položky daného clusteru na další úrovni přiblížení. Tyto podřízené položky mohou být kombinací funkcí a podkluzů. Podkluzy se stanou funkcemi s vlastnostmi odpovídajícími clusteredProperties.
zoomLevel(forExpanding cluster: Feature) Double Vypočítá úroveň přiblížení, na které se cluster začne rozbalovat nebo rozdělovat.
leaves(of cluster: Feature, offset: UInt, limit: UInt) [Feature] Načte všechny body v clusteru. limit Nastavte, aby se vrátila podmnožina bodů, a použijte offset stránku k procházení bodů.

Zobrazení clusterů pomocí bublinové vrstvy

Bublinová vrstva představuje skvělý způsob, jak vykreslit skupinové body. Pomocí výrazů můžete škálovat poloměr a změnit barvu na základě počtu bodů v clusteru. Pokud zobrazíte clustery pomocí vrstvy bublin, měli byste k vykreslení neclusterovaných datových bodů použít samostatnou vrstvu.

Pokud chcete zobrazit velikost clusteru nad bublinou, použijte vrstvu symbolů s textem a nepoužívejte ikonu.

Následující kód zobrazí skupinové body pomocí bublinové vrstvy a počet bodů v každém clusteru pomocí vrstvy symbolů. Druhá vrstva symbolů slouží k zobrazení jednotlivých bodů, které nejsou v clusteru.

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

Následující obrázek znázorňuje výše uvedené funkce clusterového bodu ve vrstvě bublin, škálované a barevné na základě počtu bodů v clusteru. Neclusterované body se vykreslují pomocí vrstvy symbolů.

Mapování skupinových umístění, která se oddělují při přiblížení mapy

Zobrazení clusterů pomocí vrstvy symbolů

Při zobrazení datových bodů vrstva symbolů automaticky skryje symboly, které se vzájemně překrývají, aby se zajistilo čistější uživatelské rozhraní. Toto výchozí chování může být nežádoucí, pokud chcete zobrazit hustotu datových bodů na mapě. Tato nastavení ale můžete změnit. Chcete-li zobrazit všechny symboly, nastavte iconAllowOverlap možnost vrstvy Symbol na true.

Pomocí clusteringu můžete zobrazit hustotu datových bodů a zachovat čisté uživatelské rozhraní. Následující ukázka ukazuje, jak přidat vlastní symboly a znázorňovat clustery a jednotlivé datové body pomocí vrstvy symbolů.

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

Pro tuto ukázku se následující obrázek načte do složky prostředků aplikace.

Obrázek ikony zemětřesení Obrázek ikony počasí s dešťovou sprchou
earthquake-icon.png warning-triangle-icon.png

Následující obrázek znázorňuje výše uvedené funkce vykreslování kódu v clusteru a neclusterovaných bodech pomocí vlastních ikon.

Mapa skupinových bodů vykreslených pomocí vrstvy symbolů

Clustering a vrstva heat mapy

Heat mapy představují skvělý způsob, jak zobrazit hustotu dat na mapě. Tato metoda vizualizace dokáže zpracovat velký počet datových bodů samostatně. Pokud jsou datové body seskupené a velikost clusteru se použije jako váha heat mapy, může heat mapa zpracovat ještě více dat. Pokud chcete tuto možnost dosáhnout, nastavte heatmapWeight možnost vrstvy heat mapy na NSExpression(forKeyPath: "point_count")hodnotu . Pokud je poloměr clusteru malý, heat mapa vypadá téměř stejně jako heat mapa pomocí neclusterovaných datových bodů, ale funguje lépe. Čím menší je však poloměr clusteru, tím přesnější je heat mapa, ale s menším počtem výhod výkonu.

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

Následující obrázek ukazuje výše uvedený kód zobrazující heat mapu optimalizovanou pomocí funkcí skupinového bodu a počet clusterů jako váha v heat mapě.

Mapa heat mapy optimalizované pomocí skupinových bodů jako váhy

Klepněte na události v clusterovaných datových bodech.

Když dojde k událostem klepnutí ve vrstvě, která obsahuje clusterované datové body, clusterovaný datový bod se vrátí k události jako objekt funkce bodu GeoJSON. Tato funkce bodu má následující vlastnosti:

Název vlastnosti Type Popis
cluster boolean Označuje, jestli funkce představuje cluster.
point_count Číslo Počet bodů, které cluster obsahuje.
point_count_abbreviated string Řetězec, který zkracuje point_count hodnotu, pokud je dlouhá. (například 4 000 se změní na 4K)

Tento příklad přebírá bublinovou vrstvu, která vykreslí body clusteru a přidá událost klepnutí. Když se aktivuje událost klepnutí, kód vypočítá a přiblíží mapu na další úroveň přiblížení, na které se cluster rozdělí. Tato funkce je implementována pomocí zoomLevel(forExpanding:) metody DataSource třídy.

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

Následující obrázek znázorňuje výše uvedený kód zobrazující skupinové body na mapě, která po klepnutí přibližuje další úroveň přiblížení, kterou cluster začne rozdělit a rozbalit.

Mapa skupinových funkcí se při klepnutí přibližuje a rozděluje.

Oblast zobrazení clusteru

Data bodu, která cluster představuje, jsou rozložena do oblasti. V této ukázce při klepnutí na cluster dojde ke dvěma hlavním chováním. Nejprve jednotlivé datové body obsažené v clusteru použité k výpočtu konvexní trupu. Pak se na mapě zobrazí konvexní trupu, aby se zobrazila oblast. Konvexní trupu je mnohoúhelník, který zabalí sadu bodů jako elastický pás a dá se vypočítat pomocí convexHull(from:) metody. Všechny body obsažené v clusteru je možné načíst ze zdroje dat pomocí leaves(of:offset:limit:) metody.

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

Následující obrázek znázorňuje výše uvedený kód zobrazující oblast všech bodů v rámci klepaného clusteru.

Mapa znázorňující konvexní mnohoúhelník trupu všech bodů v rámci klepané shluky

Agregace dat v clusterech

Clustery jsou často reprezentovány pomocí symbolu s počtem bodů, které jsou v clusteru. Někdy je ale žádoucí přizpůsobit styl clusterů dalšími metrikami. S vlastnostmi clusteru lze vytvořit vlastní vlastnosti a rovnat výpočtu na základě vlastností v jednotlivých bodech s clusterem. Vlastnosti clusteru lze definovat v clusterProperties možnosti DataSource.

Následující kód vypočítá počet na základě vlastnosti typu entity každého datového bodu v clusteru. Když uživatel klepne na cluster, zobrazí se automaticky otevírané okno s dalšími informacemi o clusteru.

// 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()
}

Automaticky otevírané okno se řídí kroky popsanými v automaticky otevíraných dokumentech.

Následující obrázek ukazuje výše uvedený kód zobrazující automaticky otevírané okno s agregovanými počty jednotlivých typů hodnot entity pro všechny body v klepaném skupinovém bodu.

Mapa zobrazující automaticky otevírané okno agregovaných počtů typů entit všech bodů v clusteru

Další informace

Přidání dalších dat do mapy: