Location Clustering

Last updated:

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

Enabling and disabling clustering is done through the SolutionConfig, in the following way:

//Enabling clustering
MapsIndoors.getSolution()?.config?.setEnableClustering(true)
//Disabling clustering
MapsIndoors.getSolution()?.config?.setEnableClustering(false)

To create custom icons for clusters you can set a MPClusterIconAdapter either on the MPMapConfig when you are creating a new instance of MapControl or you can do it on runtime by setting a MPClusterIconAdapter directly on a MapControl object. Here is an example of doing it when creating a new MapControl:

private fun initMapControl(view: View) {
val mapConfig: MPMapConfig = MPMapConfig.Builder(requireActivity(), mMap!!, getString(R.string.google_maps_key), view, true).setClusterIconAdapter { return@setClusterIconAdapter getCircularImageWithText(it.size.toString(), 15, 30, 30) }.build()
MapControl.create(mapConfig) { mapControl: MapControl?, miError: MIError? -> }
}
private fun getCircularImageWithText(text: String, textSize: Int, width: Int, height: Int): Bitmap {
val background = Paint()
background.color = Color.WHITE
// Now add the icon on the left side of the background rect
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
val radius = width shr 1
canvas.drawCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), background)
background.color = Color.BLACK
background.style = Paint.Style.STROKE
background.strokeWidth = 3f
canvas.drawCircle(radius.toFloat(), radius.toFloat(), (radius - 2).toFloat(), background)
val tp = TextPaint()
tp.textSize = textSize.toFloat()
tp.color = Color.BLACK
val bounds = Rect()
tp.getTextBounds(text, 0, text.length, bounds)
val textHeight: Int = bounds.height()
val textWidth: Int = bounds.width()
val textPosX = width - textWidth shr 1
val textPosY = height + textHeight shr 1
canvas.drawText(text, textPosX.toFloat(), textPosY.toFloat(), tp)
return result
}

Applying a ClusterIconAdapter on runtime can be done like this:

mMapControl.setClusterIconAdapter {
return@setClusterIconAdapter getCircularImageWithText(it.size.toString(), 15, 30, 30)
}

See the sample in LocationClusteringFragment.kt

  1. Developing on the new Arm-based Apple Silicon (M1) Macs requires building and running on a physical iOS device or using an iOS simulator running iOS 13.7, e.g. iPhone 11. This is a temporary limitation in Google Maps SDK for iOS, and as such also a limitation in MapsIndoors, due to the dependency to Google Maps.
  2. Note: Due to a bug in CocoaPods it is necessary to include the post_install hook in your Podfile described in the PodFile post_install wiki.

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