Navigation SDK v2 for Androidを試してみる (中身を見る編)

## はじめに
[前回](https://qiita.com/OttyLab/private/83185cca45ee9f2f406d)Navigation SDK v2 for Androidのサンプルをコピペで動かしてみました。今回はコードの中を見て、どのような処理が行われているかを追ってみます。


## 注意事項
- この記事では2021/11/25現在で最新の`v2.0.2`を使用します。
- [Pricing](https://docs.mapbox.com/android/navigation/guides/pricing/)にご注意ください。特にMAUは100まで無料ですが、[ここに記載](https://docs.mapbox.com/android/navigation/guides/pricing/#how-are-unique-users-counted)されているようにアプリケーションの削除・インストールでカウントアップします。デバッグ中に何度も削除・インストールを繰り返すと無料枠を超える可能性があります。


## 概観

サンプルで使用してる機能の概観は以下のようになります。

![qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85105/7edfd802-caab-5048-7c73-e7b291868e94.png)

|コンポーネント|説明|
|:--|---|
|`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](https://github.com/OttyLab/MapboxSamples/blob/master/navigation/android/NavigationSDKSample/app/src/main/java/com/example/navigationsdksample/TurnByTurnExperienceActivity.kt#L412)を覗いてみましょう。主に初期化処理が行われています。

### Puckの設定

`LocationPuck2D`の設定をしています。これはMaps SDKに対する設定です。

```kotlin:TurnByTurnExperienceActivity.kt
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`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/location/NavigationLocationProvider.kt)をセットします。

ちなみに、Maps SDKが実装している[LocationProviderImpl](https://github.com/mapbox/mapbox-maps-android/blob/android-v10.0.0/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationProviderImpl.kt)は直接Fused Locationから位置情報を取得します。Navigation SDKでは`LocationProviderImpl`を使用せず、独自の`NavigationLocationProvider`を用いることでマップマッチングができるようにしています。

### Navigation Coreの初期化

ここでは[`MapboxNavigation`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt)の初期化を行います。

```kotlin:TurnByTurnExperienceActivity.kt
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`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigationProvider.kt)は
`MapboxNavigation`をシングルトンとして利用するためのヘルパークラスです。`MapboxNavigation`を複数作成して使用すると思わぬバグに遭遇するので、必ず`MapboxNavigationProvider`経由で使用してください。

`NavigationOptions`で各種設定を行います。ここではトークンと`LocationEngine`の設定を行っています。サンプルでは[`ReplayLocationEngine`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/ReplayLocationEngine.kt)を使用しています。`ReplayLocationEngine`は[`MapboxReplayer`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/MapboxReplayer.kt)が再生(Play)する緯度・軽度にしたがって位置情報を更新します。シナリオに書かれたとおりに位置情報を更新していくようなイメージです。


### カメラの設定の初期化

次にカメラの設定です。

```kotlin:TurnByTurnExperienceActivity.kt
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`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/camera/data/MapboxNavigationViewportDataSource.kt)は両方のカメラのためのパラメータを計算します。[`NavigationCamera`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavui-maps/src/main/java/com/mapbox/navigation/ui/maps/camera/NavigationCamera.kt)がそのパラメータにしたがってカメラをコントロールします。

カメラコントロールはv2の目玉機能のひとつなので、ぜひ遊んでみてください。


### Banner等

方向指示等のイベントのことを"Maneuver"と呼びます。[`MapboxManeuverApi`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavui-maneuver/src/main/java/com/mapbox/navigation/ui/maneuver/api/MapboxManeuverApi.kt)を初期化しています。

```kotlin:TurnByTurnExperienceActivity.kt
480         // initialize maneuver api that feeds the data to the top banner maneuver view
481         maneuverApi = MapboxManeuverApi(
482             MapboxDistanceFormatter(distanceFormatterOptions)
483         )
```

さらに[`MapboxTripProgressApi`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavui-tripprogress/src/main/java/com/mapbox/navigation/ui/tripprogress/api/MapboxTripProgressApi.kt)を初期化しています。

```kotlin:TurnByTurnExperienceActivity.kt
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`を初期化しています。

```kotlin:TurnByTurnExperienceActivity.kt
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`の上に経路を描画するように設定しています。

```kotlin:TurnByTurnExperienceActivity.kt
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`でナビゲーションを開始しています。

```kotlin:TurnByTurnExperienceActivity.kt
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の２パターンがあり、経路を設定して開始する一般的なナビゲーションはturn-by-turnナビゲーションです。ここでは経路を設定**せず**にナビゲーションを開始するため、free-drivingナビゲーションとなります。free-drivingナビゲーションを開始する理由は、マップマッチングを開始するためです。

ここまでが初期化となります。


## 経路の探索

`onCreate`内部で、マップのロングタップに対して`findRoute`が設定されています。ロングタップした地点の座標が渡されます。

```kotlin:TurnByTurnExperienceActivity.kt
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`の処理は以下のとおりです。

```kotlin:TurnByTurnExperienceActivity.kt
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ナビゲーションを開始します。


```kotlin:TurnByTurnExperienceActivity.kt
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`で更新処理が開始します。

```kotlin:TurnByTurnExperienceActivity.kt
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`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/ReplayLocationEngine.kt#L87-L112)を見てみると、

```kotlin
    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`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/MapboxTripSession.kt#L228-L236)が実行されます。"raw location"とはGPSから得られた位置情報を示しています（サンプルではReplayで生成された経路上の位置情報です）。

```kotlin
    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`](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/MapboxTripSession.kt#L248)として取得できます。

さらに`enhancedLocation`から作成した`LocationMatcherResult`を[`LocationObserver`インターフェースを通して上位レイヤに渡します](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/ReplayLocationEngine.kt#L87-L112)。

```kotlin
    private fun updateLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
        this.locationMatcherResult = locationMatcherResult
        locationObservers.forEach { it.onNewLocationMatcherResult(locationMatcherResult) }
    }
```

ちなみに、サンプルコード内では以下の場所で`registerLocationObserver`しています。

```kotlin:TurnByTurnExperienceActivity.kt
565     override fun onStart() {
...
569         mapboxNavigation.registerRoutesObserver(routesObserver)
```

`LocationObservers`の処理は以下のようになっています。

```kotlin:TurnByTurnExperienceActivity.kt
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`の処理内容を見ると、

1) `LocationPuck2D`のパラメータの`navigationLocationProvider`に対して`changePosition`を実行します。[Maps SDKのコードが実行され](https://github.com/mapbox/mapbox-maps-android/blob/android-v10.0.0/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationComponentPluginImpl.kt#L243-L256)、Puck位置が移動します。

```kotlin:TurnByTurnExperienceActivity.kt
309             navigationLocationProvider.changePosition(
310                 location = enhancedLocation,
311                 keyPoints = locationMatcherResult.keyPoints,
312             )
```

2) `ViewportDataSource`の位置情報を更新することで、カメラを移動させます。

```kotlin:TurnByTurnExperienceActivity.kt
315             viewportDataSource.onLocationChanged(enhancedLocation)
316             viewportDataSource.evaluate()
```

## バナー等の更新

ナビゲーションの更新に必要な情報は下記のObserverで取得します。

```kotlin:TurnByTurnExperienceActivity.kt
565     override fun onStart() {
...
570         mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
...
```

このObserverはマップマッチング後に[ここ](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/MapboxTripSession.kt#L295)で呼ばれます(`enhancedLocation`と同じ場所です)。

Observerの処理内容は以下のとおりです。

```kotlin:TurnByTurnExperienceActivity.kt
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で取得します。

```kotlin:TurnByTurnExperienceActivity.kt
565     override fun onStart() {
...
572         mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
...
```

このObserverはマップマッチング後に[ここ](https://github.com/mapbox/mapbox-navigation-android/blob/v2.0.2/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/MapboxTripSession.kt#L295)で呼ばれます(`enhancedLocation`と同じ場所です)。

Observer内部で`SpeechApi`が音声を準備し、`MapboxVoiceInstructionsPlayer`を`play`します。

```kotlin:TurnByTurnExperienceActivity.kt
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         }
```

## まとめ

とても長くなりましたが、全体的なフローは以上です。次回はコードを変更しながら動きを見ていきます。