How to do clustering with Google Maps in iOS

Issue #191

Basic with Google Maps

Add to Podfile pod 'GoogleMaps'

Add a custom marker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import GoogleMaps

final class StopMarker: GMSMarker {
let stop: Stop

init(stop: Stop) {
self.stop = stop
super.init()
self.title = stop.name
self.position = stop.toCoordinate()

let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
imageView.layer.cornerRadius = 15
imageView.image = UIImage(named: "pin")
self.iconView = imageView
}
}

Show markers

1
2
3
4
5
6
7
8
9
func handle(stops: [Stop]) {
self.stops = stops
stops
.map({ StopMarker(stop: $0) })
.forEach {
$0.map = mapView
}

}

Assigning $0.map = mapView means telling GMSMapView to start rendering markers

Handle tap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension ViewController: GMSMapViewDelegate {
func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
zoomIn(coordinate: marker.position)
return false
}

func mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker) {
guard let stopMarker = marker as? StopMarker else {
return
}

let detailViewController = StopDetailViewController(stop: stopMarker.stop)
presentPanModal(detailViewController)
}
}

Clustering

Add google-maps-ios-utils manually by following https://github.com/googlemaps/google-maps-ios-utils/blob/master/Swift.md

Otherwise, with CocoaPods, we get error

1
[!] The 'Pods-MyApp' target has transitive dependencies that include static binaries: (/Users/khoa/Projects/MyApp/Pods/GoogleMaps/Base/Frameworks/GoogleMapsBase.framework, /Users/khoa/Projects/MyApp/Pods/GoogleMaps/Maps/Frameworks/GoogleMaps.framework, and /Users/khoa/Projects/MyApp/Pods/GoogleMaps/Maps/Frameworks/GoogleMapsCore.framework)

Add ClusterItem

1
2
3
4
5
6
7
8
9
10
11
12
import Foundation
import CoreLocation

class ClusterItem: NSObject, GMUClusterItem {
let position: CLLocationCoordinate2D
let stop: Stop

init(stop: Stop) {
self.stop = stop
self.position = stop.toCoordinate()
}
}

Set up cluster manager

1
2
3
4
5
6
let iconGenerator = GMUDefaultClusterIconGenerator()
let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
let renderer = GMUDefaultClusterRenderer(mapView: mapView, clusterIconGenerator: iconGenerator)
renderer.delegate = self
clusterManager = GMUClusterManager(map: mapView, algorithm: algorithm, renderer: renderer)
clusterManager.setDelegate(self, mapDelegate: self)

Default algorithm GMUNonHierarchicalDistanceBasedAlgorithm

1
2
3
4
5
6
7
8
9
A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not
* hierarchical.
* High level algorithm:
* 1. Iterate over items in the order they were added (candidate clusters).
* 2. Create a cluster with the center of the item.
* 3. Add all items that are within a certain distance to the cluster.
* 4. Move any items out of an existing cluster if they are closer to another cluster.
* 5. Remove those items from the list of candidate clusters.
* Clusters have the center of the first element (not the centroid of the items within it).

Make ClusterItem and add to clusterManager. In the end, call clusterManager.cluster()

1
2
3
4
5
6
7
8
9
10
11
func handle(stops: [Stop]) {
self.stops = stops
stops
.map({ StopMarker(stop: $0) })
.forEach {
let item = ClusterItem(stop: $0.stop)
clusterManager.add(item)
}

clusterManager.cluster()
}

By default, clusterManager uses renderer to render default pin marker. Implement GMUClusterRendererDelegate to use our custom StopMarker

1
2
3
4
5
6
7
8
9
10
extension ViewController: GMUClusterRendererDelegate {
func renderer(_ renderer: GMUClusterRenderer, markerFor object: Any) -> GMSMarker? {
switch object {
case let clusterItem as ClusterItem:
return StopMarker(stop: clusterItem.stop)
default:
return nil
}
}
}

Finally handle tapping on cluster and cluster item

1
2
3
4
5
6
7
8
9
10
11
extension ViewController: GMUClusterManagerDelegate {
func clusterManager(_ clusterManager: GMUClusterManager, didTap cluster: GMUCluster) -> Bool {
print("tap cluster")
return false
}

func clusterManager(_ clusterManager: GMUClusterManager, didTap clusterItem: GMUClusterItem) -> Bool {
print("tap cluster item")
return false
}
}

Comments