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.