Skip to content

2.0 Navigation SDK Migration Guide

Łukasz Paczos edited this page May 11, 2021 · 44 revisions

Read me first (1): Navigation SDK v2 upgrades to Mapbox Maps SDK v10. Maps SDK v10 offers 3D maps, improved performance, and Metal support (on iOS). It's also a SEMVER major release with breaking API changes. Please make sure you read the Maps SDK migration guide before reading the navigation-specific content below.

Read me first (2): This is a migration guide that doesn't intend to cover general Navigation SDK usage. For higher-level tutorials and code snippets, please check Navigation SDK's public documentation. A examples app is also available in this repository.


Artifact name changes

The Navigation SDK is now available under one Maven artifact - com.mapbox.navigation:android:{version}.

However, in addition to the one grouping artifact there are also multiple granular ones that you can pick-and-choose if you’re not planning to use all of the features and the binary size of your application is a consideration:

  • com.mapbox.navigation:core offers all the core localization features from pre-v2.
  • com.mapbox.navigation:ui-maps offers the route line APIs, navigation camera, and other tools and features that integrate with the Mapbox Maps SDK.
  • com.mapbox.navigation:ui-maneuver offers the maneuver view and its APIs that replace the pre-v2 InstructionView.
  • com.mapbox.navigation:ui-tripprogress offers the trip progress view and APIs that replace the pre-v2 SummaryBottomSheet.
  • com.mapbox.navigation:ui-voice offers all necessary APIs to play voice instructions.
  • com.mapbox.navigation:ui-speedlimit offers a view and APIs to display speed limits.

Kotlin:

The Navigation SDK v2 is written 100% in Kotlin. This is a change from how the pre-v2 versions were a mix of Java and Kotlin.

About the v2 Navigation Core Components:

Core localization components in the Navigation SDK v2 remain architecturally mostly unchanged but introduce a couple of breaking changes that improve the experience or clarify the behavior of the SDK to streamline the integration.

Requesting a route

Changed MapboxNavigation#requestRoutes to not automatically set the result as the primary route for the navigation experience. When calling MapboxNavigation#requestRoutes make sure to also call MapboxNavigation#setRoutes with a result. This change allows for dispatching and managing multiple route requests concurrently, including canceling with MapboxNavigation#cancelRouteRequest.

NavigationOptions changes

Defaults

The NavigationOptions class now contains all the right defaults out of the box, so the MapboxNavigation#defaultNavigationOptionsBuilder has been removed.

Routing tiles configuration (pre-v2 OnboardRouterOptions)

OnbourdRouterOptions were renamed to RoutingTilesOptions to better reflect the purpose. If you are using a custom base URI, dataset, or version of the tiles, those are now separate fields in the builder rather than a single URI path.

Predictive Caching

  • When migrating please ensure you have cleaned up the old navigation tiles cache folder to reclaim disk space. Navigation SDK 2.0 caches navigation tiles in a default folder under APP_FOLDER/mbx_nav/tiles/api.mapbox.com. Previous versions of Nav SDK used to cache tiles under a default folder APP_FOLDER/Offline/api.mapbox.com/$tilesVersion/tiles. The old cache is not compatible with a new version of SDK 2.0. It makes sense to delete any folders used previously for caching including a default one.
  • OnboardRouterOptions enabled you to specify a path where nav-tiles will be saved and if a custom directory was used, it should be cleared as well.

About the v2 Navigation UI Components:

The v2 UI components now focus on separating the data transformation logic (preparing for presentation) from the actual presentation layer that updates the app’s UI elements. This separation might require additional synchronization on the app level, but scales better with non-trivial apps. This returns the control over the UI components’ lifecycle back to developers who can now better fit them into more complex setups. This is also true for UI components that interact directly with the Mapbox Map - the presentation data is prepared separately from the Map itself, and the updates can be dispatched by the developer at the correct time, depending on the Map’s placement in the view hierarchy and its lifecycle.

New UI Components

Maneuvers

Maneuvers is the v2 version of the InstructionView. Maneuvers has the following components that are responsible for updating the current maneuver instructions with banner related data.

  • ManeuverApi: This is an interface responsible for consuming RouteLeg or BannerInstruction or RouteStepProgress objects and returning a state object that should be passed to the views mentioned below to update them.
  • MapboxManeuverApi: This is an implementation of the interface mentioned above.
  • MapboxPrimaryManeuver: This is an android TextView that renders primary banner instructions.
  • MapboxSecondaryManeuver: This is an android TextView that renders secondary banner instructions.
  • MapboxSubManeuver: This is an android TextView that renders sub banner instructions.
  • MapboxLaneGuidance: This is an android ImageView that renders the upcoming lanes for banner instructions.
  • MapboxTurnIconManeuver: This is an android ImageView that renders the turn icons for current banner instructions.
  • MapboxStepDistance: This is an android TextView that can be used to render the step distance remaining and total step distance for current banner instruction.
  • MapboxManeuverView: This is a UI implementation that combines all of the views above to draw a comprehensive turn by tun instruction based on current and upcoming instructions.
  • MapboxUpcomingManeuverAdapter: - Analogous to pre v2 InstructionList, this adapter renders all the instructions for a given RouteLeg.

Add the MapboxManeuverView component to your Activity or Fragment layout

<com.mapbox.navigation.ui.maneuver.view.MapboxManeuverView
      android:id="@+id/maneuverView"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_margin="4dp"
      android:visibility="gone"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"/>

To instantiate the api you need to do the following in your Activity or Fragment

val maneuverApi: ManeuverApi = MapboxManeuverApi(
    MapboxDistanceFormatter
      .Builder(applicationContext)
      .unitType(VoiceUnit.UNDEFINED)
      .roundingIncrement(Rounding.INCREMENT_FIFTY)
      .build()
)

To retrieve and render the current maneuver instruction, do the following:

private val currentManeuverCallback = object : ManeuverCallback {
    override fun onManeuver(currentManeuver: ManeuverState.CurrentManeuver) {
        // render primary instruction
        maneuverView.render(ManeuverState.ManeuverPrimary.Instruction(currentManeuver.primary))
        // render secondary instruction
        maneuverView.render(ManeuverState.ManeuverSecondary.Instruction(currentManeuver.secondary))
        // render sub instruction
        maneuverView.render(ManeuverState.ManeuverSub.Instruction(currentManeuver.sub))
        // add upcoming lanes for lane guidance
        maneuverView.render(ManeuverState.LaneGuidanceManeuver.AddLanes(currentManeuver.lane))
        // remove upcoming lanes for lane guidance
        maneuverView.render(ManeuverState.LaneGuidanceManeuver.RemoveLanes)
    }
}

private val bannerInstructionObserver = object : BannerInstructionsObserver {
    override fun onNewBannerInstructions(bannerInstructions: BannerInstructions) {
        maneuverApi.retrieveManeuver(bannerInstructions, currentManeuverCallback)
    }
}

Register a BannerInstructionObserver with MapboxNavigation:

override fun onCreate(savedInstanceState: Bundle?) {
    mapboxNavigation = MapboxNavigation(
        applicationContext,
        mapboxNavigationOptions,
        locationEngine = getLocationEngine()
    )
    mapboxNavigation.registerBannerInstructionObserver(bannerInstructionObserver)
}

Don't forget to unregister the observer in onStop or onDestroy:

override fun onDestroy() {
    super.onDestroy()
    mapboxNavigation.unregisterBannerInstructionObserver(bannerInstructionObserver) 
}

To retrieve and render the step distance remaining do the following:

private val stepDistanceRemainingCallback = object : StepDistanceRemainingCallback {
    override fun onStepDistanceRemaining(distanceRemaining: ManeuverState.DistanceRemainingToFinishStep) {
        maneuverView.render(distanceRemaining)
    }
}

private val routeProgressObserver = object : RouteProgressObserver {
    override fun onRouteProgressChanged(routeProgress: RouteProgress) {
        ifNonNull(routeProgress.currentLegProgress) { legProgress ->
            ifNonNull(legProgress.currentStepProgress) {
                maneuverApi.retrieveStepDistanceRemaining(it, stepDistanceRemainingCallback)
            }
        }
    }
}

Register a RouteProgressObserver with MapboxNavigation:

override fun onCreate(savedInstanceState: Bundle?) {
    mapboxNavigation = MapboxNavigation(
        applicationContext,
        mapboxNavigationOptions,
        locationEngine = getLocationEngine()
    )
    mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
}

Don't forget to unregister the observer in onStop or onDestroy:

override fun onDestroy() {
    super.onDestroy()
    mapboxNavigation?.unregisterRouteProgressObserver(routeProgressObserver) 
}

To retrieve and render the upcoming maneuvers do the following:

private val upcomingManeuverCallback = object : UpcomingManeuversCallback {
    override fun onUpcomingManeuvers(state: ManeuverState.UpcomingManeuvers.Upcoming) {
        maneuverView.render(state)
    }
}

private val routesReqCallback = object : RoutesRequestCallback {
    override fun onRoutesReady(routes: List<DirectionsRoute>) {
        routes[0].legs()?.let { legs ->
            if (legs.isNotEmpty()) {
                maneuverApi.retrieveUpcomingManeuvers(legs[0], upcomingManeuverCallback)
            }
        }
    }
    override fun onRoutesRequestFailure(throwable: Throwable, routeOptions: RouteOptions) {}
    override fun onRoutesRequestCanceled(routeOptions: RouteOptions) {}
}

Trip Progress

The Navigation SDK v2 replaces the SummaryBottomSheet with a trip progress component. The component is made up of a MapboxTripProgressView which is an Android View that can be added to an Activity or Fragment layout. The MapboxTripProgressView is responsible for updating the view components with trip related data. The MapboxTripProgressApi is responsible for consuming RouteProgress objects and returning a state object that should be passed to the MapboxTripProgressView to update the view.

Add the view component to your Activity or Fragment layout:

<com.mapbox.navigation.ui.tripprogress.view.MapboxTripProgressView
    android:id="@+id/tripProgressView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

Inside your Activity or Fragment:

Create a TripProgressUpdateFormatter:

This will build a default formatter. You may provide your own custom formatters if you wish.

val formatter = TripProgressUpdateFormatter.Builder(this).build() 

Create an instance of the TripProgressApi:

val tripProgressApiApi = MapboxTripProgressApi(formatter)

Create a RouteProgressObserver:

private val routeProgressObserver = object : RouteProgressObserver {
    override fun onRouteProgressChanged(routeProgress: RouteProgress) {
        val update = tripProgressApiApi.getTripProgress(routeProgress)
        tripProgressView.render(update)
    }
}

Register a RouteProgressObserver with MapboxNavigation:

override fun onCreate(savedInstanceState: Bundle?) {
    mapboxNavigation = MapboxNavigation(
        applicationContext,
        mapboxNavigationOptions,
        locationEngine = getLocationEngine()
    )
    mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
}

Don't forget to unregister the observer in onStop or onDestroy:

override fun onDestroy() {
    super.onDestroy()
    mapboxNavigation?.unregisterRouteProgressObserver(routeProgressObserver) 
}

Route Line

The Navigation SDK v2 changes the interaction with the route line component. In pre-v2 interaction was wrapped in the NavigationMapRoute component but in v2 it is interacted with directly. The NavigationMapRoute class does not exist in version v2. Utilizing the route line component involves interacting with two classes:

  • The MapboxRouteLineApi class processes input and outputs a data structure describing the mutations that will alter the appearance of the route line on the map.
  • The MapboxRouteLineView class consumes the data structure outputted by the MapboxRouteLineApi and renders the map related mutations.

Inside your Activity or Fragment:

Create an instance of the MapboxRouteLineApi and MapboxRouteLineView.

val routeLineOptions = MapboxRouteLineOptions.Builder().build()
val routeLineApi = MapboxRouteLineApi(routeLineOptions)
val routeLineView = MapboxRouteLineView(routeLineOptions)

To draw a route(s) on the map pass one or more DirectionsRoute(s) to the MapboxRouteLineApi's setRoutes() method and pass the result of that call to the render method of MapboxRouteLineView. The MapboxRouteLineApi class will create data describing the map mutations but the mutations will not take place until the MapboxRouteLineView's render method is called with that data.

routeLineApi.setRoutes(
    listOf(RouteLine(aDirectionsRoute, null)),
    object : MapboxNavigationConsumer<Expected<RouteSetValue, RouteLineError>> {
        override fun accept(value: Expected<RouteSetValue, RouteLineError>) {
            routeLineView.renderRouteDrawData(mapboxMap.getStyle()!!, value)
        }
    }
)

It's also important to inform the MapboxRouteLineApi when there is a new route. One reason a new route could be generated is as a result of a reroute event. One way to update the route line on the map as a result of a reroute event is to register a RouteProgressObserver with MapboxNavigation and in the onRouteProgressChanged() method check for a change in the route and if detected update the MapboxRouteLineApi, rendering the result. Below is an example demonstrating this:

val routeProgressObserver: RouteProgressObserver = object : RouteProgressObserver {
    override fun onRouteProgressChanged(routeProgress: RouteProgress) {
        val currentRoute = routeProgress.route
        val hasGeometry = (currentRoute.geometry() != null && currentRoute.geometry()!!.isNotEmpty())
        val isNewRoute = (hasGeometry && currentRoute !== mapboxRouteLineApi.getPrimaryRoute())

        if (isNewRoute) {
            mapboxRouteLineApi.setRoutes(
                listOf(RouteLine(routeProgress.route, null))),
                object : MapboxNavigationConsumer<Expected<RouteSetValue, RouteLineError>> {
                    override fun accept(value: Expected<RouteSetValue, RouteLineError>) {
                        mapboxRouteLineView.renderRouteDrawData(mapboxMap.getStyle()!!, value)
                    }
                }
        }
    }
}

Also be sure to unregister the RouteProgressObserver in onStop() or onDestroy() to avoid resource leaks.

MapRouteLineInitializedCallback

The MapRouteLineInitializedCallback is not available anymore. As soon as you call MapboxRouteLineView#render, the layers are added to the map synchronously, so this is the point in time after which you can reference them for z-positioning other runtime layers. You can check RouteLineConstant for layer IDs. Also, you may call MapboxRouteLineView#initializeLayers in advance to initialize the layers manually.

Alternative route selection and padding

Alternative route selection recognition is performed by the MapRouteLineApi#findClosestRoute which also takes padding as an argument.

Vanishing Route Line

The vanishing route line feature can be enabled to change the color of the route line behind the puck during active navigation. Enable this feature in the MapboxRouteLineOptions. The color that appears behind the puck can be customized with the RouteLineColorResources::routeLineTraveledColor parameter.

val colorResources = RouteLineColorResources.Builder().routeLineTraveledColor(Color.parseColor("#ffcc00")).build()
val routeLineResources = RouteLineResources.Builder().routeLineColorResources(colorResources).build()

val options = MapboxRouteLineOptions.Builder(context)
    .withVanishingRouteLineEnabled(true)
    .withRouteLineResources(routeLineResources)
    .build()

There are two observers that need to be registered in order to use the vanishing route line feature effectively.

In the RouteProgressObserver:

val routeProgressObserver: RouteProgressObserver = object : RouteProgressObserver {
    override fun onRouteProgressChanged(routeProgress: RouteProgress) {
         mapboxRouteLineApi.updateWithRouteProgress(routeProgress)
    }
}

In the OnIndicatorPositionChangedListener:

mapView.getLocationComponentPlugin().addOnIndicatorPositionChangedListener(
    onIndicatorPositionChangedListener
)

private val onIndicatorPositionChangedListener = OnIndicatorPositionChangedListener { point ->
    routeLineApi.updateTraveledRouteLine(point).apply {
    routeLineView.renderVanishingRouteLineUpdateValue(style, this)
    }
}

Be sure to unregister the RouteProgressObserver and the OnIndicatorPositionChangedListener in onStop() or onDestroy() to avoid resource leaks.

Maneuver Arrow

In pre-v2 the maneuver arrow was also wrapped in the NavigationMapRoute class but in v2 it is interacted with directly. The maneuver arrow is updated by RouteProgress events and uses the data to determine the placement of the arrow on the route line. Utilizing the maneuver arrow component involves interacting with two classes:

  • The MapboxRouteArrowApi class processes input and outputs a data structure describing the mutations that will alter the appearance of the maneuver arrow on the map.
  • The MapboxRouteArrowView class consumes the data structure outputted by the MapboxRouteArrowApi and renders the map related mutations.

Inside your Activity or Fragment:

Create an instance of the MapboxRouteArrowApi and MapboxRouteArrowView.

val routeArrow = MapboxRouteArrowApi()
val routeArrowOptions = RouteArrowOptionsBuilder(context).build()
val routeArrowView = MapboxRouteArrowView(routeArrowOptions)

To display a maneuver arrow during navigation, register a RouteProgressObserver with MapboxNavigation and in the onRouteProgressChanged() method pass the route progress to the MapboxRouteArrowView and render the result.

val routeProgressObserver: RouteProgressObserver = object : RouteProgressObserver {
    override fun onRouteProgressChanged(routeProgress: RouteProgress) {
        val updateArrowState: UpdateManeuverArrowState = routeArrow.updateUpcomingManeuverArrow(routeProgress)
        routeArrowView.render(style, updateArrowState)
    }
}

There are also methods to hide and show the maneuver arrow according to your use cases. Also be sure to unregister the RouteProgressObserver in onStop() or onDestroy() to avoid resource leaks.

Navigation Camera

Mapbox Maps SDK v10 removes the CameraModes that were part of the LocationComponent and used to order the map camera to follow the location puck. Now, the concepts of updating the puck’s position and updating the camera’s position are completely decoupled.

With Maps SDK v10, all camera properties can be manipulated independently and in parallel, so there should be no limits as to how you can build a custom system that tracks the puck’s position in navigation scenarios. This wasn’t the case with Maps SDK v9 and earlier that was the cause for the introduction of CameraModes that aren’t needed anymore.

That said, we still want to make it easy to integrate a camera system that tracks the puck’s location and this is where NavigationCamera and the concept of the ViewportDataSource are introduced.

NavigationCamera is a class that tries to simplify management of the Map's camera object in typical navigation scenarios, maintains a state (IDLE/FOLLOWING/OVERVIEW), and executes camera transitions. NavigationCamera does not produce any camera position values, the positions that the camera should transition to are generated by the ViewportDataSource interface. Mapbox also provides a default MapboxNavigationViewportDataSource implementation that produces opinionated camera positions based on the inputted data.

The new NavigationCamera also replaces the legacy Navigation SDK implementation of DynamicCamera and SimpleCamera implementations that were wrappers on top of the Maps SDK CameraModes.

The below guide will try to walk you through a couple of LocationComponent camera usage examples and how they can be recreated or improved with the NavigationCamera and the MapboxNavigationViewportDataSource.

Initialize camera

viewportDataSource = MapboxNavigationViewportDataSource(
    MapboxNavigationViewportDataSourceOptions.Builder().build(),
    mapView.getMapboxMap()
)
navigationCamera = NavigationCamera(
    mapView.getMapboxMap(),
    mapView.getCameraAnimationsPlugin(),
    viewportDataSource
)

Starting to track the users' location

Before
locationComponent.setCameraMode(cameraMode)

This would transition to the last known location sample that the LocationComponent knew (or do nothing if not available).

Now

First, provide the MapboxNavigationViewportDataSource with location data:

viewportDataSource.onLocationChanged(location)
viewportDataSource.evaluate()

The location can be obtained from the navigation's LocationObserver or MapMatcherResultObserver if the trip session has already been started (MapboxNavigation#startTripSession), or from any LocationEngine instance. You can also fetch the initial data directly from the LocationComponentPlugin's LocationProvider.

Then, initialize the FOLLOWING state:

navigationCamera.requestNavigationCameraToFollowing()

Tracking users' location with bearing matching location

Before

Any location update pushed to the LocationComponent after setting the CameraMode would update the camera.

Now

Update the viewport data source:

viewportDataSource.onLocationChanged(location)
viewportDataSource.evaluate()

which will trigger a camera update when NavigationCamera is in a FOLLOWING state.

Tracking users' location with locked bearing

Before

Tracking the users' location while having the camera pointing to the north was a separate state:

locationComponent.setCameraMode(CameraMode.TRACKING_GPS_NORTH)
Now

To detach the camera's bearing from the location, use the property overrides:

viewportDataSource.followingBearingPropertyOverride(0.0)
viewportDataSource.evaluate()

When the override is set, the MapboxNavigationViewportDataSource will not change the value of the overridden property. This means that now you can not only lock the camera's bearing to the north while tracking but also to any other value.

To reset back to location bearing matching, clear the override:

viewportDataSource.followingBearingPropertyOverride(null)
viewportDataSource.evaluate()

Changing the zoom level, pitch, or padding

Zoom level, pitch, and padding manipulation operations have the same before and now changes, only the property name changes. Let's take zoom as an example.

Before

The zoom level could be changed via the locationComponent.zoomWhileTracking tracking method. The method only worked if CameraMode was already set to one of the tracking modes, otherwise, the request was lost.

The zoom level could also be changed by overriding the com.mapbox.navigation.ui.camera.Camera#zoom method and recomputing the value that Navigation SDK requested.

Now

Now, the zoom level control can either be left to the MapboxNavigationViewportDataSource which will have a constant zoom level when in a free-drive mode (when there is no route present) and a dynamic zoom level when we're in an active guidance session, or overridden and controlled manually.

To let the default viewport data source manage the zoom when in an active guidance session, provide an active route via the:

viewportDataSource.onRouteChanged(route)

often used with the RoutesObserver:

private val routesObserver = object : RoutesObserver {
    override fun onRoutesChanged(routes: List<DirectionsRoute>) {
        if (routes.isNotEmpty()) {
            viewportDataSource.onRouteChanged(routes.first())
            viewportDataSource.evaluate()
        } else {
            viewportDataSource.clearRouteData()
            viewportDataSource.evaluate()
        }
    }
}

and provide the RouteProgress via:

viewportDataSource.onRouteProgressChanged(routeProgress)

often used with the RouteProgressObserver:

private val routeProgressObserver = object : RouteProgressObserver {
    override fun onRouteProgressChanged(routeProgress: RouteProgress) {
        viewportDataSource.onRouteProgressChanged(routeProgress)
        viewportDataSource.evaluate()
    }
}

When a route and progress are available, the viewport data source will always try to produce camera positions that best frame the upcoming maneuvers together with the user's location from viewportDataSource.onLocationChanged(location).

But you can also always manually control the zoom level by overriding the zoom property:

viewportDataSource.followingZoomPropertyOverride(16.5)
viewportDataSource.evaluate()
Additional route state notes

Remember to call viewportDataSource.clearRouteData() whenever the active route is cleared to prevent the default viewport data source from trying to frame the route's geometry that's not visible anymore.

Additional padding notes

The default MapboxNavigationViewportDataSource does not apply any padding. You can apply padding by overriding the property:

viewportDataSource.followingPaddingPropertyOverride(padding)
viewportDataSource.evaluate()

Camera transition collisions and navigation gestures management

Both the legacy CameraModes and the new NavigationCamera assume full ownership of the camera object while not idle. This means that by design, any outside transition (like MapboxMap#easeTo and others) would compete with the internal transitions of the navigation camera features.

Before

Whenever any other transition was scheduled CameraMode would fallback to NONE but you could use the LocationComponentOptions#trackingGesturesManagement to prevent gestures like double-tap to zoom in, two-tap to zoom out, or quick sale from breaking the tracking state.

Now

By default, the NavigationCamera only falls back to the IDLE state when another transition interrupts the navigation camera's scheduled transition. However, in practice, developers should use the CameraAnimationsLifecycleListener to avoid competing transitions.

Mapbox provides 2 default implementations of that interface:

  • NavigationBasicGesturesHandler which requests the NavigationCamera to IDLE whenever any outside transition or gesture interaction happens.
  • NavigationScaleGestureHandler which requests the NavigationCamera to IDLE whenever any outside transition happens but allows for executing navigation gestures described above, same as LocationComponentOptions#trackingGesturesManagement did in the past.

You can enable those implementations by registering the lifecycle listeners:

mapView.getCameraAnimationsPlugin().addCameraAnimationsLifecycleListener(
    NavigationScaleGestureHandler(
        context,
        navigationCamera,
        mapView.getMapboxMap(),
        mapView.getGesturesPlugin(),
        mapView.getLocationComponentPlugin(),
        object : NavigationScaleGestureActionListener {
            override fun onNavigationScaleGestureAction() {
                viewportDataSource.followingZoomUpdatesAllowed = false
            }
        }
    ).apply { initialize() }
)
Note about managing zoom level updates after gesture interaction

When an allowed scale gesture interaction happens and the zoom level changes, the navigation camera would try to reset that zoom level back to the opinionated value computed via the default viewport data source provider as soon as another MapboxNavigationViewportDataSource#evaluate call is made. To prevent that, notice the:

object : NavigationScaleGestureActionListener {
    override fun onNavigationScaleGestureAction() {
        viewportDataSource.followingZoomUpdatesAllowed = false
    }
}

above. This prevents the viewport data source from producing any zoom values after the gesture interaction happens. Reset the followingZoomUpdatesAllowed value whenever appropriate to restore the dynamic (or overridden) zoom level updates.

Route overview

Before

The CameraModes did not offer any means of showing the overview of the route in the context of the position of the puck.

The legacy Navigation SDK APIs did have a concept of an overview, but it also did not take into account the current user's location.

Now

NavigationCamera offers an OVERVIEW state available via navigationCamera.requestNavigationCameraToOverview(). When paired with the MapboxNavigationViewportDataSource, the overview state will always frame the whole route, or if the RouteProgress is also being provided it will frame the remainder of the route. Here's how an overview with padding can be requested:

private val routesObserver = object : RoutesObserver {
    override fun onRoutesChanged(routes: List<DirectionsRoute>) {
        if (routes.isNotEmpty()) {
            viewportDataSource.onRouteChanged(routes.first())
            viewportDataSource.overviewPaddingPropertyOverride(overviewEdgeInsets)
            viewportDataSource.evaluate()
            navigationCamera.requestNavigationCameraToOverview()
        } else {
            viewportDataSource.clearRouteData()
        }
    }
}

Then, on each RouteProgress update, the camera position will be recalculated to only frame the remainder of the route:

private val routeProgressObserver = object : RouteProgressObserver {
    override fun onRouteProgressChanged(routeProgress: RouteProgress) {
        viewportDataSource.onRouteProgressChanged(routeProgress)
        viewportDataSource.evaluate()
    }
}

Framing other points

Before

CameraModes did not allow for framing any arbitrary points in the camera's frame. We had to rely on the location and zoom level to manipulate what's visible on the map.

Now

The MapboxNavigationViewportDataSource allows for passing an arbitrary list of points and the API will make sure that all of those points are visible in the frame together with the provided location and route (if present), as long as the zoom level property is not overridden. Additional points that you'd like to be captured in the camera's frame can be passed via:

viewportDataSource.additionalPointsToFrameForFollowing(points)
viewportDataSource.evaluate()

This can be powerful when paired with bearing manipulation, like:

val center = mapboxMap.getCameraOptions(null).center
    ?: Point.fromLngLat(0.0, 0.0)
viewportDataSource.additionalPointsToFrameForFollowing(listOf(lookAtPoint))
viewportDataSource.followingBearingPropertyOverride(
    TurfMeasurement.bearing(center, lookAtPoint)
)
viewportDataSource.evaluate()

In the active guidance, the above operation would frame the current location, the upcoming maneuver, the lookAtPoint but also changes the bearing of the camera to head towards that new point.

Mapbox Electronic Horizon

The electronic horizon API was changed. You can use both callbacks and direct calls to get the electronic horizon data. With callbacks you get only the most important data, which can be more efficient and save resources. If you need additional data (for example: edge metadata, edge shape, road object metadata, etc.) you can make a direct call.

Check docs to get more details.

Voice

Voice is the v2 version of the NavigationSpeechPlayer. Voice has the following components that are responsible for playing timely and detailed voice instructions.

  • MapboxSpeechApi: This allows you to generate an announcement based on VoiceInstructions objects and returns a state object (that includes the announcement to be played when the announcement is ready or an error and a fallback with the raw announcement) that should be passed to MapboxVoiceInstructionsPlayer text-to-speech engine to be played.
  • MapboxVoiceInstructionsPlayer: Text-to-speech engine implementation. Internally, this uses VoiceInstructionsFilePlayer or VoiceInstructionsTextPlayer speech players, if a synthesized speech mp3 is provided or not, respectively.

To instantiate the Api you need to do the following in your Activity or Fragment:

val speechApi = MapboxSpeechApi(this, getMapboxAccessTokenFromResources(), Locale.US.language)

To instantiate the text-to-speech engine you need to do the following in your Activity or Fragment:

val voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer =
    MapboxVoiceInstructionsPlayer(
        this,
        getMapboxAccessTokenFromResources(),
        Locale.US.language
    )

The result of invoking MapboxSpeechApi#generate is returned as a callback containing either a success in the form of SpeechValue or failure in the form of SpeechError. So in order to retrieve and play an announcement, do the following:

val speechCallback =
    object : MapboxNavigationConsumer<Expected<SpeechValue, SpeechError>> {
        override fun accept(value: Expected<SpeechValue, SpeechError>) {
            when (value) {
                is Expected.Success -> {
                    // The announcement data obtained (synthesized speech mp3 file from Mapbox's API Voice) is played
                    // using [MapboxVoiceInstructionsPlayer]
                    voiceInstructionsPlayer.play(
                        value.value.announcement,
                        voiceInstructionsPlayerCallback
                    )
                }
                is Expected.Failure -> {
                    // In case of error, a fallback announcement is returned that can be played
                    // using [MapboxVoiceInstructionsPlayer]
                    voiceInstructionsPlayer.play(
                        value.error.fallback,
                        voiceInstructionsPlayerCallback
                    )
                }
            }
        }
    }

val voiceInstructionsObserver = object : VoiceInstructionsObserver {
    override fun onNewVoiceInstructions(voiceInstructions: VoiceInstructions) {
        // The data obtained must be used to generate the speech announcement
        speechApi.generate(
            voiceInstructions,
            speechCallback
        )
    }
}

The result of invoking MapboxVoiceInstructionsPlayer#play is returned as a callback containing SpeechAnnouncement. This can be used to cleanup any associated files previously generated:

val voiceInstructionsPlayerCallback =
    object : MapboxNavigationConsumer<SpeechAnnouncement> {
        override fun accept(value: SpeechAnnouncement) {
            speechApi.clean(value)
        }
    }

Also, every time a new route is obtained make sure to cancel any potential in-flight MapboxSpeechApi requests and clear the MapboxVoiceInstructionsPlayer queue:

val routesObserver = object : RoutesObserver {
    override fun onRoutesChanged(routes: List<DirectionsRoute>) {
        speechApi.cancel()
        voiceInstructionsPlayer.clear()
    }
}

Register a VoiceInstructionsObserver with MapboxNavigation:

mapboxNavigation = MapboxNavigation(
    mapboxNavigationOptions
)
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)

Don't forget to unregister the observer, cancel any potential in-flight MapboxSpeechApi requests and shutdown MapboxVoiceInstructionsPlayer in onStop or onDestroy:

override fun onDestroy() {
    super.onDestroy()
    mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
    speechApi.cancel()
    voiceInstructionsPlayer.shutdown()
}

Noting that currently there's no replacement for VoiceInstructionLoader. Tracking that work in https://github.com/mapbox/mapbox-navigation-android/issues/4208