3

My application stores data, which includes coordinates and other info, in a local database. Due to the number of data points, the application uses clustering to display the data with Mapbox on iOS. Some of the marker styling is based on data, which can change on the run. Map setup is:

// fetch data from DB
let dataArray: [MyData] = fetchData()

// build features from data array
var features = [MGLPointFeature]()
dataArray.forEach({ (data) in
   let feature = MGLPointFeature()
   feature.identifier = data.id
   feature.coordinate = CLLocationCoordinate2D(latitude: data.lat, longitude: data.lng)
   // our attributes
   feature.attributes = [
      "amount": data.amount
      "marked": false
   ]
   features.append(feature)
})

// make and add source
let source = MGLShapeSource(identifier: "MySourceId", features: features, options: [
   .clustered: true
])
style.addSource(source)

// regular marker layer
let layer = MGLSymbolStyleLayer(identifier: "unclustered", source: source)
layer.iconImageName = NSExpression(forConstantValue: "MyIcon")
layer.text = NSExpression(forKeyPath: "amount")
layer.iconScale = NSExpression(forMGLConditional: NSPredicate(format: "%@ == true", NSExpression(forKeyPath: "marked")), trueExpression: NSExpression(forConstantValue: 2.0), falseExpression: NSExpression(forConstantValue: 1.0))
layer.predicate = NSPredicate(format: "cluster != YES")
style.addLayer(layer)

// point_count layers
...

The above code is simplified to help illustrate the concept more clearly. Array of MGLPoint is used because data is stored in DB, so we don't have a GeoJSON file or a URL. MGLShapeSource is used because clustering is required, and that's what I found in the examples. MGLShapeSource constructor taking "features" as a parameter is used because that's the one that matches the data I have. The regular marker layer is setup to show different size icons based on the value of "marked" attribute in iconScale.

During runtime, the value of "marked" attribute can change (for example, when a marker is tapped), and the corresponding icon's size needs to be updated to reflect the change in "marker" value. However, I am unable to figure out how to change the marker attribute value. MGPShapeSource only shows access to shape and URL, neither of which is the features array I initialized the source with. I need to access the features array the source was constructed with, change the marker value, and have the marker icons updated.

I have thought about remaking the source on each data change. But with the number of markers involved, this would perform poorly. Plus I believe I would also need to remake all of the style layers, as they're constructed with the actual source object, which would make this perform even worse.

I need help figuring out how to change the attribute value of MGLPointFeature within MGLShapeSource during runtime and have the map updated.

Will
  • 81
  • 1
  • 6

1 Answers1

5

I did not find a solution close to what I was hoping for, but I did find something that's better than remaking everything every time.

On feature attribute value change, instead of remaking everything, including the source and all the layers, I update the source's shape with the MGLPointFeature array. I do have to remake the MGLPointFeature array, but then I can make a MGLShapeCollectionFeature with this array and set it as existing source's shape. This way, the existing layers don't need to be changed, either. Something like:

// build features from data array
...same as in original question, but with updated feature.attributes values as needed.

// look for existing source and update it if found; otherwise, make a new source
if let existingSource = style.source(withIdentifier: "MySourceId") {
   // update data
   guard let shapeSource = existingSource as? MGLShapeSource else {
      throw <some_error>
   }
   shapeSource.shape = MGLShapeCollectionFeature(shapes: features)
} else {
   // make new (same as original post)
   let source = MGLShapeSource(identifier: "MySourceId", features: features, options: [.clustered: true])
   style.addSource(source)
}
Will
  • 81
  • 1
  • 6