個人開発アプリで二輪のルートを返してくれるHere Routing API使ってGoogle Mapに経路を表示しました。
完成品
注意事項
- Google MapとHere APIでは地図データやアルゴリズムの違いから経路が完全に一致していない可能性があります。
Here Routing APIのリクエスト
色々なパラメータがありますが、必須パラメータは以下3つです。
-
transportationMode
:car
/truck
/pedestrian
/bicycle
/scooter
taxi
/bus
/privateBus
-
origin
: 出発地の緯度経度 -
destination
:到着地の緯度経度
経路情報を取得するためにreturn
パラメータでpolyline
を設定します。
-
return
:polyline
今回は上記に加えて、Scooter Routingを参考に二輪経路用に以下のパラメータを追加しました。
-
vechicle
-
speedCap
-> 最高速度(m/s) -
engineSizeCc
-> 排気量
-
※Google Routes APIですら二輪用ルートは日本対応してないのでHere APIの経路がどれだけ正確なのかは分からないです。
最終的なリクエスト
suspend fun searchRoute(start: LatLng, end: LatLng): List<Route> =
withContext(dispatcherProvider.io()) {
client.get("https://router.hereapi.com/v8/routes") {
parameter("apiKey", BuildConfig.HERE_API_KEY)
parameter("transportMode", "scooter")
parameter("origin", "${start.latitude},${start.longitude}")
parameter("destination", "${end.latitude},${end.longitude}")
parameter("vehicle[speedCap]", "27.78")
parameter("vehicle[engineSizeCc]", "399")
parameter("return", "polyline")
parameter("lang", "ja")
}.body<Top>().routes
}
レスポンス
{
"routes": [
{
"id": "7c71f586-38c3-4fb3-b626-e02cbf7be8c5",
"sections": [
{
"id": "87e57035-b27c-4a76-a85a-59443bf75e63",
"type": "vehicle",
"departure": {
"time": "2024-10-13T16:18:33+09:00",
"place": {
"type": "place",
"location": {
"lat": 35.6920143,
"lng": 139.7548055
},
"originalLocation": {
"lat": 35.685175,
"lng": 139.7527999
}
}
},
"arrival": {
"time": "2024-10-13T16:53:23+09:00",
"place": {
"type": "place",
"location": {
"lat": 35.8071864,
"lng": 139.6794498
},
"originalLocation": {
"lat": 35.8072291,
"lng": 139.6793604
}
}
},
"polyline": "BG8-uikCqz-xqI1M4KtP8CrP-CzK8CxK6CzJ6E1P4HzJ6EmEoL4GyS4G0SoCmJH0OnB4P_B8N_CsU_CsU9CuU_CsU_B8NtBwJNgDqJ0BqJ0BwHqB0EaiJsBsU2BkMD0XSsKlCqhBhHsKnC2OhD0QvD2QvD0OhD-TvE0I5ByI7BiI9B2QpD8GvB4GjByI3ByI5BiJjCiJhCkJ9BmJ9ByH1B2O3DkPzEqLnGqLnG6KxG6KzGqD7ByK9F0D9B8JzFqNxHmL9FwJxF8HrEwN5H0I7E0I9EyJrF0JrFiKzFiKzF8L3G6L3Gqb5NkI9DkI7DiPnHiLtFwGxG4OxF2OzF0EtBgJpCyDPuL0C0BM0HsDoI0C4Fe-IHoKpBmX1GsJlDqQvFqQtFoQvFqQvFsJlD0J7C6gB9JsQ9E0J9CqJ1CyKhDwQ3EuQ3EuQ3EuQ3EuQ3E0KhDkDdkG5BqBLuOnE8gBzJwOnE0H1CmJ5CuQ9EmJ5C0P3EsQ_E2P3EqFzB-J9CgK9CsItCsIvCyN_D6M5D6M7D2OpD0Q5D2OrDkOzCoDZ6LpC6LpC4c1F-EhB2QjD2QhDoK5B4Q7C2Q7CoK3BohB7FqNrCoNtCmL5BmL7BkMjC2Q_CkMlCmK5BuhB7F4Q_CoK5BmDR-OzCgP1C6IxB2QhD4Q_C4IxBuJtDsJrDuLlGuN5H0H7HmJnJiNlNgNjNoJpJyMxM0MzMmMxNmMvNoJtJinB1nBqJtJkI5I0M3NkI5IyG3GoFtFsJjJmN9MoN9MqJjJ0KpJqO7JsO7JsK1GgKrG0JjF2JjF8JnEiH_CgU3FoOnC4Q1CoOpC2JvByO9BmLvBmLxBwL_C-N7GuGzEuFpFuIvLoLtPuIvL0J_MqLpPsLrP0J_MoW1XiR1R8G_GgNpN6G_GyXtZ6I5IwN5O2TvUyJ_JkI3HiI3HyM1L0M1LuFjG6JhLuHnJ8L1OwHnJiGnHgD3DgIjKgIlKsL9N2GnI2GpIgBpB0G1H0G3H0BjCyLnN8M3PqJxLwE_EoJhL4J7K6J7KoHzIoH1IoV5U2K9FmHrG2NnMkHrGmL_JgNnMoCnC4JhJ2J_IoDnDgKjK4MrN8E_E8G7Gmana6G7GyI1H0NnMyIzHiHtGoMjMqMjM8B1H-LpLuL5KiN9LgN9LiLrKyKhK2DtDmG5F2GrGiIzH8KnKuM5LuM3L4IrI4IpIuJtIiL1KkL1KgGhGsClCqDnD4C1C6JnJsNzMoX9V4IhI2B1B8D3DgGxFoI_HoN9MoI_HgSxR-LxLqN7MoN5MgMxLiJjIgJhI2IjI0IlI8L9KsL_LgGtIiGvIsFlI8GlK6GjK6JtP6JrP6HnMuKnQwKpQ6HlMmGnJkGnJiHlLoKtQgHlLqFlIqFlIsJtPuJvPyFjJuJzPuGxKkFnJwGrLuGpLkL5SCB8BtD4BhD8B3DkChFABuDnJABmE3OABoCjLmB1ImC7TAA6CjayDvmBiB7KiCzUiB7KkFtxBQ1E0B9SyB7SsB_LqBhMqE1tBqG9iCoB5M2B5MwC7SyC7SwCnS6CtU6CtUwCnS8D_agDzM0FvQyD9HoGzM0DzFiCpDsH1KgMpOkMtOgMpOoJ5KoMnOoMpOoMnOoJ5K-GjIuKhMwKhMoCvDiRvWyD9EqC7C6CjDuC9CwDtDyEtD2DpCkF5C4OC4OEiBFgEVmGnB8NjDiO3EiO3EgMpEiMpEgPxFgPvFuM3EuM1EwO1FiQpGkQrGiQpGwO1FyR5G-PjGwJ3DuChBmMtEmMtEiNlFiLlEiLlEiF_BqM1EqU_HiEvBwGxC-M_E2E1B8VpIuFhCmH3CqarK-IrDoGvC0GzCub7K0ItDoCZmFnC4DtB2U5HiQhGyDrByHlDgGpCidvLmX1J2UlIwJxDuJxDgelLwLlEyIrDkQnGyIrDoI9CyJtDoQ3FyJtD4Q1FqKvDqIjD8MpE8MpE4I5CoFzB2OvE2H7CsFlC0GvC-ClB4BXqCf8CrBkEhCwG5BwCvB6DzC0S1MsLtIiMzIiMzIkJzG2MnJiEnDkI_F8H7FsG5E8E5DiI1FiI3FqE1DiT7QqHzGqHzG8DtD4H_G-FjFwH3GwH5GqM9K8CxCiNrLkNrLmLzK2arZmLzKiHjHiNlNiHjHoJ9IqJ7I2J_J-MpN2J9J0L1MmG1GwG_G0L1MkVlXsL1O2L_OuL1OiHlJiHjJuL1MyM7NsLzMqH5H4MxNoH5HwL5KuNxMyL7KqClCiI_HkNjNiI_HyLtM2M1NuZpbwLtMuM_MuM9M4ItJ0M3N0C5CiK9K2ItJkLvLkLxL8P9P6GjH6L1L4L1LsDnDoH9GsN3MqH9GiK7J8MrM8MpMkL3KqN7MkL5KoJ5IqN3MoJ5IqF3J6JrJqN3MqN3M6JrJiJ7IkJ9I6L_KwNxMuNxM6L_KsH_GwGpG2LxIiEvC4CxB8CtBiLlFyf3OiLlFiHXgJjE6EnC8PnH6PnH6NpGkFlCqYrJ4NpF6NpFwLpDyLnD8II0FgBkEY2QiD2J6B0OyCyOwC6NsCwL8B4Q4C4Q6CcE6P0CwL8BwDUgQ-CgQ-CyBK4NqC6HqB-FgB6GY4NwB4NyBsMoB8LmB8Q2B6Q4B8LmBmJtD-MqB-MqBoNkBoNkB6OW2HEgKV-JV0OhCwQvDwQvDuP9D6FvBuNtDyQlEihBrIyQnEyQlEyQlEihBtIyQlEihBtIyQlEwNtD6O7DwQpEsflI0PxD6MzC4MzC-MlC0O3B0O3BqGLqMNsMLiNL8QPiNL-LX8LVoIbia3DsJ3B4QlD2QjDsJ5BuPhDsPhDqGe-M5C0QzD8M3CoGvBiQxD0Q3DiQxD8CRhFhlBnB3K5BnN7CtU5BnNzBzTHjOEzOkC9N6CvLgF1PuCvG8E1L8EzLxOpKzM_I",
"transport": {
"mode": "scooter"
}
}
]
}
]
}
Polylineのデコード
上記で返却されたPolylineは経路の緯度経度ペアの配列です。そのまま返すと要素数が多いため、エンコードされて返ってくるのでflexible-polylineを使ってデコードします。
fun Section.toPolyline(): List<LatLng> {
return PolylineEncoderDecoder.decode(polyline).map { LatLng(it.lat, it.lng) }
}
経路の表示
上記でデコードした配列をMap Compose
のPolylineのpoints
に渡せば単純な経路は表示されます。
Polyline(
color = Color.Red,
points = routeLineState.routeLine,
)
経路のアニメーション
どうやるか悩んだのですが、最終的にPolylineを2個重ねて、上のPolylineをアニメーションさせる形にしました。
-
animatedProgress
: 0f-1fのアニメーション進捗オフセット -
animatedRouteLine
:animatedProgress
に応じてアニメーションさせるpointの配列を更新 -
fadeAlpha
: 赤いラインがパッと白に切り替わらないようにアニメーション終了時にフェードさせる
@Composable
fun PreviewRoute(routeLine: List<LatLng>) {
val infiniteTransition = rememberInfiniteTransition(label = "") // 繰り返しアニメーション
val animatedProgress by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 4000
0f at 0
0f at 1000 // 開始
1f at 3000 // 2秒かけて線を描画
1f at 4000 // 1秒間停止
},
repeatMode = RepeatMode.Restart
),
label = "progress"
) // アニメーション進捗オフセット
val fadeAlpha by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 4000
1f at 0
1f at 3800
0f at 4000
},
repeatMode = RepeatMode.Restart
),
label = "fade"
) // animationProgressが0fに戻る時にフェードで切り替わるようにする
val animatedRouteLine by remember(routeLine, animatedProgress) {
mutableStateOf(routeLine.take((routeLine.size * animatedProgress).toInt()))
} // アニメーションさせるポイント
Polyline(
zIndex = 2f,
color = Color.White,
points = routeLine,
width = 5f
)
Polyline(
zIndex = 3f,
color = Color.Red.copy(alpha = fadeAlpha),
points = animatedRouteLine,
width = 6f
)
}
以上です!
補足
Polyline
のspan
を使えばグラデーションのアニメーションとかも細かく制御できます。
@Composable
fun RouteLine(routeLineState: PreviewRouteLineState) {
val startColor = Color.Red
val endColor = Color.Blue
val segments = 100 //適宜調整
val infiniteTransition = rememberInfiniteTransition(label = "")
val animationOffset by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
), label = ""
)
val spans by remember(animationOffset) {
derivedStateOf {
buildList {
for (i in 0 until segments) {
// 各セグメントの位置を0-1の範囲に正規化し、animationOffsetで周期的に変化させる
val fraction = (i.toFloat() / segments + animationOffset) % 1f
// abs(fraction * 2 - 1)で0->1->0のパターンを作り、
// startColorとendColorの間を周期的に補間する
val color = lerp(startColor, endColor, abs(fraction * 2 - 1))
add(StyleSpan(color.toArgb(), routeLineState.routeLine.size.toDouble() / segments))
}
}
}
}
Polyline(
spans = spans,
points = routeLineState.routeLine,
width = 6f
)
}