[Maps SDK for Android] 地図にMarkerを置く（その2・Utility Library編）

[前に書いた記事](https://qiita.com/outerlet/items/358088ca702721e1f3c4)で、Maps SDK for Android で表示した地図に配置する Marker について書きました。基本機能だけでも色々できますが、Google Maps Android API Utility Library を導入すると更にできることが広がります。
ここでは Utility Library の導入と使用法についてまとめてみました。

1. 基本機能だけでできること（前の記事）
2. Utility Library を使ってできること（この記事）

# この記事でやること
* Utility Library の導入
* Marker をクラスタにまとめる
* クラスタ化した Marker をカスタマイズする
* Icon Generator を使って地図に地名を表示する

# 注意事項
Android Studio で Maps SDK for Android が使えることを前提にしています。そのための手順は[以前まとめました](https://qiita.com/outerlet/items/0d74061fa5d625a33dfb)ので、そちらご確認下さい。

# 環境
このページに書かれているコード等は以下の環境下で動作・検証しています。

macOS Mojave バージョン10.14.6
Android Studio 3.5
Pixel3a + Android 9

また、ここに書いた内容をもとにしたサンプルコードを [GitHub](https://github.com/outerlet/MarkerImpl) で公開しています。

# Utility Library の導入
モジュールの build.gradle に以下の dependencies を追加します。

```groovy
dependencies {
    compile 'com.google.maps.android:android-maps-utils:0.5+'
}
```

# Marker をクラスタにまとめる
地図のズームレベルが下がって複数の Marker が一箇所に集まったとき、「クラスタ」にまとまるようにすることができます。
下の図だと、左が全ての Marker がバラバラに表示されている状態、右がクラスタになって表示されている状態です。

<img width="170" alt="util_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/14098be1-ac3d-577b-7993-5a68aff51d08.png"> <img width="170" alt="util_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/66509bf7-b63f-ffcf-f901-9904f37c8bda.png">

以下の手順で実装することができます。

1. ClusterItem を実装したクラスを作成
2. ClusterManager オブジェクトを生成
3. ClusterItem を ClusterManager に追加

### ClusterItem を実装したクラスを作成
Marker ひとつひとつに相当するクラスです。 com.google.maps.android.clustering.ClusterItem を実装して、必要なメソッドを定義すればOKです。
getSnippet / getTitle / getPosition では、それぞれ Marker の snippet / titie / position に相当するものを返すようにします。以下は実装例です。

```kotlin
class SegmentClusterItem(val segment: Segment) : ClusterItem {
    override fun getSnippet(): String = segment.flowerName
    override fun getTitle(): String = segment.title
    override fun getPosition(): LatLng = segment.coordinate
}
```

### ClusterManager オブジェクトを生成
com.google.maps.android.clustering.ClusterManager クラスをインスタンス化します。
このとき GoogleMap の以下のメソッドに ClusterManager インスタンスを設定するようにします。

* setOnCameraIdleListener
* setOnMarkerClickListener

以下は ClusterManager をインスタンス化しているところです。

```kotlin
val manager = ClusterManager<SegmentClusterItem>(this@ClusteringActivity, this).apply {
    setOnCameraIdleListener(this)
    setOnMarkerClickListener(this)
}
```

### ClusterItem を ClusterManager に追加
あとは ClusterManager#addItem で ClusterManager に ClusterItem を追加していくだけです。これで Marker がある程度まで接近するとクラスタにまとまるようになります。

```kotlin
Segment.values().forEach {
    manager.addItem(SegmentClusterItem(it))
}
```

### クラスタにならない場合
上記の実装をしても Marker がクラスタにまとまらない場合は、Marker の数が少ないことが考えられます。
**Marker がクラスタにまとまるかどうかは、一定の領域内に含まれる Marker(=ClusterItem)の数で判定されます。**デフォルトでは、5個以上の Marker が領域内に含まれないとクラスタになってくれません。これを変更するには、クラスタにする Marker の数を定義した ClusterRenderer オブジェクトを ClusterManager#setRenderer にセットする必要があります。

以下は、DefaultClusterRenderer を継承した ClusterRenderer クラスの実装例です。shouldRenderAsCluster で「何個以上の Marker が集まったらクラスタにするか」を判定します。この例では、2つ以上でクラスタになるようにしています。

```kotlin
class SegmentClusterRenderer(context: Context, map: GoogleMap, manager: ClusterManager<SegmentClusterItem>) : DefaultClusterRenderer<SegmentClusterItem>(context, map, manager) {
    override fun shouldRenderAsCluster(cluster: Cluster<SegmentClusterItem>?): Boolean {
        // ClusterItemが一定距離内にいくつ集まったらクラスタ化するかをBooleanで返す
        return cluster?.size ?: 0 >= 2
    }
}
```

以下でこの Renderer を Manager にセットしています。

```kotlin
manager.renderer = SegmentClusterRenderer(this@ClusteringActivity, map, manager)
```

# クラスタ化した Marker をカスタマイズする
ClusterRenderer と IconGenerator を併用することで、Marker のアイコンやクラスタのアイコンを変更することができます。
下の図は、大阪市の各区役所に Marker を立てて、区の花をアイコンで表示しています。クラスタになると、そこに含まれる Marker の数が女性のイラストに表示されるようにしています。
<img width="200" alt="util_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/c8af3f05-5e75-ba4f-c32c-36ee8f1e85d8.png"> <img width="200" alt="util_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/6ffe70a0-21de-351a-3f8e-0b5bc0da5d5d.png">

ClusterRenderer で継承すべきメソッドは以下の通りです。
両方とも実装すべき処理は同じで、Marker やクラスタが描画されるときの MarkerOptions を変更することになります。

|メソッド|実装内容|
|:--|:--|
|onBeforeClusterItemRendered|Marker(=ClusterItem) のカスタマイズ|
|onBeforeClusterRendered|クラスタのカスタマイズ|

また com.google.maps.android.ui.IconGenerator を使えば任意のレイアウトで Bitmap を生成することが可能です。生成した Bitmap を、BitmapDescriptorFactory#fromBitmap を使って MarkerOptions#icon にセットすることで、Marker やクラスタのアイコンを任意に変更しています。
以下は DefaultClusterRenderer を継承した ClusterRenderer の例で、 res/layout/icon_segment.xml のレイアウトを使って Marker にアイコンを設定しています。icon_segment.xml には imageIcon というIDが付与された ImageView だけが配置されています。

```kotlin
private val itemImageView: ImageView
private val itemIconGenerator: IconGenerator = IconGenerator(context).apply {
    val iconView = LayoutInflater.from(context).inflate(R.layout.icon_segment, null, false).apply {
        itemImageView = findViewById(R.id.imageIcon)
    }
    setContentView(iconView)
}

override fun onBeforeClusterItemRendered(item: SegmentClusterItem, markerOptions: MarkerOptions) {
    itemImageView.setImageResource(item.segment.imageResId)
    val icon = itemIconGenerator.makeIcon()
    markerOptions.icon(BitmapDescriptorFactory.fromBitmap(icon))
}
```

ざっくり以下のような手順です。

1. レイアウトから View をインフレート
2. インフレートした View から、操作したい View を findViewById で取得
3. IconGenerator#setContentView で、1の View をセット
4. 2に任意の文字や画像をセット
5. IconGenerator#makeIcon で Bitmap を生成
6. MarkerOptions#icon に生成した Bitmap をセット

# Icon Generator を使って地図に地名を表示する
地図上に地名などを表示したいという要件もあると思いますが、Marker に title や snippet をセットするだけでは Info Window のコンテンツとしてしか表示できません。
なのでここでは IconGenerator を使って、マーカーとして地名を強引に表示してみました。
<img width="212" alt="util_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/323244e9-3678-ec7f-c6e7-bcd239467545.png">

IconGenerator で文字画像を作成するのは、前項と同じやり方で可能です。TextView を含んだレイアウト（もしくは TextView そのもの）を IconGenerator のコンテンツとして設定し、任意の文字列を TextView にセットしてから IconGenerator#makeIcon するという手順になります。
これで Bitmap ができますので、あとは Marker を生成して icon にセットするということになります。

```kotlin
val textView: TextView
val iconGenerator = IconGenerator(this@CustomIconActivity).apply {
    setBackground(null)    // nullにしないとデフォルトの背景が表示されてしまう(文字だけにならない)
    textView = layoutInflater.inflate(R.layout.icon_station_name, null, false) as TextView
    setContentView(textView)
}

textView.text = station.title

addMarker(MarkerOptions()
    .icon(BitmapDescriptorFactory.fromBitmap(iconGenerator.makeIcon()))
    .position(station.position)
    .anchor(0.5f, 0.0f)
    .zIndex(2.1f))
```

# まとめ

以上、Utility Library を使った Marker のカスタマイズについてまとめてみました。
Utility Library には KML を扱ったりヒートマップを追加したりと、他にも多彩な機能が用意されています。
「参考」に公式ドキュメントへのリンクを貼っておきましたので、実際に読んで色々触ってみるのをお勧めします。この記事がそのきっかけになれば、大変嬉しく思います。

# 参考

[Google Maps Android API Utility Library](https://developers.google.com/maps/documentation/android-sdk/utility/)
[googlemaps/android-maps-utils (GitHub)](https://github.com/googlemaps/android-maps-utils)