Skip to main content

Use camera to frame the map

Note

This example is a part of the Navigation SDK Examples. You can find the values for all referenced resources in the res directory. For example, see res/values/strings.xml for R.string.* references used in this example. The dependencies can be found here.The examples use View binding.See setup documention if necessary.

mapbox_activity_camera_transitions
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.mapbox.maps.MapView
android:id="@+id/mapView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/route_button"
app:layout_constraintEnd_toEndOf="@id/mapView">

<com.mapbox.navigation.ui.components.maps.camera.view.MapboxRouteOverviewButton
android:id="@+id/overview_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" />

<com.mapbox.navigation.ui.components.maps.camera.view.MapboxRecenterButton
android:id="@+id/recenter_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:visibility="invisible" />

</LinearLayout>

<Button
android:id="@+id/route_button"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="36dp"
android:background="@drawable/mapbox_button"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:ignore="HardcodedText" />

</androidx.constraintlayout.widget.ConstraintLayout>
ShowCameraTransitionsActivity.kt
package com.mapbox.navigation.examples.standalone.camera

import android.annotation.SuppressLint
import android.content.res.Resources
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.common.location.Location
import com.mapbox.geojson.Point
import com.mapbox.maps.EdgeInsets
import com.mapbox.maps.ImageHolder
import com.mapbox.maps.Style
import com.mapbox.maps.plugin.LocationPuck2D
import com.mapbox.maps.plugin.animation.camera
import com.mapbox.maps.plugin.locationcomponent.location
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.base.route.NavigationRouterCallback
import com.mapbox.navigation.base.route.RouterFailure
import com.mapbox.navigation.base.route.RouterOrigin
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
import com.mapbox.navigation.core.lifecycle.requireMapboxNavigation
import com.mapbox.navigation.core.replay.route.ReplayProgressObserver
import com.mapbox.navigation.core.replay.route.ReplayRouteMapper
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.core.trip.session.LocationObserver
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
import com.mapbox.navigation.examples.R
import com.mapbox.navigation.examples.databinding.MapboxActivityCameraTransitionsBinding
import com.mapbox.navigation.ui.maps.camera.NavigationCamera
import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
import com.mapbox.navigation.ui.maps.camera.lifecycle.NavigationBasicGesturesHandler
import com.mapbox.navigation.ui.maps.camera.state.NavigationCameraState
import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions
import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
import com.mapbox.navigation.ui.maps.route.line.MapboxRouteLineApiExtensions.setNavigationRoutes
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
import kotlinx.coroutines.launch
import java.util.Date

/**
* This example demonstrates the usage of [NavigationCamera] to track user location, and frame the route and upcoming maneuvers.
*
* Before running the example make sure you have put your access_token in the correct place
* inside [app/src/main/res/values/mapbox_access_token.xml]. If not present then add this file
* at the location mentioned above and add the following content to it
*
* <?xml version="1.0" encoding="utf-8"?>
* <resources xmlns:tools="http://schemas.android.com/tools">
* <string name="mapbox_access_token"><PUT_YOUR_ACCESS_TOKEN_HERE></string>
* </resources>
*
* The example assumes that you have granted location permissions and does not enforce it. However,
* the permission is essential for proper functioning of this example. The example also uses replay
* location engine to facilitate navigation without actually physically moving.
*
* How to use this example:
* - The example uses a list of predefined coordinates that will be used to fetch a route.
* - When the example starts, the camera transitions to the location where the route origin is.
* The status is free driving.
* - You can manage whether we're in active guidance and the route reference is available for framing upcoming maneuvers with set/clear route button.
* - Click recenter/overview buttons to change the camera state.
*/
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class ShowCameraTransitionsActivity : AppCompatActivity() {

private val routeCoordinates = listOf(
Point.fromLngLat(-122.4192, 37.7627),
Point.fromLngLat(-122.4106, 37.7676),
)

/**
* Debug observer that makes sure the replayer has always an up-to-date information to generate mock updates.
*/
private lateinit var replayProgressObserver: ReplayProgressObserver

/**
* Bindings to the example layout.
*/
private lateinit var binding: MapboxActivityCameraTransitionsBinding

/**
* Used to execute camera transitions based on the data generated by the [viewportDataSource].
* This includes transitions from route overview to route following and continuously updating the camera as the location changes.
*/
private lateinit var navigationCamera: NavigationCamera

/**
* Produces the camera frames based on the location and routing data for the [navigationCamera] to execute.
*/
private lateinit var viewportDataSource: MapboxNavigationViewportDataSource

/*
* Below are generated camera padding values to ensure that the route fits well on screen while
* other elements are overlaid on top of the map (including instruction view, buttons, etc.)
*/
private val pixelDensity = Resources.getSystem().displayMetrics.density
private val overviewPadding: EdgeInsets by lazy {
EdgeInsets(
140.0 * pixelDensity,
40.0 * pixelDensity,
120.0 * pixelDensity,
40.0 * pixelDensity
)
}
private val followingPadding: EdgeInsets by lazy {
EdgeInsets(
180.0 * pixelDensity,
40.0 * pixelDensity,
150.0 * pixelDensity,
40.0 * pixelDensity
)
}

/**
* Generates updates for the [routeLineView] with the geometries and properties of the routes that should be drawn on the map.
*/
private lateinit var routeLineApi: MapboxRouteLineApi

/**
* Draws route lines on the map based on the data from the [routeLineApi]
*/
private lateinit var routeLineView: MapboxRouteLineView

/**
* [NavigationLocationProvider] is a utility class that helps to provide location updates generated by the Navigation SDK
* to the Maps SDK in order to update the user location indicator on the map.
*/
private val navigationLocationProvider = NavigationLocationProvider()

/**
* Gets notified with location updates.
*
* Exposes raw updates coming directly from the location services
* and the updates enhanced by the Navigation SDK (cleaned up and matched to the road).
*/
private val locationObserver = object : LocationObserver {
var firstLocationUpdateReceived = false

override fun onNewRawLocation(rawLocation: Location) {
// not handled
}

override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
val enhancedLocation = locationMatcherResult.enhancedLocation
// update location puck's position on the map
navigationLocationProvider.changePosition(
location = enhancedLocation,
keyPoints = locationMatcherResult.keyPoints,
)

// update camera position to account for new location
viewportDataSource.onLocationChanged(enhancedLocation)
viewportDataSource.evaluate()

// if this is the first location update the activity has received,
// it's best to immediately move the camera to the current user location
if (!firstLocationUpdateReceived) {
firstLocationUpdateReceived = true
navigationCamera.requestNavigationCameraToOverview(
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
.maxDuration(0) // instant transition
.build()
)
}
}
}

/**
* Gets notified with progress along the currently active route.
*/
private val routeProgressObserver = RouteProgressObserver { routeProgress ->
// update the camera position to account for the progressed fragment of the route
viewportDataSource.onRouteProgressChanged(routeProgress)
viewportDataSource.evaluate()
}

/**
* Gets notified whenever the tracked routes change.
*
* A change can mean:
* - routes get changed with [MapboxNavigation.setNavigationRoutes]
* - routes annotations get refreshed (for example, congestion annotation that indicate the live traffic along the route)
* - driver got off route and a reroute was executed
*/
private val routesObserver = RoutesObserver { routeUpdateResult ->
lifecycleScope.launch {
if (routeUpdateResult.navigationRoutes.isNotEmpty()) {
routeLineApi.setNavigationRoutes(
newRoutes = routeUpdateResult.navigationRoutes,
alternativeRoutesMetadata = mapboxNavigation.getAlternativeMetadataFor(
routeUpdateResult.navigationRoutes
)
).apply {
routeLineView.renderRouteDrawData(
binding.mapView.mapboxMap.style!!,
this
)
}
// update the camera position to account for the new route
viewportDataSource.onRouteChanged(routeUpdateResult.navigationRoutes.first())
viewportDataSource.evaluate()
} else {
routeLineApi.clearRouteLine { value ->
routeLineView.renderClearRouteLineValue(
binding.mapView.mapboxMap.style!!,
value
)
}
// remove the route reference from camera position evaluations
viewportDataSource.clearRouteData()
viewportDataSource.evaluate()
}
}
}

private val mapboxNavigation: MapboxNavigation by requireMapboxNavigation(
onResumedObserver = object : MapboxNavigationObserver {
@SuppressLint("MissingPermission")
override fun onAttached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.registerRoutesObserver(routesObserver)
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
mapboxNavigation.registerLocationObserver(locationObserver)

replayProgressObserver = ReplayProgressObserver(mapboxNavigation.mapboxReplayer)
mapboxNavigation.registerRouteProgressObserver(replayProgressObserver)

// Start the trip session to being receiving location updates in free drive
// and later when a route is set also receiving route progress updates.
// In case of `startReplayTripSession`,
// location events are emitted by the `MapboxReplayer`
mapboxNavigation.startReplayTripSession()
}

override fun onDetached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.unregisterRoutesObserver(routesObserver)
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
mapboxNavigation.unregisterLocationObserver(locationObserver)
mapboxNavigation.unregisterRouteProgressObserver(replayProgressObserver)
mapboxNavigation.mapboxReplayer.finish()
}
},
onInitialize = this::initNavigation
)

@SuppressLint("MissingPermission", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = MapboxActivityCameraTransitionsBinding.inflate(layoutInflater)
setContentView(binding.root)
val mapboxMap = binding.mapView.mapboxMap

// initialize Navigation Camera
viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap)
navigationCamera = NavigationCamera(
mapboxMap,
binding.mapView.camera,
viewportDataSource
)
// set the animations lifecycle listener to ensure the NavigationCamera stops
// automatically following the user location when the map is interacted with
binding.mapView.camera.addCameraAnimationsLifecycleListener(
NavigationBasicGesturesHandler(navigationCamera)
)
navigationCamera.registerNavigationCameraStateChangeObserver { navigationCameraState ->
// shows/hide the recenter button depending on the camera state
when (navigationCameraState) {
NavigationCameraState.TRANSITION_TO_FOLLOWING,
NavigationCameraState.FOLLOWING ->
binding.recenterButton.visibility = View.GONE
NavigationCameraState.TRANSITION_TO_OVERVIEW,
NavigationCameraState.OVERVIEW,
NavigationCameraState.IDLE -> binding.recenterButton.visibility = View.VISIBLE
}
}
// set the padding values depending to correctly frame maneuvers and the puck
viewportDataSource.overviewPadding = overviewPadding
viewportDataSource.followingPadding = followingPadding

// initialize route line, the routeLineBelowLayerId is specified to place
// the route line below road labels layer on the map
// the value of this option will depend on the style that you are using
// and under which layer the route line should be placed on the map layers stack
val mapboxRouteLineOptions = MapboxRouteLineViewOptions.Builder(this)
.routeLineBelowLayerId("road-label")
.build()
routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
routeLineView = MapboxRouteLineView(mapboxRouteLineOptions)

// add click listeners for buttons
binding.recenterButton.setOnClickListener {
navigationCamera.requestNavigationCameraToFollowing()
}
binding.overviewButton.setOnClickListener {
navigationCamera.requestNavigationCameraToOverview()
}

binding.routeButton.text = "Fetch route"

// load map style
mapboxMap.loadStyle(
Style.MAPBOX_STREETS
) {
// only once the style is loaded expose an ability to add and draw a route
binding.routeButton.setOnClickListener {
if (mapboxNavigation.getNavigationRoutes().isEmpty()) {
fetchRoute()
binding.routeButton.text = "Clear route"
} else {
// clear the routes
mapboxNavigation.setNavigationRoutes(listOf())
binding.routeButton.text = "Fetch route"
}
}
}
}

override fun onDestroy() {
super.onDestroy()
routeLineApi.cancel()
routeLineView.cancel()
}

private fun initNavigation() {
MapboxNavigationApp.setup(
NavigationOptions.Builder(this)
.build()
)

binding.mapView.location.apply {
this.locationPuck = LocationPuck2D(
bearingImage = ImageHolder.Companion.from(
R.drawable.mapbox_navigation_puck_icon
)
)
setLocationProvider(navigationLocationProvider)

puckBearingEnabled = true
enabled = true
}

replayOriginLocation()
}

private fun setNavigationRoutes(routes: List<NavigationRoute>) {
// disable navigation camera
navigationCamera.requestNavigationCameraToIdle()
// set a route to receive route progress updates and provide a route reference
// to the viewport data source (via RoutesObserver)
mapboxNavigation.setNavigationRoutes(routes)
// enable the camera back
navigationCamera.requestNavigationCameraToOverview()
}

private fun fetchRoute() {
mapboxNavigation.requestRoutes(
RouteOptions.builder()
.applyDefaultNavigationOptions()
.applyLanguageAndVoiceUnitOptions(this)
.alternatives(false)
.coordinatesList(routeCoordinates)
.layersList(listOf(mapboxNavigation.getZLevel(), null))
.build(),

object : NavigationRouterCallback {
override fun onRoutesReady(
routes: List<NavigationRoute>,
@RouterOrigin routerOrigin: String
) {
setNavigationRoutes(routes)
}

override fun onFailure(
reasons: List<RouterFailure>,
routeOptions: RouteOptions
) {
Log.d(LOG_TAG, "onFailure: $reasons")
}

override fun onCanceled(
routeOptions: RouteOptions,
@RouterOrigin routerOrigin: String
) {
Log.d(LOG_TAG, "onCanceled")
}
}
)
}

private fun replayOriginLocation() {
with(mapboxNavigation.mapboxReplayer) {
play()
pushEvents(
listOf(
ReplayRouteMapper.mapToUpdateLocation(
Date().time.toDouble(),
routeCoordinates.first()
)
)
)
playFirstLocation()
playbackSpeed(3.0)
}
}

private companion object {
val LOG_TAG: String = ShowCameraTransitionsActivity::class.java.simpleName
}
}
Was this example helpful?