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を使って地図検索アプリを作ってみます。前回は位置情報をデータストアにインポートする流れを解説しました。今回は位置情報検索と、その結果を地図上に反映する流れを解説します。
完成版のコード
作成したデモアプリのコードはNCMBMania/kotlin-map-searchにアップロードしてあります。
Jetpack用Googleマップの追加
GoogleがJetpack compose用のGoogleマップコンポーネントを提供しています。インストールの流れはここを参考にしてください。なお、利用する際にはGoogleマップのAPIキーが必要です。
地図画面について
地図画面はメイン画面のタブで MapScreen として読み込まれています。
NavHost(navController = navController, startDestination = "Map") {
composable("Map") { MapScreen()}
composable("Import") { ImportScreen()}
}
UIについて
MapScreenは地図画面である*GoogleMap*を読み込んで、さらにマーカー消去用のフローティングアクションボタンを配置しています。
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)
)
}
}
}
マーカーは MapScreen の markers と stations にデータを追加すると、画面に反映されます。
// タップした箇所を表示するマーカー
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回目のタップしたマーカーと合わせて矩形検索を行います。
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 関数は以下のようになります。
// 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()
}
2点による矩形検索処理は以下のようになります。
// 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()
}
どちらも検索結果の駅一覧をマーカーに反映します。
// 選択されている位置情報の数に応じて検索方法を変更
val ary = if (markers.size == 1)
searchStationsNear(position) // 位置情報1つなら付近の駅を検索
else searchStationSquare(markers) // 位置情報が2つなら矩形検索
ary.forEach {
stations.add(it)
}
後はフローティンアクションボタンをタップした際の処理で、マーカーを削除します。
FloatingActionButton(
modifier = Modifier.padding(end = 50.dp, bottom = 50.dp),
onClick = {
// 選択されたマーカー、駅情報をクリアする
markers.clear()
stations.clear()
}) {
Icon(Icons.Filled.Delete, contentDescription = "削除")
}
これで地図検索アプリの完成です。
まとめ
NCMBを使えば位置情報を使った検索も簡単に実装できます。スマートフォンアプリでは位置情報をよく使いますので、ぜひNCMBの位置情報機能を役立ててください。


