Wednesday, July 27, 2016

Marker Clustering with Google's Utility library for Maps SDK (Google-Maps-iOS-Utils)

This tutorial shows how to group multiple map markers with Marker Clustering utility library provided by Google-Maps-iOS-Utils(V1.0.1), which requires Google Maps SDK for iOS (V2.0). This tutorial is created in Swift 2.2 with Xcode 7.3.1. The following screenshots show a lot of map markers and the clustering results using GMUClusterManager with default circles or custom images.




Follow these steps:

1. Install CocoaPods as the dependency manager.

Type this command in the terminal:

sudo gem install cocoapods

2. Create a Single View Application project with Xcode and close it.

3. Go to the Xcode project directory in the terminal, type pod init or nano Podfile to create a Podfile and save it as:


platform :ios, '9.0'
target "ProjectName" do
    pod 'GoogleMaps'
    pod 'Google-Maps-iOS-Utils'
end

4.  Type this terminal command:

pod install

You should see something like this in the terminal:




If your GoogleMaps is an old version, update it with this terminal command:

pod update

5. Open the projectName.xcworkspace file just automatically created. (Don't open the original .xcodeproj file)



6. Add a temporary Objective-C file to your project. You may give it any name you like, e.g. Temp.m.



Select Create Bridging Header.




7. Delete the temporary Objective-C file (Temp.m) you just created.

8. In the projectName-Bridging-Header.h file just created, add this line:

#import <Google-Maps-iOS-Utils/GMUMarkerClustering.h>

9. Get the iOS API key like AIza................... from Google Developers Console. (For more details, see Step 6 of the Using Google Maps SDK for iOS in Swift tutorial.

10. Edit the AppDelegate.swift file:
    func application(application: UIApplicationdidFinishLaunchingWithOptions launchOptions: [NSObjectAnyObject]?) -> Bool {
        
        GMSServices.provideAPIKey("AIza....") //iOS API key
        
        return true

    }

10. Modify ViewController.swift as below:


import UIKit

class ViewController: UIViewController, GMSMapViewDelegate, GMUClusterManagerDelegate {
    
    private var mapView : GMSMapView!
    private var clusterManager: GMUClusterManager!
    
    //true - marker clustering / false - map markers without clustering
    let isClustering : Bool = true
    
    //true - images / false - default icons
    let isCustom : Bool = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        mapView = GMSMapView(frame: view.frame)
        
        //Default position at Chungli (Zhongli) Railway Station, Taoyuan, Taiwan.
        mapView.camera = GMSCameraPosition.cameraWithLatitude(24.953232, longitude: 121.225353, zoom: 12.0)
        
        mapView.mapType = kGMSTypeNormal
        mapView.delegate = self
        
        view.addSubview(mapView)
        
        if isClustering {
            var iconGenerator : GMUDefaultClusterIconGenerator!
            if isCustom {
                var images : [UIImage] = []
                for imageID in 1...5 {
                    images.append(UIImage(named: "m\(imageID).png")!)
                }
                iconGenerator = GMUDefaultClusterIconGenerator(buckets: [ 10, 50, 100, 200, 500 ], backgroundImages: images)
            } else {
                iconGenerator = GMUDefaultClusterIconGenerator()
            }

            let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
            let renderer = GMUDefaultClusterRenderer(mapView: mapView, clusterIconGenerator: iconGenerator)
            
            clusterManager = GMUClusterManager(map: mapView, algorithm: algorithm, renderer: renderer)
            
            generateCoord(true)
            
            // Call cluster() after items have been added to perform the clustering and rendering on map.
            clusterManager.cluster()
            
            // Register self to listen to both GMUClusterManagerDelegate and GMSMapViewDelegate events.
            clusterManager.setDelegate(self, mapDelegate: self)
        } else {
            generateCoord(false)
        }
    }
    
    /// Point of Interest Item which implements the GMUClusterItem protocol.
    class POIItem: NSObject, GMUClusterItem {
        var position: CLLocationCoordinate2D
        var name: String!
        
        init(position: CLLocationCoordinate2D, name: String) {
            self.position = position
            self.name = name
        }
    }

    func generateCoord(isCluster: Bool) {
        
        let latitudeMin   : Double = 24.79
        let latitudeMax   : Double = 25.10
        let latitudeDiff  : Double = latitudeMax - latitudeMin
        let longitudeMin  : Double = 120.99
        let longitudeMax  : Double = 121.50
        let longitudeDiff : Double = longitudeMax - longitudeMin
        
        for count in 1...5000 {
            
            let latitude  = latitudeMin  + Double(arc4random()%10000)/10000*latitudeDiff
            let longitude = longitudeMin + Double(arc4random()%10000)/10000*longitudeDiff
        
            let position = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
            
            if isCluster {
                let item = POIItem(position: position, name: "#\(count)")
                
                clusterManager.addItem(item)
            } else {
                let marker = GMSMarker(position: position)
                marker.title = "#\(count)"
                marker.map = mapView
            }
        }

    }
    
    func clusterManager(clusterManager: GMUClusterManager, didTapCluster cluster: GMUCluster) {
        let newCamera = GMSCameraPosition.cameraWithTarget(cluster.position,
                                                           zoom: mapView.camera.zoom + 1)
        let update = GMSCameraUpdate.setCamera(newCamera)
        mapView.moveCamera(update)
    }
    
    //Show the marker title while tapping
    func mapView(mapView: GMSMapView, didTapMarker marker: GMSMarker) -> Bool {
        let item : POIItem = marker.userData as! POIItem

        marker.title = item.name
        
        mapView.selectedMarker = marker
        
        return true
    }
    
    //Optional Feature:
    //Add new markers while tapping at coordinates without markers/clusters
    func mapView(mapView: GMSMapView, didTapAtCoordinate coordinate: CLLocationCoordinate2D) {
        
        let item = POIItem(position: coordinate, name: "NEW")
        
        clusterManager.addItem(item)
        
        clusterManager.cluster()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

}

The above code is modified from Google's ViewController.swift of the SwiftDemoApp.

11. Add the file image files below to the Xcode project:

https://github.com/googlemaps/google-maps-ios-utils/tree/master/app/Resources/Images

12. Edit the Info.plist file (Required for Xcode 7 and iOS 9):

Key: LSApplicationQueriesSchemes
Type: Array

Key: Item 0
Type: String
Value: googlechromes

Key: Item 1
Type: String
Value: comgooglemaps



Without modifying Info.plist, you'll get


Pressing the Google logo on the map in the iOS simulator shows:


-canOpenURL: failed for URL: "comgooglemaps://" - error: "This app is not allowed to query for scheme comgooglemaps"


-canOpenURL: failed for URL: "googlechromes://" - error: "This app is not allowed to query for scheme

This is because the iOS simulator does not include Google Maps and Chrome apps. So check this feature with a device.


13. Run the code. You should see result like this:



14. Set isCustom as true to see the custom clustering images:

    //true - images / false - default icons
    let isCustom : Bool = true


15. Try tap at different locations on the map to add new markers:


Related Information:

Marker Clustering
Google-Maps-iOS-Utils(GitHub)
Google Maps SDK for iOS
CocoaPods Tutorial - Google Maps SDK for iOS

2 comments: