Google Mapsで大量発生したマーカーをクラスター化できるようになった！- Marker Clustering -

# TL; DR
Google Maps SDK for iOSに、[Marker Clusteringが新登場](http://googlegeodevelopers.blogspot.jp/2016/07/announcing-marker-clustering-in-google.html)したので試してみました！
Marker Clusteringでは、ズームインするとMarkerがクラスターから分離し、ズームアウトするとMarkerがクラスター化されます。これにより、**大量のMarkerで地図が埋め尽くされ、見づらくなってしまう問題が解決されます。** 

<img src="https://media.giphy.com/media/3o7TKGZecssMDsr2ZG/giphy.gif">

# 環境
* Xcode Version 8.0 beta
* iPhone 6s iOS 9.3.3
* Swift 3.0
* Cocoapods 1.0.1
* Google Maps SDK 1.13.2

# GMSをCocoapodsでインストール
Cocoapods経由で[Google Maps SDK for iOS](https://developers.google.com/maps/documentation/ios-sdk/)と、[Google Maps SDK for iOS Utility Library](https://developers.google.com/maps/documentation/ios-sdk/utility/)をインストールします。

```ruby:Podfile
platform :ios, '9.0'

target プロジェクト名 do
   pod 'GoogleMaps'
   pod 'Google-Maps-iOS-Utils'
end

```

# Swiftプロジェクトで使う準備
GMSは、Objective-Cで書かれています。
SwiftプロジェクトではBridging-Headerを作成し、GMUMarkerClusteringをインポートします。

```GoogleMapsTrial-Bridging-Header.h
#import <Google-Maps-iOS-Utils/GMUMarkerClustering.h>
```

AppDelegateでGoogle Maps API Keyの設定を忘れないようにもします。

```Swift:AppDelegate.swift
import UIKit
import GoogleMaps

// Google Maps API Key
let APIKey = "YOUR_API_KEY"

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        
        GMSServices.provideAPIKey(APIKey)
        
        let mapViewController = UINavigationController.init(rootViewController: MapViewController())
        
        self.window = UIWindow(frame: UIScreen.main().bounds)
        self.window?.rootViewController = mapViewController
        self.window?.makeKeyAndVisible()
        
        return true
    }
}
```

# GMUClusterItemプロトコルに適合したItemを作成
Mapに表示するMarkerItemを作成します。

```Swift:MarkerItem.swift
class MarkerItem: NSObject, GMUClusterItem {
    var position: CLLocationCoordinate2D //必須
    
    init(position: CLLocationCoordinate2D) {
        self.position = position
    }
}
```

Mapを表示するViewControllerを作成

```Swift:MapViewController.swift

import UIKit
import GoogleMaps


// 新宿フロントタワー
let cameraLatitude = 35.695978
let cameraLongitude = 139.689340

class MapViewController: UIViewController, GMUClusterManagerDelegate, GMSMapViewDelegate {
    private let camera = GMSCameraPosition.camera(withLatitude: cameraLatitude,
                                                  longitude: cameraLongitude, zoom: 15)
    private let mapView = GMSMapView.init(frame: CGRect.zero)
    
    private lazy var clusterManager: GMUClusterManager = {
        let iconGenerator = GMUDefaultClusterIconGenerator()
        let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
        let renderer = GMUDefaultClusterRenderer(mapView: self.mapView, clusterIconGenerator: iconGenerator)
        return GMUClusterManager(map: self.mapView, algorithm: algorithm, renderer: renderer)
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.mapView.camera = camera
        self.view = self.mapView
        
        // Marker × 1000 ランダム座標を生成し、ClusterManagerにadd
        for _ in 0...1000 {
            let extent = 0.1
            let latitude = cameraLatitude + extent * randomScale()
            let longitude = cameraLongitude + extent * randomScale()
            clusterManager.add(MarkerItem.init(position: CLLocationCoordinate2DMake(latitude, longitude)))
        }
        
        // MarkerItemをClusteringし、地図にプロット
        clusterManager.cluster()
        
        // GMUClusterManagerDelegate + GMSMapViewDelegateを設定
        clusterManager.setDelegate(self, mapDelegate: self)
    }
    
    // MARK: - GMUMapViewDelegate
    
    // Marker or Cluster Markerがタップされた
    func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
        if marker.userData is MarkerItem {
            debugPrint("ClusterのMarkerItemがタップされた")
        } else {
            debugPrint("通常のMarkerがタップされた")
        }
        return false
    }
    
    // MARK: - GMUClusterManagerDelegate
    
    // Clusterがタップされたたら、Camera Positionを移動
    func clusterManager(_ clusterManager: GMUClusterManager, didTap cluster: GMUCluster) {
        let newCamera = GMSCameraPosition.camera(withTarget: cluster.position,
                                                 zoom: mapView.camera.zoom + 1)
        let update = GMSCameraUpdate.setCamera(newCamera)
        mapView.moveCamera(update)
    }
    
    
    private func randomScale() -> Double {
        return Double(arc4random()) / Double(UINT32_MAX) * 2.0 - 1.0
    }

}

```

# 利用可能なクラスタリングアルゴリズム
まだドキュメントが見当たらないので、Jump to Definitionで飛んでいったところ、2種類のクラスタリングアルゴリズムを確認。
以下の説明文は意訳です。なお、冒頭のgifは```GMUNonHierarchicalDistanceBasedAlgorithm```を使用しています。

```Swift
// マップをグリッド状に分割
GMUGridBasedClusterAlgorithm

/*
非階層的クラスタリング
1. 追加順にitemsをイテレート（クラスター候補）
2. itemの中心点を持つクラスターを作成
3. 特定の距離圏内に含まれるすべてのitemをクラスターに追加
4. 別のクラスターに近ければ、既存クラスターから追加済みitemを移動させる
5. それらをクラスター候補から削除
各クラスターは最初のitemの中心点を持つ（複数itemの重心ではなく）
*/
GMUNonHierarchicalDistanceBasedAlgorithm
```

# 参考
以下のドキュメント、ソースコードを参考にしました。

* [googlemaps/google-maps-ios-utils](https://github.com/googlemaps/google-maps-ios-utils)
* [Marker Clustering | Google Maps SDK for iOS | Google Developers](https://developers.google.com/maps/documentation/ios-sdk/utility/marker-clustering?utm_source=geodevsite&utm_medium=website&utm_campaign=2016-geo-na-website-gmedia-blogs-us-blogPost&utm_content=iOSmarkers)