I am using Mapbox to create an iOS application. The application gets makes a request to my API that returns a number of events going on within the map's bounding box in JSON format.
I was previously not using clustering, so some map annotations were simply covering others. I am using this Mapbox tutorial which creates an MGLShapeCollectionFeature
from a GeoJSON file, creates an MGLShapeSource
from the shape collection feature, and then creates a marker layer as an MGLSymbolStyleLayer
, a circle layer as an MGLCircleStyleLayer
, and a numbers layer as an MGLSymbolStyleLayer
. The marker layer shows each individual event geographically, the circle layer and number layer come together to represent the marker count of each cluster.
The final product should look similar to the Mapbox example:
This is the GeoJSON file that the example uses to show clustering seaports on a world map.
The following is the relevant code that the example uses to convert said GeoJSON into the relevant source and layers to populate the map:
let url = URL(fileURLWithPath: Bundle.main.path(forResource: "ports", ofType: "geojson")!)
let source = MGLShapeSource(identifier: "clusteredPorts",
url: url,
options: [.clustered: true, .clusterRadius: icon.size.width])
style.addSource(source)
// Use a template image so that we can tint it with the `iconColor` runtime styling property.
style.setImage(icon.withRenderingMode(.alwaysTemplate), forName: "icon")
// Show unclustered features as icons. The `cluster` attribute is built into clustering-enabled
// source features.
let ports = MGLSymbolStyleLayer(identifier: "ports", source: source)
ports.iconImageName = NSExpression(forConstantValue: "icon")
ports.iconColor = NSExpression(forConstantValue: UIColor.darkGray.withAlphaComponent(0.9))
ports.predicate = NSPredicate(format: "cluster != YES")
style.addLayer(ports)
// Color clustered features based on clustered point counts.
let stops = [
20: UIColor.lightGray,
50: UIColor.orange,
100: UIColor.red,
200: UIColor.purple
]
// Show clustered features as circles. The `point_count` attribute is built into
// clustering-enabled source features.
let circlesLayer = MGLCircleStyleLayer(identifier: "clusteredPorts", source: source)
circlesLayer.circleRadius = NSExpression(forConstantValue: NSNumber(value: Double(icon.size.width) / 2))
circlesLayer.circleOpacity = NSExpression(forConstantValue: 0.75)
circlesLayer.circleStrokeColor = NSExpression(forConstantValue: UIColor.white.withAlphaComponent(0.75))
circlesLayer.circleStrokeWidth = NSExpression(forConstantValue: 2)
circlesLayer.circleColor = NSExpression(format: "mgl_step:from:stops:(point_count, %@, %@)", UIColor.lightGray, stops)
circlesLayer.predicate = NSPredicate(format: "cluster == YES")
style.addLayer(circlesLayer)
// Label cluster circles with a layer of text indicating feature count. The value for
// `point_count` is an integer. In order to use that value for the
// `MGLSymbolStyleLayer.text` property, cast it as a string.
let numbersLayer = MGLSymbolStyleLayer(identifier: "clusteredPortsNumbers", source: source)
numbersLayer.textColor = NSExpression(forConstantValue: UIColor.white)
numbersLayer.textFontSize = NSExpression(forConstantValue: NSNumber(value: Double(icon.size.width) / 2))
numbersLayer.iconAllowsOverlap = NSExpression(forConstantValue: true)
numbersLayer.text = NSExpression(format: "CAST(point_count, 'NSString')")
numbersLayer.predicate = NSPredicate(format: "cluster == YES")
style.addLayer(numbersLayer)
This is the GeoJSON format that my events are being returned from my API as. This formatting should be correct, as Mapbox is accepting it and creating an MGLShapeCollectionFeature
from its data.
My code is very similar to that seen in the Mapbox example. I first create the GeoJSON file
//geoJson is my GeoJSON file as [String: Any]
var shapes: MGLShapeCollectionFeature!
if let data = try? JSONSerialization.data(withJSONObject: geoJson, options: .prettyPrinted) {
do {
shapes = try MGLShape(data: data, encoding: String.Encoding.utf8.rawValue) as! MGLShapeCollectionFeature
} catch {
print(error.localizedDescription)
}
}
I know this GeoJSON is being converted into MGLShapeCollectionFeature
as the app would crash if it didn't, and the MGLShapeCollectionFeature
created successfully creates a source that layers are being created from/populating the map. So I create an MGLShapeSource
from this MGLShapeCollectionFeature
:
let marker = UIImage(named: "redPin")?.resize(targetSize: CGSize(width: 25, height: 25))
let source = MGLShapeSource(identifier: "clusteredPoints", shape: shapes, options: [.clustered: true, .clusterRadius: 0.5])
self.mapStyle!.addSource(source)
// Use a template image so that we can tint it with the `iconColor` runtime styling property.
self.mapStyle!.setImage(marker!, forName: "marker")
I then create layers from 'source' and add them to my map's style.
// Show unclustered features as icons. The `cluster` attribute is built into clustering-enabled
// source features.
let events = MGLSymbolStyleLayer(identifier: "events", source: source)
events.iconImageName = NSExpression(forConstantValue: "marker")
events.iconColor = NSExpression(forConstantValue: UIColor.darkGray.withAlphaComponent(0.9))
events.predicate = NSPredicate(format: "cluster != YES")
self.mapStyle!.addLayer(events)
// Color clustered features based on clustered point counts.
let stops = [
5: UIColor.lightGray,
10: UIColor.orange,
20: UIColor.red,
30: UIColor.purple
]
// Show clustered features as circles. The `point_count` attribute is built into
// clustering-enabled source features.
let circlesLayer = MGLCircleStyleLayer(identifier: "clusteredEvents", source: source)
circlesLayer.circleRadius = NSExpression(forConstantValue: NSNumber(value: Double(self.mapStyle!.image(forName: "marker")!.size.width) / 2))
circlesLayer.circleOpacity = NSExpression(forConstantValue: 0.75)
circlesLayer.circleStrokeColor = NSExpression(forConstantValue: UIColor.white.withAlphaComponent(0.75))
circlesLayer.circleStrokeWidth = NSExpression(forConstantValue: 2)
circlesLayer.circleColor = NSExpression(format: "mgl_step:from:stops:(point_count, %@, %@)", UIColor.lightGray, stops)
circlesLayer.predicate = NSPredicate(format: "cluster == YES")
self.mapStyle!.addLayer(circlesLayer)
// Label cluster circles with a layer of text indicating feature count. The value for
// `point_count` is an integer. In order to use that value for the
// `MGLSymbolStyleLayer.text` property, cast it as a string.
let numbersLayer = MGLSymbolStyleLayer(identifier: "clusteredEventsNumbers", source: source)
numbersLayer.textColor = NSExpression(forConstantValue: UIColor.white)
numbersLayer.textFontSize = NSExpression(forConstantValue: NSNumber(value: Double(self.mapStyle!.image(forName: "marker")!.size.width) / 2))
numbersLayer.iconAllowsOverlap = NSExpression(forConstantValue: true)
numbersLayer.text = NSExpression(format: "CAST(point_count, 'NSString')")
numbersLayer.predicate = NSPredicate(format: "cluster == YES")
self.mapStyle!.addLayer(numbersLayer)
So the code is essentially the exact same, just the GeoJSON being input is different. And yet, the circle layer and numbers layer is not appearing when the events markers cluster. See below:
I know that the issue isn't that the Mapbox example's source is being loaded from a URL while my implementation's source is being loaded from an MGLShapeCollectionFeature
, because I've tried loading the Mapbox example's seaports GeoJSON as an MGLShapeCollectionFeature
and the seaports still show the circle/numbers layers when clustered.
So, I feel like an idiot.
The issue was in the MGLShapeSource:
MGLShapeSource(identifier: "clusteredPoints", shape: shapes, options: [.clustered: true, .clusterRadius: 0.5])
For whatever reason, I had been messing around with the clusterRadius, and had it set to 0.5, which I assume is in points. Note that the example was using the marker's width to determine the cluster radius.
let source = MGLShapeSource(identifier: "clusteredPorts",
url: url,
options: [.clustered: true, .clusterRadius: icon.size.width])
I though that, because some of the markers were disappearing whenever they overlapped with another marker, that they were clustering but the cluster layer was not being shown. They weren't clustering, I guess shape sources are just able to know when they are overlapping with another, and will disappear accordingly. Just because they disappear doesn't meant that they are clustered.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With