NCMBのKotlin SDKを使って地図検索アプリを作る（その3：位置情報検索を実装する）

NCMBでは公式SDKとしてSwift/Objective-C/Kotlin/Java/Unity/JavaScript SDKを用意しています。また、それ以外にもコミュニティSDKとして、非公式ながらFlutter/React Native/Google Apps Script/C#/Ruby/Python/PHPなど幅広い言語向けにSDKが開発されています。

今回は公式SDKの一つ、Kotlin SDKを使って地図検索アプリを作ってみます。[前回は位置情報をデータストアにインポートする流れを解説](https://qiita.com/goofmint/items/a5f0a30e8cea57b34903)しました。今回は位置情報検索と、その結果を地図上に反映する流れを解説します。

## 完成版のコード

作成したデモアプリのコードは[NCMBMania/kotlin-map-search](https://github.com/NCMBMania/kotlin-map-search)にアップロードしてあります。

## Jetpack用Googleマップの追加

GoogleがJetpack compose用のGoogleマップコンポーネントを提供しています。[インストールの流れはここを参考にしてください](https://developers.google.com/maps/documentation/android-sdk/maps-compose?hl=ja)。なお、利用する際にはGoogleマップのAPIキーが必要です。

## 地図画面について

地図画面はメイン画面のタブで MapScreen として読み込まれています。

```kotlin
NavHost(navController = navController, startDestination = "Map") {
    composable("Map") { MapScreen()}
    composable("Import") { ImportScreen()}
}
```

## UIについて

MapScreenは地図画面である*`GoogleMap`*を読み込んで、さらにマーカー消去用のフローティングアクションボタンを配置しています。

```kotlin
val context = LocalContext.current
// タップした位置情報が入る
var markers = remember { mutableStateListOf<LatLng>() }
// 検索条件にマッチする駅一覧が入る
var stations = remember { mutableStateListOf<NCMBObject>() }
// 画面について
Scaffold(
    // フローティングアイコンボタン
    floatingActionButton = {
        FloatingActionButton(
            modifier = Modifier.padding(end = 50.dp, bottom = 50.dp),
            onClick = {
                // 選択されたマーカー、駅情報をクリアする
                markers.clear()
                stations.clear()
            }) {
            Icon(Icons.Filled.Delete, contentDescription = "削除")
        }
    }
) { padding ->
    // 初期表示の位置情報（東京タワー）
    val tokyo = LatLng(35.6585805, 139.7454329)
    // 初期表示のズーム設定
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(tokyo, 13f)
    }
    // Googleマップのコンポーネント
    GoogleMap(
        modifier = Modifier
            .padding(padding)
            .fillMaxSize(),
        cameraPositionState = cameraPositionState,
        onMapClick = { position ->
            // 駅情報はクリア
            stations.clear()
            if (markers.size == 0) {
                // なければ追加するだけ
                markers.add(position)
            } else {
                // 選択された位置情報を最初に追加
                markers.add(0, position)
            }
            // 位置情報が2つ以上なら、最後のは不要
            if (markers.size > 2) {
                markers.removeAt(2)
            }
            // 選択されている位置情報の数に応じて検索方法を変更
            val ary = if (markers.size == 1)
                searchStationsNear(position) // 位置情報1つなら付近の駅を検索
                else searchStationSquare(markers) // 位置情報が2つなら矩形検索
            ary.forEach {
                stations.add(it)
            }
        }
    ) {
        // タップした箇所を表示するマーカー
        for(marker in markers) {
            Marker(
                state = MarkerState(position = marker),
                title = "marker",
            )
        }

        // 駅を青いマーカーで表示
        for (station in stations) {
            val geo = station.getGeo("geo")
            Marker(
                // 位置情報を取り出す
                state = MarkerState(position = LatLng(geo.mlatitude, geo.mlongitude)),
                title = "${station.getString("name")}駅",
                icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)
            )
        }
    }
}
```

![Screenshot_20230317_171003のコピー2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f900fff3-e795-9a78-8261-2e55c6a11103.png)


マーカーは `MapScreen` の `markers` と `stations` にデータを追加すると、画面に反映されます。

```kotlin
// タップした箇所を表示するマーカー
for(marker in markers) {
    Marker(
        state = MarkerState(position = marker),
        title = "marker",
    )
}

// 駅を青いマーカーで表示
for (station in stations) {
    val geo = station.getGeo("geo")
    Marker(
        // 位置情報を取り出す
        state = MarkerState(position = LatLng(geo.mlatitude, geo.mlongitude)),
        title = "${station.getString("name")}駅",
        icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)
    )
}
```

## タップした際の処理

地図をタップした際の処理です。 onMapClick が実行されます。この時、1回目のタップだった時にはその付近の駅を、2回目以降のタップだった場合には1回目のタップしたマーカーと合わせて矩形検索を行います。

```kotlin
onMapClick = { position ->
  // 駅情報はクリア
  stations.clear()
  if (markers.size == 0) {
      // なければ追加するだけ
      markers.add(position)
  } else {
      // 選択された位置情報を最初に追加
      markers.add(0, position)
  }
  // 位置情報が2つ以上なら、最後のは不要
  if (markers.size > 2) {
      markers.removeAt(2)
  }
  // 選択されている位置情報の数に応じて検索方法を変更
  val ary = if (markers.size == 1)
      searchStationsNear(position) // 位置情報1つなら付近の駅を検索
      else searchStationSquare(markers) // 位置情報が2つなら矩形検索
  ary.forEach {
      stations.add(it)
  }
}
```

付近の駅を検索する `searchStationsNear` 関数は以下のようになります。

```kotlin
// 1つのマーカーの付近（3.0km）を検索する関数
fun searchStationsNear(position: LatLng): List<NCMBObject> {
    // 検索するNCMBのデータストアクラス
    val query = NCMBQuery.forObject("Station")
    // 位置情報をNCMBGeoPointに変換
    val geo = NCMBGeoPoint(latitude = position.latitude, longitude = position.longitude)
    query.whereNearSphereKilometers("geo", geo, 3.0)
    return query.find()
}
```

![Screenshot_20230317_171037.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/8da246c5-d509-a2e0-4855-6c166d5e73cc.png)


2点による矩形検索処理は以下のようになります。

```kotlin
// 2つのマーカーに挟まれた駅を検索する関数
fun searchStationSquare(positions: List<LatLng>): List<NCMBObject> {
    // 検索するNCMBのデータストアクラス
    val query = NCMBQuery.forObject("Station")
    // 位置情報をNCMBGeoPointに変換
    val geo1 = NCMBGeoPoint(latitude = positions[0].latitude, longitude = positions[0].longitude)
    val geo2 = NCMBGeoPoint(latitude = positions[1].latitude, longitude = positions[1].longitude)
    // 検索条件に指定
    query.whereWithinGeoBox("geo", geo1, geo2)
    // 検索実行
    return query.find()
}
```

![Screenshot_20230317_171052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/b3fd0def-221e-f25e-4ee0-c309c3dcb9ad.png)


どちらも検索結果の駅一覧をマーカーに反映します。

```kotlin
// 選択されている位置情報の数に応じて検索方法を変更
val ary = if (markers.size == 1)
    searchStationsNear(position) // 位置情報1つなら付近の駅を検索
    else searchStationSquare(markers) // 位置情報が2つなら矩形検索
ary.forEach {
    stations.add(it)
}
```

後はフローティンアクションボタンをタップした際の処理で、マーカーを削除します。

```kotlin
FloatingActionButton(
    modifier = Modifier.padding(end = 50.dp, bottom = 50.dp),
    onClick = {
        // 選択されたマーカー、駅情報をクリアする
        markers.clear()
        stations.clear()
    }) {
    Icon(Icons.Filled.Delete, contentDescription = "削除")
}
```

これで地図検索アプリの完成です。

## まとめ

NCMBを使えば位置情報を使った検索も簡単に実装できます。スマートフォンアプリでは位置情報をよく使いますので、ぜひNCMBの位置情報機能を役立ててください。

[mBaaSでサーバー開発不要！ | ニフクラ mobile backend](https://mbaas.nifcloud.com/)