共用方式為


在 iOS SDK 中群集點資料 (預覽版)

注意

Azure 地圖服務 iOS SDK 淘汰

適用於 iOS 的 Azure 地圖服務 原生 SDK 現在已被取代,將於 3/31/25 淘汰。 若要避免服務中斷,請透過 3/31/25 移轉至 Azure 地圖服務 Web SDK。 如需詳細資訊,請參閱 Azure 地圖服務 iOS SDK 移轉指南

在地圖上顯示許多資料點時,資料點可能會彼此重疊。 重疊會導致地圖無法閱讀且難以使用。 群集位置點資料的程序會結合彼此接近的位置點資料,並在地圖上以單一群集資料點的方式加以表示。 當使用者放大地圖時,群集便會分解成個別的資料點。 當處理大量資料點時,請使用叢集處理序來改善使用者體驗。

Internet of Things Show - 在 Azure 地圖服務中群集點資料

必要條件

請務必完成快速入門:建立 iOS 應用程式文件中的步驟。 本文中的程式碼區塊可以插入 ViewControllerviewDidLoad 函式中。

對資料來源啟用叢集

cluster 選項設定為 true,以在 DataSource 類別中啟用群集。 設定 clusterRadius 以選取附近的資料點,並將這些資料點合併成叢集。 clusterRadius 的值以點為單位。 使用 clusterMaxZoom 指定停用群集邏輯的縮放層級。 以下範例示範如何在資料來源中啟用叢集。

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

警告

群集僅適用於 Point 功能。 如果您的資料來源包含其他幾何類型的功能,例如 PolylinePolygon,會發生錯誤。

提示

如果兩個資料點彼此很靠近,不論使用者放大到什麼程度,叢集可能都不會拆分開。 若要解決此問題,您可將 clusterMaxZoom 選項設為停用叢集邏輯,只顯示所有項目。

DataSource 類別也提供下列群集相關方法。

方法 傳回類型 描述
children(of cluster: Feature) [Feature] 擷取給定群集在下一個縮放層級上的子系。 這些子系可以是功能和子叢集的組合。 子叢集會成為屬性符合 ClusteredProperties 的特徵。
zoomLevel(forExpanding cluster: Feature) Double 計算叢集要開始擴大或分解的縮放層級。
leaves(of cluster: Feature, offset: UInt, limit: UInt) [Feature] 擷取群集中的所有位置點。 設定 limit 可傳回位置點的子集,使用 offset 則可逐頁查看位置點。

使用泡泡圖層顯示叢集

泡泡圖層是呈現叢集資料點的絕佳方式。 使用運算式調整半徑,並根據叢集中的資料點數量變更色彩。 如果使用泡泡圖層顯示叢集,則建議使用其他圖層呈現未叢集化的資料點。

若要在泡泡頂端顯示叢集大小,請使用符號圖層搭配文字,且不要使用圖示。

下列程式碼會使用泡泡圖層顯示叢集點,以及使用符號圖層顯示每個叢集中的點數目。 第二個符號圖層是用來顯示不在叢集中的個別點。

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

下圖說明上方程式碼會在泡泡圖層中顯示叢集點功能,根據叢集中的點數目縮放和著色。 未叢集的點會使用符號圖層呈現。

放大地圖時,地圖叢集位置會分開。

使用符號圖層顯示叢集

顯示資料點時,符號圖層會自動隱藏彼此重疊的符號,以確保使用者介面更整潔。 如果想在地圖上顯示資料點的密度,則可能不需要這種預設行為。 但您可變更這些設定。 若要顯示所有符號,請將符號圖層的 iconAllowOverlap 選項設為 true

使用叢集顯示資料點密度,同時保持乾淨的使用者介面。 以下範例示範如何使用符號圖層來新增自訂符號,以及表示叢集和個別資料點。

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

在此範例中,下列影像會載入至應用程式的 assets 資料夾。

地震圖示影像 雨雨的天氣圖示影像
earthquake-icon.png warning-triangle-icon.png

下圖說明上方程式碼使用自訂圖示呈現叢集和未叢集點功能。

使用符號圖層呈現的叢集點對應。

群集和熱度圖圖層

熱度圖是在地圖上顯示資料密度的絕佳方式。 這個視覺效果方法可自行處理大量的資料點。 如果資料點已叢集化,且叢集大小作為熱度圖的權數使用,則此熱度圖可處理更多資料。 若要完成此選項,請將熱度圖圖層的 [heatmapWeight] 選項設為 [NSExpression(forKeyPath: "point_count")]。 如果叢集半徑很小,則此熱度圖和使用未叢集化資料點的熱度圖看起來幾乎一致,但執行效果更好。 不過,叢集半徑愈小,熱度圖的精確度就愈高,但效能優勢也隨之減少。

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

下圖說明上方程式碼顯示使用叢集點功能最佳化的熱度圖,並將叢集數目當作熱度圖中的加權。

使用叢集點作為權數優化之熱度圖的地圖。

叢集資料點的點選事件

點選事件發生在包含叢集資料點的圖層上時,叢集資料點會以 GeoJSON 點功能物件的形式傳回事件。 這個點特徵具有下列屬性:

屬性名稱 類型​ 描述
cluster boolean 指出功能是否代表群集。
point_count 數值 群集包含的位置點數目。
point_count_abbreviated string 會縮寫較長 point_count 值的字串。 (例如,4,000 變成 4K)

此範例使用會呈現叢集點並新增點選事件的泡泡圖層。 觸發點選事件時,程式碼會計算地圖,將其縮放至可拆分叢集的下一個縮放層級。 這項功能是使用 DataSource 類別的 zoomLevel(forExpanding:) 方法來實作。

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

下圖說明上方程式碼會在點選時在地圖上顯示叢集點,並放大至開始拆分並展開叢集的下一個縮放層級。

點選時放大和分開的叢集特徵地圖。

顯示叢集區域

叢集代表的點資料會散佈在某個區域。 在此範例中,點選叢集時會發生兩個主要行為。 首先,使用包含在叢集中的個別資料點來計算凸殼。 然後,凸殼會出現在地圖上以顯示某個區域。 凸殼是一個多邊形,其類似橡皮筋一樣包裹住一組可用 convexHull(from:) 方法計算的資料點。 您可使用 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)
    }
}

下圖說明上方程式碼顯示點選叢集內所有點的區域。

地圖顯示點選叢集中所有點的凸體多邊形。

彙總叢集中的資料

叢集通常會使用一個符號加上叢集中的點數來表示。 但有時候,您會想要使用其他計量來自訂叢集樣式。 您可以使用叢集屬性,建立自訂屬性,並等於根據叢集之每個點內的屬性的計算。 叢集屬性可在 DataSourceclusterProperties 選項中定義。

以下程式碼會根據叢集中每個資料點的實體類型屬性來計算數量。 當使用者點選叢集時,快顯視窗就會顯示與叢集相關的其他資訊。

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

快顯視窗會按照顯示快顯視窗 (機器翻譯) 文件中列出的步驟。

下圖說明上方程式碼顯示快顯視窗,其中包含點選叢集點中所有點的每個實體值類型彙總數目。

顯示叢集中所有點之實體類型匯總計數快顯的對應。

其他資訊

若要將更多資料新增至您的地圖: