/
SymbolClusteringExample.swift
202 lines (166 loc) · 9.23 KB
/
SymbolClusteringExample.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import UIKit
import MapboxMaps
@objc(SymbolClusteringExample)
class SymbolClusteringExample: UIViewController, ExampleProtocol {
internal var mapView: MapView!
override public func viewDidLoad() {
super.viewDidLoad()
// Create a `MapView` centered over Washington, DC.
let center = CLLocationCoordinate2D(latitude: 38.889215, longitude: -77.039354)
let cameraOptions = CameraOptions(center: center, zoom: 11)
let mapInitOptions = MapInitOptions(cameraOptions: cameraOptions, styleURI: .dark)
mapView = MapView(frame: view.bounds, mapInitOptions: mapInitOptions)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)
// Add the source and style layers once the map has loaded.
mapView.mapboxMap.onNext(event: .mapLoaded) { _ in
self.addSymbolClusteringLayers()
}
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(gestureRecognizer:)))
mapView.addGestureRecognizer(tapGestureRecognizer)
}
func addSymbolClusteringLayers() {
let style = mapView.mapboxMap.style
// The image named `fire-station-11` is included in the app's Assets.xcassets bundle.
// In order to recolor an image, you need to add a template image to the map's style.
// The image's rendering mode can be set programmatically or in the asset catalogue.
let image = UIImage(named: "fire-station-11")!.withRenderingMode(.alwaysTemplate)
// Add the image tp the map's style. Set `sdf` to `true`. This allows the icon images to be recolored.
// For more information about `SDF`, or Signed Distance Fields, see
// https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/#what-are-signed-distance-fields-sdf
try! style.addImage(image, id: "fire-station-icon", sdf: true)
// Fire_Hydrants.geojson contains information about fire hydrants in the District of Columbia.
// It was downloaded on 6/10/21 from https://opendata.dc.gov/datasets/DCGIS::fire-hydrants/about
let url = Bundle.main.url(forResource: "Fire_Hydrants", withExtension: "geojson")!
// Create a GeoJSONSource using the previously specified URL.
var source = GeoJSONSource()
source.data = .url(url)
// Enable clustering for this source.
source.cluster = true
source.clusterRadius = 75
// Create expression to identify the max flow rate of one hydrant in the cluster
// ["max", ["get", "FLOW"]]
let maxExpression = Exp(.max) {Exp(.get) { "FLOW" }}
// Create expression to determine if a hydrant with EngineID E-9 is in the cluster
// ["any", ["==", ["get", "ENGINEID"], "E-9"]]
let ine9Expression = Exp(.any) {
Exp(.eq) {
Exp(.get) { "ENGINEID" }
"E-9"
}
}
// Create expression to get the sum of all of the flow rates in the cluster
// [["+", ["accumulated"], ["get", "sum"]], ["get", "FLOW"]]
let sumExpression = Exp {
Exp(.sum) {
Exp(.accumulated)
Exp(.get) { "sum" }
}
Exp(.get) { "FLOW" }
}
// Add the expressions to the cluster as ClusterProperties so they can be accessed below
let clusterProperties: [String: Expression] = [
"max": maxExpression,
"in_e9": ine9Expression,
"sum": sumExpression
]
source.clusterProperties = clusterProperties
let sourceID = "fire-hydrant-source"
var clusteredLayer = createClusteredLayer()
clusteredLayer.source = sourceID
var unclusteredLayer = createUnclusteredLayer()
unclusteredLayer.source = sourceID
// `clusterCountLayer` is a `SymbolLayer` that represents the point count within individual clusters.
var clusterCountLayer = createNumberLayer()
clusterCountLayer.source = sourceID
// Add the source and two layers to the map.
try! style.addSource(source, id: sourceID)
try! style.addLayer(clusteredLayer)
try! style.addLayer(unclusteredLayer, layerPosition: .below(clusteredLayer.id))
try! style.addLayer(clusterCountLayer)
// This is used for internal testing purposes only and can be excluded
// from your implementation.
finish()
}
func createClusteredLayer() -> CircleLayer {
// Create a symbol layer to represent the clustered points.
var clusteredLayer = CircleLayer(id: "clustered-circle-layer")
// Filter out unclustered features by checking for `point_count`. This
// is added to clusters when the cluster is created. If your source
// data includes a `point_count` property, consider checking
// for `cluster_id`.
clusteredLayer.filter = Exp(.has) { "point_count" }
// Set the color of the icons based on the number of points within
// a given cluster. The first value is a default value.
clusteredLayer.circleColor = .expression(Exp(.step) {
Exp(.get) { "point_count" }
UIColor.systemGreen
50
UIColor.systemBlue
100
UIColor.systemRed
})
clusteredLayer.circleRadius = .constant(25)
return clusteredLayer
}
func createUnclusteredLayer() -> SymbolLayer {
// Create a symbol layer to represent the points that aren't clustered.
var unclusteredLayer = SymbolLayer(id: "unclustered-point-layer")
// Filter out clusters by checking for `point_count`.
unclusteredLayer.filter = Exp(.not) {
Exp(.has) { "point_count" }
}
unclusteredLayer.iconImage = .constant(.name("fire-station-icon"))
unclusteredLayer.iconColor = .constant(StyleColor(.white))
// Rotate the icon image based on the recorded water flow.
// The `mod` operator allows you to use the remainder after dividing
// the specified values.
unclusteredLayer.iconRotate = .expression(Exp(.mod) {
Exp(.get) { "FLOW" }
360
})
return unclusteredLayer
}
func createNumberLayer() -> SymbolLayer {
var numberLayer = SymbolLayer(id: "cluster-count-layer")
// check whether the point feature is clustered
numberLayer.filter = Exp(.has) { "point_count" }
// Display the value for 'point_count' in the text field
numberLayer.textField = .expression(Exp(.get) { "point_count" })
numberLayer.textSize = .constant(12)
return numberLayer
}
@objc func handleTap(gestureRecognizer: UITapGestureRecognizer) {
let point = gestureRecognizer.location(in: mapView)
// Look for features at the tap location within the clustered and
// unclustered layers.
mapView.mapboxMap.queryRenderedFeatures(with: point,
options: RenderedQueryOptions(layerIds: ["unclustered-point-layer", "clustered-circle-layer"],
filter: nil)) { [weak self] result in
switch result {
case .success(let queriedFeatures):
// Return the first feature at that location, then pass attributes to the alert controller.
// Check whether the feature has values for `ASSETNUM` and `LOCATIONDETAIL`. These properties
// come from the fire hydrant dataset and indicate that the selected feature is not clustered.
if let selectedFeatureProperties = queriedFeatures.first?.feature.properties,
case let .string(featureInformation) = selectedFeatureProperties["ASSETNUM"],
case let .string(location) = selectedFeatureProperties["LOCATIONDETAIL"] {
self?.showAlert(withTitle: "Hydrant \(featureInformation)", and: "\(location)")
// If the feature is a cluster, it will have `point_count` and `cluster_id` properties. These are assigned
// when the cluster is created.
} else if let selectedFeatureProperties = queriedFeatures.first?.feature.properties,
case let .number(pointCount) = selectedFeatureProperties["point_count"],
case let .number(clusterId) = selectedFeatureProperties["cluster_id"],
case let .number(maxFlow) = selectedFeatureProperties["max"],
case let .number(sum) = selectedFeatureProperties["sum"],
case let .boolean(in_e9) = selectedFeatureProperties["in_e9"] {
// If the tap landed on a cluster, pass the cluster ID and point count to the alert.
let inEngineNine = in_e9 ? "Some hydrants belong to Engine 9." : "No hydrants belong to Engine 9."
self?.showAlert(withTitle: "Cluster ID \(Int(clusterId))", and: "There are \(Int(pointCount)) hydrants in this cluster. The highest water flow is \(Int(maxFlow)) and the collective flow is \(Int(sum)). \(inEngineNine)")
}
case .failure(let error):
self?.showAlert(withTitle: "An error occurred: \(error.localizedDescription)", and: "Please try another hydrant")
}
}
}
}