Working with Location Grouping (Clustering)

Posted on September 30, 2019

This is an example of enabling and disabling location grouping on the map as well as providing custom cluster tapping behavior and custom cluster images.

Start by creating a UIViewController class that conforms to the MPMapControlDelegate protocol

class ClusteringController: UIViewController, MPMapControlDelegate {

Add a GMSMapView and a MPMapControl to the class Also define a clustering enabling/disabling button and a dictionary to store the clustering images for reuse

var map: GMSMapView? = nil
var mapControl: MPMapControl? = nil
let clusteringButton = UIButton.init()
var clusteringImageDictionary = Dictionary<String, UIImage>()

Setup map so that it shows the demo venue and initialise mapControl

self.map = GMSMapView.init(frame: CGRect.zero)
self.map?.camera = .camera(withLatitude: 57.057964, longitude: 9.9504112, zoom: 20)
self.mapControl = MPMapControl.init(map: self.map!)
self.mapControl?.delegate = self

Setup a button that enables/disables the location grouping / clustering mechanism

clusteringButton.setTitle("Clustering disabled", for: .normal)
clusteringButton.setTitle("Clustering enabled", for: .selected)
clusteringButton.addTarget(self, action: #selector(toggleClustering), for: .touchUpInside)
clusteringButton.backgroundColor = UIColor.blue

Arrange the map view and the button in a stackview

let stackView = UIStackView.init(arrangedSubviews: [map!, clusteringButton])
stackView.axis = .vertical
view = stackView

Define an objective-c method toggleClustering that will receive events from your button, and toggle the clustering flag:

  • Check current state
  • Swap state
  • Make button reflect the state
@objc func toggleClustering() {
    if MPMapControl.locationClusteringEnabled {
        MPMapControl.locationClusteringEnabled = false
    } else {
        MPMapControl.locationClusteringEnabled = true
    }
    clusteringButton.isSelected = MPMapControl.locationClusteringEnabled
}

Define the delegate method didTap that will receive tap events from a cluster marker

  • Check if zoom is possible and increment map zoom
  • Return true to indicate that you handle the event and do not want default behavior to happen
func didTap(_ marker: GMSMarker, forPoiGroup locations: [MPLocation]?, moreZoomPossible: Bool) -> Bool {
    if moreZoomPossible {
        self.map?.animate(toZoom: self.map!.camera.zoom + 1)
    }
    return true
}

Define the delegate method getImageSizeForPoiGroup that provides the size of the potential cluster

  • Check if zoom is possible and increment map zoom
  • Return true to indicate that you handle the event and do not want default behavior to happen
func getImageSizeForPoiGroup(withCount count: UInt, clusterId: String) -> CGSize {
    let width = 48 * (Int(log10(Double(count))) + 1)
    let height = 48
    return CGSize.init(width: width, height: height)
}

Define the delegate method getImageForPoiGroup that asynchronously provides the image of the potential cluster

func getImageForPoiGroup(_ poiGroup: [MPLocation], imageSize: CGSize, clusterId: String, completion: @escaping (UIImage?) -> Void) -> Bool {

In getImageForPoiGroup create a string hash for the image

let imgHash = "img\(poiGroup.count)\(clusterId)"

In getImageForPoiGroup check if image already exists. If image does not exist, go in a background thread to get a dummy image and call the completion handler. Return true to indicate that you handle the clustering image.

var img = clusteringImageDictionary[imgHash]
if img == nil {
    DispatchQueue.global().async {
        let imgUrlString = "https://placem.at/people?txt=\(poiGroup.count)&random=\(Int.random(in: 0 ..< 10))&w=\(imageSize.width*2)&h=\(imageSize.height*2)"
        let imgUrl = URL(string: imgUrlString)
        do {
            let imgData = try Data.init(contentsOf: imgUrl!)
            img = UIImage(data: imgData, scale: 2)
            completion(img)
            self.clusteringImageDictionary[imgHash] = img
        } catch {
            completion(nil)
        }
    }
} else {
    completion(img)
}
return true

See the sample in ClusteringController.swift