はじめに
前回Navigation SDK v2 for Androidのサンプルをコピペで動かしてみました。今回はコードの中を見て、どのような処理が行われているかを追ってみます。
注意事項
- この記事では2021/11/25現在で最新の
v2.0.2
を使用します。 - Pricingにご注意ください。特にMAUは100まで無料ですが、ここに記載されているようにアプリケーションの削除・インストールでカウントアップします。デバッグ中に何度も削除・インストールを繰り返すと無料枠を超える可能性があります。
概観
サンプルで使用してる機能の概観は以下のようになります。
コンポーネント | 説明 |
---|---|
LocationEngine |
位置情報のソースとなる実装のインターフェース |
MapboxNavigation |
Navigation Coreの中心モジュール。Navigation Nativeへのインターフェース |
Navigation Native | マップマッチングや進捗管理を行う。C++で実装され、iOSと共通 |
ViewportDataSource |
マップマッチング済みの位置情報からカメラコントロールに必要なパラメータ(center/zoom/bearing/pitch)を計算 |
NavigationCamera |
実際にカメラをコントロール。アニメーションも担当 |
LocationProvider |
Maps SDKで定義されるインターフェース。Maps SDKのLocationComponent で使用 |
LocationPuck2D |
Maps SDKで実装される自車位置アイコン。LocationProvider で指定された位置にアイコンを表示 |
ManeuverApi |
次の方向指示を取得 |
MapboxManeuverView |
次の方向指示を表示するView |
TripProgressApi |
進捗状況を取得 |
MapboxTripProgressView |
進捗状況を表示 |
SpeechApi |
音声案内を生成(音声ファイル) |
VoiceInstructionPlayer |
音声を再生 |
一番左のLocationEngine
がフローの起点となります。LocationEngine
は位置情報を提供します。通常、デバイスのGPS(SDKのデフォルトではFused Location)がソースとなりますが、LocationEngine
を実装することで外部接続のGPSを使用することもできます。サンプルではReplayLocationEngine
を用いて経路に沿った位置情報を順次生成しています。
LocationEngine
は位置情報をMapboxNavigation
経由でNavigation Nativeに渡します。Navigation Nativeは得られた位置情報を元にマップマッチングを行います。また、設定された経路を参照し、現在の進捗も管理します。
マップマッチング済みの位置情報や進捗情報がNavigation NativeからMapboxNavigation
に渡され、各種機能に提供されます。
onCreate
まず、エントリポイントであるonCreateを覗いてみましょう。主に初期化処理が行われています。
Puckの設定
LocationPuck2D
の設定をしています。これはMaps SDKに対する設定です。
418 // initialize the location puck
419 binding.mapView.location.apply {
420 this.locationPuck = LocationPuck2D(
421 bearingImage = ContextCompat.getDrawable(
422 this@TurnByTurnExperienceActivity,
423 R.drawable.mapbox_navigation_puck_icon
424 )
425 )
426 setLocationProvider(navigationLocationProvider)
427 enabled = true
428 }
LocationPuck2D
はデフォルトで青いドットを表示しますが、ここではR.drawable.mapbox_navigation_puck_icon
をbearingImage
に設定することでカーナビっぽいアイコンを表示しています。
setLocationProvider
にはNavigation SDKで実装しているNavigationLocationProvider
をセットします。
ちなみに、Maps SDKが実装しているLocationProviderImplは直接Fused Locationから位置情報を取得します。Navigation SDKではLocationProviderImpl
を使用せず、独自のNavigationLocationProvider
を用いることでマップマッチングができるようにしています。
Navigation Coreの初期化
ここではMapboxNavigation
の初期化を行います。
430 // initialize Mapbox Navigation
431 mapboxNavigation = if (MapboxNavigationProvider.isCreated()) {
432 MapboxNavigationProvider.retrieve()
433 } else {
434 MapboxNavigationProvider.create(
435 NavigationOptions.Builder(this.applicationContext)
436 .accessToken(getString(R.string.mapbox_access_token))
437 // comment out the location engine setting block to disable simulation
438 .locationEngine(replayLocationEngine)
439 .build()
440 )
441 }
MapboxNavigationProvider
は
MapboxNavigation
をシングルトンとして利用するためのヘルパークラスです。MapboxNavigation
を複数作成して使用すると思わぬバグに遭遇するので、必ずMapboxNavigationProvider
経由で使用してください。
NavigationOptions
で各種設定を行います。ここではトークンとLocationEngine
の設定を行っています。サンプルではReplayLocationEngine
を使用しています。ReplayLocationEngine
はMapboxReplayer
が再生(Play)する緯度・軽度にしたがって位置情報を更新します。シナリオに書かれたとおりに位置情報を更新していくようなイメージです。
カメラの設定の初期化
次にカメラの設定です。
443 // initialize Navigation Camera
444 viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap)
445 navigationCamera = NavigationCamera(
446 mapboxMap,
447 binding.mapView.camera,
448 viewportDataSource
449 )
カメラは視点の移動に利用されます。カメラにはOverviewとFollowingの二種類があります。Overviewは上空からまっすぐ見下ろすカメラです。FollowingはPuckの後方上空から見下ろすようなカメラです。MapboxNavigationViewportDataSource
は両方のカメラのためのパラメータを計算します。NavigationCamera
がそのパラメータにしたがってカメラをコントロールします。
カメラコントロールはv2の目玉機能のひとつなので、ぜひ遊んでみてください。
Banner等
方向指示等のイベントのことを"Maneuver"と呼びます。MapboxManeuverApi
を初期化しています。
480 // initialize maneuver api that feeds the data to the top banner maneuver view
481 maneuverApi = MapboxManeuverApi(
482 MapboxDistanceFormatter(distanceFormatterOptions)
483 )
さらにMapboxTripProgressApi
を初期化しています。
485 // initialize bottom progress view
486 tripProgressApi = MapboxTripProgressApi(
487 TripProgressUpdateFormatter.Builder(this)
488 .distanceRemainingFormatter(
489 DistanceRemainingFormatter(distanceFormatterOptions)
490 )
491 .timeRemainingFormatter(
492 TimeRemainingFormatter(this)
493 )
494 .percentRouteTraveledFormatter(
495 PercentDistanceTraveledFormatter()
496 )
497 .estimatedTimeToArrivalFormatter(
498 EstimatedTimeToArrivalFormatter(this, TimeFormat.NONE_SPECIFIED)
499 )
500 .build()
501 )
音声案内の初期化
MapboxSpeechApi
およびMapboxVoiceInstructionsPlayer
を初期化しています。
503 // initialize voice instructions api and the voice instruction player
504 speechApi = MapboxSpeechApi(
505 this,
506 getString(R.string.mapbox_access_token),
507 Locale.US.language
508 )
509 voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer(
510 this,
511 getString(R.string.mapbox_access_token),
512 Locale.US.language
513 )
経路描画の初期化
経路を描画するための初期化処理です。ここではroad-label
の上に経路を描画するように設定しています。
515 // initialize route line, the withRouteLineBelowLayerId is specified to place
516 // the route line below road labels layer on the map
517 // the value of this option will depend on the style that you are using
518 // and under which layer the route line should be placed on the map layers stack
519 val mapboxRouteLineOptions = MapboxRouteLineOptions.Builder(this)
520 .withRouteLineBelowLayerId("road-label")
521 .build()
522 routeLineApi = MapboxRouteLineApi(mapboxRouteLineOptions)
523 routeLineView = MapboxRouteLineView(mapboxRouteLineOptions)
524
525 // initialize maneuver arrow view to draw arrows on the map
526 val routeArrowOptions = RouteArrowOptions.Builder(this).build()
527 routeArrowView = MapboxRouteArrowView(routeArrowOptions)
ナビゲーションの開始
実はonCreate
でナビゲーションを開始しています。
560 // start the trip session to being receiving location updates in free drive
561 // and later when a route is set also receiving route progress updates
562 mapboxNavigation.startTripSession()
ナビゲーションにはfree-drivingとturn-by-turnの2パターンがあり、経路を設定して開始する一般的なナビゲーションはturn-by-turnナビゲーションです。ここでは経路を設定せずにナビゲーションを開始するため、free-drivingナビゲーションとなります。free-drivingナビゲーションを開始する理由は、マップマッチングを開始するためです。
ここまでが初期化となります。
経路の探索
onCreate
内部で、マップのロングタップに対してfindRoute
が設定されています。ロングタップした地点の座標が渡されます。
533 // add long click listener that search for a route to the clicked destination
534 binding.mapView.gestures.addOnMapLongClickListener { point ->
535 findRoute(point)
536 true
537 }
findRoute
の処理は以下のとおりです。
609 private fun findRoute(destination: Point) {
...
620 mapboxNavigation.requestRoutes(
621 RouteOptions.builder()
622 .applyDefaultNavigationOptions()
623 .applyLanguageAndVoiceUnitOptions(this)
624 .coordinatesList(listOf(originPoint, destination))
625 // provide the bearing for the origin of the request to ensure
626 // that the returned route faces in the direction of the current user movement
627 .bearingsList(
628 listOf(
629 Bearing.builder()
630 .angle(originLocation.bearing.toDouble())
631 .degrees(45.0)
632 .build(),
633 null
634 )
635 )
636 .build(),
637 object : RouterCallback {
638 override fun onRoutesReady(
639 routes: List<DirectionsRoute>,
640 routerOrigin: RouterOrigin
641 ) {
642 setRouteAndStartNavigation(routes)
643 }
...
MapboxNavigation#requestRoutes
が経路探索を行います。経路探索に関する設定はRouteOptions
です。MapboxNavigation
はオンライン時にはDirections APIで、オフライン時にはNavigation Native内部で経路探索を行います。経路情報が得られるとsetRouteAndStartNavigation
でturn-by-turnナビゲーションを開始します。
659 private fun setRouteAndStartNavigation(routes: List<DirectionsRoute>) {
660 // set routes, where the first route in the list is the primary route that
661 // will be used for active guidance
662 mapboxNavigation.setRoutes(routes)
...
MapboxNavigation#setRoutes
で経路を設定することでfree-drivingからturn-by-turnに切り替わります。
位置情報の更新
このサンプルアプリでは設定された経路に沿って位置情報が更新されます(Replay)。具体的には以下のstartSimulation
で更新処理が開始します。
659 private fun setRouteAndStartNavigation(routes: List<DirectionsRoute>) {
...
664 // start location simulation along the primary route
665 startSimulation(routes.first())
...
674 }
...
90 private fun startSimulation(route: DirectionsRoute) {
691 mapboxReplayer.run {
...
697 play()
698 }
699 }
ReplayLocationEngine
を見てみると、
override fun replayEvents(replayEvents: List<ReplayEventBase>) {
replayEvents.forEach { event ->
when (event) {
is ReplayEventUpdateLocation -> replayLocation(event)
}
}
}
...
private fun replayLocation(event: ReplayEventUpdateLocation) {
...
registeredCallbacks.forEach { it.onSuccess(locationEngineResult) }
...
}
MapboxReplayer#play
が実行されると定期的にReplayLocationEngine#replayEvents
を呼び出し、registeredCallbacks
として登録されているMapboxTripSession#updateRawLocation
が実行されます。"raw location"とはGPSから得られた位置情報を示しています(サンプルではReplayで生成された経路上の位置情報です)。
private fun updateRawLocation(rawLocation: Location) {
if (state != TripSessionState.STARTED) return
this.rawLocation = rawLocation
locationObservers.forEach { it.onNewRawLocation(rawLocation) }
mainJobController.scope.launch {
navigator.updateLocation(rawLocation.toFixLocation())
}
}
ここで大事なのがnavigator.updateLocation(rawLocation.toFixLocation())
です。navigator
は内部的にはNavigation Nativeを指しており、updateLocation
にraw locationを渡すことでマップマッチングが行われます。結果はコールバック内でenhancedLocation
として取得できます。
さらにenhancedLocation
から作成したLocationMatcherResult
をLocationObserver
インターフェースを通して上位レイヤに渡します。
private fun updateLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
this.locationMatcherResult = locationMatcherResult
locationObservers.forEach { it.onNewLocationMatcherResult(locationMatcherResult) }
}
ちなみに、サンプルコード内では以下の場所でregisterLocationObserver
しています。
565 override fun onStart() {
...
569 mapboxNavigation.registerRoutesObserver(routesObserver)
LocationObservers
の処理は以下のようになっています。
299 private val locationObserver = object : LocationObserver {
300 var firstLocationUpdateReceived = false
301
302 override fun onNewRawLocation(rawLocation: Location) {
303 // not handled
304 }
305
306 override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
307 val enhancedLocation = locationMatcherResult.enhancedLocation
308 // update location puck's position on the map
309 navigationLocationProvider.changePosition(
310 location = enhancedLocation,
311 keyPoints = locationMatcherResult.keyPoints,
312 )
313
314 // update camera position to account for new location
315 viewportDataSource.onLocationChanged(enhancedLocation)
316 viewportDataSource.evaluate()
317
318 // if this is the first location update the activity has received,
319 // it's best to immediately move the camera to the current user location
320 if (!firstLocationUpdateReceived) {
321 firstLocationUpdateReceived = true
322 navigationCamera.requestNavigationCameraToOverview(
323 stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
324 .maxDuration(0) // instant transition
325 .build()
326 )
327 }
328 }
329 }
onNewLocationMatcherResult
の処理内容を見ると、
-
LocationPuck2D
のパラメータのnavigationLocationProvider
に対してchangePosition
を実行します。Maps SDKのコードが実行され、Puck位置が移動します。
309 navigationLocationProvider.changePosition(
310 location = enhancedLocation,
311 keyPoints = locationMatcherResult.keyPoints,
312 )
-
ViewportDataSource
の位置情報を更新することで、カメラを移動させます。
315 viewportDataSource.onLocationChanged(enhancedLocation)
316 viewportDataSource.evaluate()
バナー等の更新
ナビゲーションの更新に必要な情報は下記のObserverで取得します。
565 override fun onStart() {
...
570 mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
...
このObserverはマップマッチング後にここで呼ばれます(enhancedLocation
と同じ場所です)。
Observerの処理内容は以下のとおりです。
331 /**
332 * Gets notified with progress along the currently active route.
333 */
334 private val routeProgressObserver = RouteProgressObserver { routeProgress ->
335 // update the camera position to account for the progressed fragment of the route
336 viewportDataSource.onRouteProgressChanged(routeProgress)
337 viewportDataSource.evaluate()
338
339 // draw the upcoming maneuver arrow on the map
340 val style = mapboxMap.getStyle()
341 if (style != null) {
342 val maneuverArrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
343 routeArrowView.renderManeuverUpdate(style, maneuverArrowResult)
344 }
345
346 // update top banner with maneuver instructions
347 val maneuvers = maneuverApi.getManeuvers(routeProgress)
348 maneuvers.fold(
349 { error ->
350 Toast.makeText(
351 this@TurnByTurnExperienceActivity,
352 error.errorMessage,
353 Toast.LENGTH_SHORT
354 ).show()
355 },
356 {
357 binding.maneuverView.visibility = View.VISIBLE
358 binding.maneuverView.renderManeuvers(maneuvers)
359 }
360 )
361
362 // update bottom trip progress summary
363 binding.tripProgressView.render(
364 tripProgressApi.getTripProgress(routeProgress)
365 )
366 }
ViewportDataSource
ここでもViewportDataSource
を更新していますが今回はonRouteProgressChanged
を実行しています。これは主にManeuver
周辺でpitchをゼロにして真上から見下ろすカメラワークに使用されます。
ManeuverApi
maneuverApi.getManeuvers
で方向指示を取り出し、binding.maneuverView.renderManeuvers(maneuvers)
でManeivuerView
を更新します。
TripProgressApi
tripProgressApi.getTripProgress(routeProgress)
で進捗情報を取り出し、TripProgressView
を更新します。
音声案内の更新
音声案内の更新に必要な情報は下記のObserverで取得します。
565 override fun onStart() {
...
572 mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
...
このObserverはマップマッチング後にここで呼ばれます(enhancedLocation
と同じ場所です)。
Observer内部でSpeechApi
が音声を準備し、MapboxVoiceInstructionsPlayer
をplay
します。
247 /**
248 * Observes when a new voice instruction should be played.
249 */
250 private val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions ->
251 speechApi.generate(voiceInstructions, speechCallback)
252 }
253
254 /**
255 * Based on whether the synthesized audio file is available, the callback plays the file
256 * or uses the fall back which is played back using the on-device Text-To-Speech engine.
257 */
258 private val speechCallback =
259 MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>> { expected ->
260 expected.fold(
261 { error ->
262 // play the instruction via fallback text-to-speech engine
263 voiceInstructionsPlayer.play(
264 error.fallback,
265 voiceInstructionsPlayerCallback
266 )
267 },
268 { value ->
269 // play the sound file from the external generator
270 voiceInstructionsPlayer.play(
271 value.announcement,
272 voiceInstructionsPlayerCallback
273 )
274 }
275 )
276 }
まとめ
とても長くなりましたが、全体的なフローは以上です。次回はコードを変更しながら動きを見ていきます。