iOS SDK でのポイント データのクラスタリング (プレビュー)
Note
Azure Maps iOS SDK の廃止
iOS 用 Azure Maps Native SDK は非推奨となり、2025 年 3 月 31 日に廃止されます。 サービスの中断を回避するには、2025 年 3 月 31 日までに Azure Maps Web SDK に移行します。 詳細については、「Azure Maps iOS SDK 移行ガイド」を参照してください。
多数のデータ ポイントをマップ上に表示すると、データ ポイントが互いに重なり合うことがあります。 重なり合うと、マップが読み取れなくなり、使用が困難になる可能性があります。 ポイント データのクラスタリングは、互いに近いポイント データを結合し、単一のクラスター化されたデータ ポイントとしてマップ上に表現するプロセスです。 ユーザーがマップにズーム インすると、クラスターは個々のデータ ポイントに分解します。 大量のデータ ポイントを操作する場合は、クラスタリング プロセスを使用して、ユーザー エクスペリエンスを向上させます。
モノのインターネット ショー - Azure Maps におけるポイント データのクラスタリング
前提条件
必ず、iOS アプリの作成のクイック スタートに関するドキュメントの手順を完了してください。 この記事のコード ブロックは、ViewController
の viewDidLoad
関数に挿入できます。
データ ソースでのクラスタリングの有効化
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
フィーチャーでのみ動作します。 データ ソースに Polyline
や Polygon
などの他の geometry 型のフィーチャーが含まれている場合は、エラーが発生します。
ヒント
2 つのデータ ポイントがきわめて近い場合、ユーザーがどれだけ近くまでズーム インしてもクラスターが分解しない可能性があります。 これに対処するには、clusterMaxZoom
オプションを設定し、クラスタリング ロジックを無効化して単純にすべてを表示することができます。
DataSource
クラスには、クラスタリングに関連する以下のメソッドも用意されています。
Method | の戻り値の型 : | 説明 |
---|---|---|
children(of cluster: Feature) |
[Feature] |
次のズーム レベルで指定されたクラスターの子を取得します。 これらの子はフィーチャーとサブクラスターの組み合わせの場合があります。 サブクラスターは ClusteredProperties と一致するプロパティをもつフィーチャーになります。 |
zoomLevel(forExpanding cluster: Feature) |
Double |
クラスターが拡大し始めるか、または分解するズーム レベルを計算します。 |
leaves(of cluster: Feature, offset: UInt, limit: UInt) |
[Feature] |
クラスター内のすべてのポイントを取得します。 ポイントのサブセットを返すには limit を設定し、ポイントをページ送りするには offset を使用します。 |
バブル レイヤーを使用してクラスターを表示する
バブルレイヤーは、クラスター化されたポイントをレンダリングするための優れた方法です。 クラスター内のポイントの数に基づき、式を使用して半径を拡大し、色を変更します。 バブル レイヤーを使用してクラスターを表示する場合、クラスター化されていないデータ ポイントをレンダリングするための別のレイヤーを使用することをお勧めします。
バブルの上にクラスターのサイズを表示するには、テキスト付きのシンボル レイヤーを使用し、アイコンを使用しないようにします。
次のコードでは、バブル レイヤーを使用してクラスター化されたポイントが表示され、シンボル レイヤーを使用して各クラスター内のポイント数が表示されます。 2 つ目のシンボル レイヤーは、クラスター内にない個々のポイントを表示するために使用されます。
// 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"))
]
)
)
このサンプルでは、次の画像がアプリのドローアブル フォルダーに読み込まれます。
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 ポイント フィーチャー オブジェクトとしてイベントに返されます。 このポイント フィーチャーには次のプロパティがあります。
プロパティ名 | 種類 | Description |
---|---|---|
cluster |
boolean | フィーチャーがクラスターを表すかどうかを示します。 |
point_count |
number | クラスターに含まれているポイントの数。 |
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)
])
}
次の画像は、上記のコードによって、タップするとクラスターが分裂と拡大を開始する次のズーム レベルへとズームインする、マップ上のクラスター化されたポイントが表示されている状態を示しています。
クラスター領域を表示する
クラスターが表すポイント データは領域に分散しています。 このサンプルでは、クラスターがタップされたときに主に 2 つの動作が生じます。 まず、クラスターに含まれている個々のデータ ポイントを使用して、凸包を計算します。 次に、領域を表示するために凸包がマップに表示されます。 凸包は輪ゴムのように一連のポイントを囲む多角形であり、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)
}
}
次の画像は、上記のコードにより、タップされたクラスター内のすべてのポイントの領域が表示される状態を示しています。
クラスター内のデータの集計
クラスターは多くの場合、クラスター内のポイントの数を示すシンボルを使用して表現されます。 ただし、追加のメトリックを使用して、クラスターのスタイルをカスタマイズすることが望ましい場合もあります。 クラスター プロパティを使用すると、カスタム プロパティを作成し、クラスターの各ポイント内のプロパティに基づいた計算と等しくすることができます。 クラスター プロパティは、DataSource
の clusterProperties
オプションで定義できます。
次のコードでは、クラスター内の各データ ポイントのエンティティ型プロパティに基づいてカウントを計算します。 ユーザーがクラスターをタップすると、クラスターに関する追加情報を示すポップアップが表示されます。
// 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()
}
ポップアップでは、「ポップアップを表示する」に記載されている手順に従います。
次の画像は、上記コードにより、タップされたクラスター化されたポイント内のすべてのポイントについて、各エンティティ値の型の集計数が含まれたポップアップが表示される状態を示しています。
関連情報
マップにさらにデータを追加するには: