Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapbox iOS clustering works, but circle style layer and numbers layer aren't appearing/reflecting the marker density of the cluster

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:

enter image description here

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:

enter image description here

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.

like image 897
David Chopin Avatar asked Apr 17 '19 20:04

David Chopin


1 Answers

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.

like image 54
David Chopin Avatar answered Oct 20 '22 00:10

David Chopin