1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

MapboxAdvent Calendar 2021

Day 2

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

Last updated at Posted at 2021-12-01

はじめに

前回Navigation SDK v2 for Androidのサンプルをコピペで動かしてみました。今回はコードの中を見て、どのような処理が行われているかを追ってみます。

注意事項

  • この記事では2021/11/25現在で最新のv2.0.2を使用します。
  • Pricingにご注意ください。特にMAUは100まで無料ですが、ここに記載されているようにアプリケーションの削除・インストールでカウントアップします。デバッグ中に何度も削除・インストールを繰り返すと無料枠を超える可能性があります。

概観

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

qiita.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を覗いてみましょう。主に初期化処理が行われています。

Puckの設定

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

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_iconbearingImageに設定することでカーナビっぽいアイコンを表示しています。

setLocationProviderにはNavigation SDKで実装しているNavigationLocationProviderをセットします。

ちなみに、Maps SDKが実装しているLocationProviderImplは直接Fused Locationから位置情報を取得します。Navigation SDKではLocationProviderImplを使用せず、独自のNavigationLocationProviderを用いることでマップマッチングができるようにしています。

Navigation Coreの初期化

ここではMapboxNavigationの初期化を行います。

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
MapboxNavigationをシングルトンとして利用するためのヘルパークラスです。MapboxNavigationを複数作成して使用すると思わぬバグに遭遇するので、必ずMapboxNavigationProvider経由で使用してください。

NavigationOptionsで各種設定を行います。ここではトークンとLocationEngineの設定を行っています。サンプルではReplayLocationEngineを使用しています。ReplayLocationEngineMapboxReplayerが再生(Play)する緯度・軽度にしたがって位置情報を更新します。シナリオに書かれたとおりに位置情報を更新していくようなイメージです。

カメラの設定の初期化

次にカメラの設定です。

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は両方のカメラのためのパラメータを計算します。NavigationCameraがそのパラメータにしたがってカメラをコントロールします。

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

Banner等

方向指示等のイベントのことを"Maneuver"と呼びます。MapboxManeuverApiを初期化しています。

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

さらにMapboxTripProgressApiを初期化しています。

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を初期化しています。

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

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

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

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

経路の探索

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

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の処理は以下のとおりです。

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ナビゲーションを開始します。

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で更新処理が開始します。

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を見てみると、

    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から作成したLocationMatcherResultLocationObserverインターフェースを通して上位レイヤに渡します

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

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

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

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

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のコードが実行され、Puck位置が移動します。
TurnByTurnExperienceActivity.kt
309             navigationLocationProvider.changePosition(
310                 location = enhancedLocation,
311                 keyPoints = locationMatcherResult.keyPoints,
312             )
  1. ViewportDataSourceの位置情報を更新することで、カメラを移動させます。
TurnByTurnExperienceActivity.kt
315             viewportDataSource.onLocationChanged(enhancedLocation)
316             viewportDataSource.evaluate()

バナー等の更新

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

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

このObserverはマップマッチング後にここで呼ばれます(enhancedLocationと同じ場所です)。

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

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で取得します。

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

このObserverはマップマッチング後にここで呼ばれます(enhancedLocationと同じ場所です)。

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

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         }

まとめ

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?