[GIS] Mapbox GL control visibility on multiple labels using zoom attribute

ioslabelingmapbox-glmapbox-studioswift

I have a point dataset that is intended only as labels. Each label has a pre-calculated minZoom attribute to one decimal place, e.g. 6.1. While there are labels that share the same value, they vary considerably. I've figured out how to programmatically add a single label. It's workable, I suppose, to add a layer for each label. But performance may suffer. Is there a way, either via Mapbox Studio or programmatically (for the iOS SDK) to set the camera stops individually for each label but keep them within a single layer?

func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle)
{
    // Try adding point with label
    let coordinates: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 49, longitude: -114)

    let testPt = MGLPointFeature()
    testPt.coordinate = coordinates
    testPt.title = "Testing label"
    testPt.attributes = ["title": testPt.title as Any]

    let src = MGLShapeSource(identifier: "testPtID", features: [testPt], options: nil)

    mapView.style?.addSource(src)

    let symbols = MGLSymbolStyleLayer(identifier: "testPtSym", source: src)

    let color = UIColor(red: 0.08, green: 0.75, blue: 0.96, alpha: 1.0)

    symbols.textColor = MGLStyleValue(rawValue: color)
    symbols.text = MGLStyleValue(rawValue: "{title}")
    symbols.textFontSize = MGLStyleValue(rawValue: 24)
    symbols.textOpacity = MGLStyleValue(interpolationMode: .exponential,
                                        cameraStops: [
                                            6: MGLStyleValue(rawValue: 0),
                                            6.1: MGLStyleValue(rawValue: 1)
        ], options: nil
    )

    mapView.style?.addLayer(symbols)
}

Best Answer

Mapbox Studio and Mapbox Maps SDK for iOS v4.0 support expressions, which are much more flexible and powerful than style functions. The first step is to convert your existing textOpacity style function to an expression. Based on this migration guide, the expression would look like this:

NSExpression(format: "mgl_step:from:stops:($zoomLevel, 0, %@)", 
             [6: 0, 6.1: 1])

(If you want a smooth fade between these zoom levels, you can use the mgl_interpolate:withCurveType:parameters:stops: function instead of the mgl_step:from:stops: function.)

The snippet above associates each zoom level with an integer literal for convenience, but each zoom level can be associated with a full-blown expression:

NSExpression(format: "mgl_step:from:stops:($zoomLevel, 0, %@)", 
             [6: NSExpression(forConstantValue: 0),
              6.1: NSExpression(forConstantValue: 1)])

Unfortunately, it isn’t possible to vary the stop dictionary’s keys based on each feature’s minZoom attribute. But since you know that the minZoom values only go out to one decimal place, you can programmatically generate all the possible keys, setting each one to an expression that checks whether the minZoom attribute is greater than that particular zoom level:

var stops: [Double: NSExpression] = [:]
for zoomLevel in stride(from: 0, to: 20, by: 0.1) {
    stops[zoomLevel] = NSExpression(format: "TERNARY(%@ > minZoom, 1, 0)", zoomLevel)
}
symbols.textOpacity = NSExpression(format: "mgl_step:from:stops:($zoomLevel, 0, %@)", 
                                   stops)

Depending on the layer’s minimum and maximum zoom levels, this is a lot of stops, so I’m not sure if the performance is any better than what you’re seeing with individual layers for each feature. It probably also depends on how many features your source contains. If it doesn’t contain a lot of features, you could optimize the stop dictionary by only including stops for actual minZoom values:

for zoomLevel in points.map({ $0.attribute(forKey: "minZoom") }) {
    stops[zoomLevel] = NSExpression(format: "TERNARY(%@ > minZoom, 1, 0)", zoomLevel)
}
Related Question