-
Notifications
You must be signed in to change notification settings - Fork 319
2.0 Navigation SDK Migration Guide
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.
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-v2InstructionView
. -
com.mapbox.navigation:ui-tripprogress
offers the trip progress view and APIs that replace the pre-v2SummaryBottomSheet
. -
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.
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.
- https://kotlinlang.org/docs/reference/java-interop.html#calling-java-code-from-kotlin and https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html have helpful information on Java and Kotlin interoperability.
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.
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
.
The NavigationOptions
class now contains all the right defaults out of the box, so the MapboxNavigation#defaultNavigationOptionsBuilder
has been removed.
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.
- 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 folderAPP_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.
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.
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
orBannerInstruction
orRouteStepProgress
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 givenRouteLeg
.
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) {}
}
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)
}
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 theMapboxRouteLineApi
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.
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 recognition is performed by the MapRouteLineApi#findClosestRoute
which also takes padding as an argument.
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.
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 theMapboxRouteArrowApi
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.
Mapbox Maps SDK v10 removes the CameraMode
s 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 CameraMode
s 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 CameraMode
s.
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
.
viewportDataSource = MapboxNavigationViewportDataSource(
MapboxNavigationViewportDataSourceOptions.Builder().build(),
mapView.getMapboxMap()
)
navigationCamera = NavigationCamera(
mapView.getMapboxMap(),
mapView.getCameraAnimationsPlugin(),
viewportDataSource
)
locationComponent.setCameraMode(cameraMode)
This would transition to the last known location sample that the LocationComponent
knew (or do nothing if not available).
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()
Any location update pushed to the LocationComponent
after setting the CameraMode
would update the camera.
Update the viewport data source:
viewportDataSource.onLocationChanged(location)
viewportDataSource.evaluate()
which will trigger a camera update when NavigationCamera
is in a FOLLOWING
state.
Tracking the users' location while having the camera pointing to the north was a separate state:
locationComponent.setCameraMode(CameraMode.TRACKING_GPS_NORTH)
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()
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.
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, 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()
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.
The default MapboxNavigationViewportDataSource
does not apply any padding. You can apply padding by overriding the property:
viewportDataSource.followingPaddingPropertyOverride(padding)
viewportDataSource.evaluate()
Both the legacy CameraMode
s 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.
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.
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 theNavigationCamera
toIDLE
whenever any outside transition or gesture interaction happens. -
NavigationScaleGestureHandler
which requests theNavigationCamera
toIDLE
whenever any outside transition happens but allows for executing navigation gestures described above, same asLocationComponentOptions#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() }
)
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.
The CameraMode
s 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.
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()
}
}
CameraMode
s 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.
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.
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 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 toMapboxVoiceInstructionsPlayer
text-to-speech engine to be played. -
MapboxVoiceInstructionsPlayer: Text-to-speech engine implementation. Internally, this uses
VoiceInstructionsFilePlayer
orVoiceInstructionsTextPlayer
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