diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bce98b0c..3344e7d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -109,6 +109,10 @@ + + \ No newline at end of file diff --git a/app/src/main/java/com/mapbox/navigation/examples/ExamplesList.kt b/app/src/main/java/com/mapbox/navigation/examples/ExamplesList.kt index f02671d8..061532e1 100644 --- a/app/src/main/java/com/mapbox/navigation/examples/ExamplesList.kt +++ b/app/src/main/java/com/mapbox/navigation/examples/ExamplesList.kt @@ -11,6 +11,7 @@ import com.mapbox.navigation.examples.junctions.ShowJunctionsActivity import com.mapbox.navigation.examples.location.ShowCurrentLocationActivity import com.mapbox.navigation.examples.maneuvers.ShowManeuversActivity import com.mapbox.navigation.examples.multiplewaypoints.MultipleWaypointsActivity +import com.mapbox.navigation.examples.preview.PreviewActivity import com.mapbox.navigation.examples.replay.ReplayHistoryActivity import com.mapbox.navigation.examples.routeline.RenderRouteLineActivity import com.mapbox.navigation.examples.signboard.MapboxSignboardActivity @@ -82,6 +83,12 @@ fun Context.examplesList() = listOf( getString(R.string.description_turn_by_turn), TurnByTurnExperienceActivity::class.java ), + MapboxExample( + ContextCompat.getDrawable(this, R.drawable.mapbox_route_preview), + getString(R.string.title_preview), + getString(R.string.description_preview), + PreviewActivity::class.java + ), MapboxExample( ContextCompat.getDrawable(this, R.drawable.mapbox_screenshot_multiple_waypoints), getString(R.string.title_multiple_way_points), diff --git a/app/src/main/java/com/mapbox/navigation/examples/preview/PreviewActivity.kt b/app/src/main/java/com/mapbox/navigation/examples/preview/PreviewActivity.kt new file mode 100644 index 00000000..b2c07604 --- /dev/null +++ b/app/src/main/java/com/mapbox/navigation/examples/preview/PreviewActivity.kt @@ -0,0 +1,390 @@ +package com.mapbox.navigation.examples.preview + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.res.Resources +import android.location.Location +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import com.mapbox.api.directions.v5.models.Bearing +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.geojson.Point +import com.mapbox.maps.EdgeInsets +import com.mapbox.maps.MapView +import com.mapbox.maps.MapboxMap +import com.mapbox.maps.Style +import com.mapbox.maps.plugin.LocationPuck2D +import com.mapbox.maps.plugin.animation.camera +import com.mapbox.maps.plugin.gestures.gestures +import com.mapbox.maps.plugin.locationcomponent.location +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.MapboxNavigationProvider +import com.mapbox.navigation.core.directions.session.RoutesObserver +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import com.mapbox.navigation.core.trip.session.LocationObserver +import com.mapbox.navigation.examples.R +import com.mapbox.navigation.examples.databinding.ActivityPreviewBinding +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.transition.NavigationCameraTransitionOptions +import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider +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.MapboxRouteLineOptions + +/** + * This example demonstrates: + * - A basic route preview; + * - Switching between free drive, preview, and active guidance. + * + * 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 + * + * + * + * + * + * + * 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. + * + * How to use this example: + * - You can long-click the map to select a destination. + * - Click "Start active guidance" to start navigation. + * - Click "Finish active guidance" to switch back to free drive. + */ +class PreviewActivity : Activity() { + + /** + * Bindings to the example layout. + */ + private lateinit var binding: ActivityPreviewBinding + + /** + * Mapbox Maps entry point obtained from the [MapView]. + * You need to get a new reference to this object whenever the [MapView] is recreated. + */ + private lateinit var mapboxMap: MapboxMap + + /** + * [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() + + /** + * Mapbox Navigation entry point. There should only be one instance of this object for the app. + * You can use [MapboxNavigationProvider] to help create and obtain that instance. + */ + private lateinit var mapboxNavigation: MapboxNavigation + + /** + * 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 + ) + } + + /** + * The observer 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) { + // Use raw location only for cycling and walking cases. + // For vehicles use map matched location. + } + + 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() + ) + } + } + } + + /** + * The observer gets notified whenever the tracked routes change. + * Use this observer to draw routes during active guidance or to cleanup when navigation switches to free drive. + * The observer isn't triggered in free drive. + */ + private val routesObserver = RoutesObserver { routeUpdateResult -> + val navigationRoutes = routeUpdateResult.navigationRoutes + if (navigationRoutes.isNotEmpty()) { + routeLineApi.setNavigationRoutes( + navigationRoutes, + // alternative metadata is available only in active guidance. + mapboxNavigation.getAlternativeMetadataFor(navigationRoutes) + ) { value -> + mapboxMap.getStyle()?.apply { + routeLineView.renderRouteDrawData(this, value) + } + } + + // update the camera position to account for the new route + viewportDataSource.onRouteChanged(navigationRoutes.first()) + viewportDataSource.evaluate() + } else { + // remove route line from the map + mapboxMap.getStyle()?.let { style -> + routeLineApi.clearRouteLine { value -> + routeLineView.renderClearRouteLineValue( + style, + value + ) + } + } + // remove the route reference from camera position evaluations + viewportDataSource.clearRouteData() + viewportDataSource.evaluate() + navigationCamera.requestNavigationCameraToOverview() + } + } + + /** + * 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 + + @SuppressLint("MissingPermission") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityPreviewBinding.inflate(layoutInflater) + setContentView(binding.root) + + mapboxMap = binding.mapView.getMapboxMap() + + // initialize the location puck + binding.mapView.location.apply { + this.locationPuck = LocationPuck2D( + bearingImage = ContextCompat.getDrawable( + this@PreviewActivity, + R.drawable.mapbox_navigation_puck_icon + ) + ) + setLocationProvider(navigationLocationProvider) + enabled = true + } + + // initialize Mapbox Navigation + mapboxNavigation = if (MapboxNavigationProvider.isCreated()) { + MapboxNavigationProvider.retrieve() + } else { + MapboxNavigationProvider.create( + NavigationOptions.Builder(this.applicationContext) + .accessToken(getString(R.string.mapbox_access_token)) + .build() + ) + } + + // initialize Navigation Camera + viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap) + navigationCamera = NavigationCamera( + mapboxMap, + binding.mapView.camera, + viewportDataSource + ) + // set camera paddings + viewportDataSource.overviewPadding = overviewPadding + viewportDataSource.followingPadding = followingPadding + // 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) + ) + + // load map style + mapboxMap.loadStyleUri( + Style.MAPBOX_STREETS + ) { + // add long click listener that search for a route to the clicked destination + binding.mapView.gestures.addOnMapLongClickListener { point -> + findRoute(point) + true + } + } + + // initialize route line, the withRouteLineBelowLayerId 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 = MapboxRouteLineOptions.Builder(this) + .withRouteLineBelowLayerId("road-label") + .build() + routeLineApi = MapboxRouteLineApi(mapboxRouteLineOptions) + routeLineView = MapboxRouteLineView(mapboxRouteLineOptions) + + // We recommend starting a trip session for routes preview to get, display, + // and use for route request a map matched location. + // See [PreviewActivity#locationObserver]. + mapboxNavigation.startTripSession() + } + + override fun onStart() { + super.onStart() + mapboxNavigation.registerLocationObserver(locationObserver) + mapboxNavigation.registerRoutesObserver(routesObserver) + } + + override fun onStop() { + super.onStop() + mapboxNavigation.unregisterLocationObserver(locationObserver) + mapboxNavigation.unregisterRoutesObserver(routesObserver) + } + + override fun onDestroy() { + super.onDestroy() + mapboxNavigation.onDestroy() + } + + private fun findRoute(destination: Point) { + val originLocation = navigationLocationProvider.lastLocation + val originPoint = originLocation?.let { + Point.fromLngLat(it.longitude, it.latitude) + } ?: return + + // execute a route request + // it's recommended to use the + // applyDefaultNavigationOptions and applyLanguageAndVoiceUnitOptions + // that make sure the route request is optimized + // to allow for support of all of the Navigation SDK features + mapboxNavigation.requestRoutes( + RouteOptions.builder() + .applyDefaultNavigationOptions() + .applyLanguageAndVoiceUnitOptions(this) + .coordinatesList(listOf(originPoint, destination)) + // provide the bearing for the origin of the request to ensure + // that the returned route faces in the direction of the current user movement + .bearingsList( + listOf( + Bearing.builder() + .angle(originLocation.bearing.toDouble()) + .degrees(45.0) + .build(), + null + ) + ) + .layersList(listOf(mapboxNavigation.getZLevel(), null)) + .alternatives(true) + .build(), + object : NavigationRouterCallback { + override fun onRoutesReady( + routes: List, + routerOrigin: RouterOrigin + ) { + previewRoutes(routes) + } + + override fun onFailure( + reasons: List, + routeOptions: RouteOptions + ) { + // no impl + } + + override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { + // no impl + } + } + ) + } + + private fun previewRoutes(routes: List) { + // Mapbox navigation doesn't have a special state for route preview. + // Preview state is managed by an application. + // Display the routes you received on the map. + routeLineApi.setNavigationRoutes(routes) { value -> + mapboxMap.getStyle()?.apply { + routeLineView.renderRouteDrawData(this, value) + // update the camera position to account for the new route + viewportDataSource.onRouteChanged(routes.first()) + viewportDataSource.evaluate() + navigationCamera.requestNavigationCameraToOverview() + } + } + binding.buttonStartActiveGuidance.apply { + visibility = View.VISIBLE + setOnClickListener { + startActiveGuidance(routes) + } + } + } + + private fun startActiveGuidance(routes: List) { + binding.buttonStartActiveGuidance.visibility = View.GONE + // Set routes to switch navigator from free drive to active guidance state. + // In active guidance navigator emits your route progress, voice and banner instructions, etc. + mapboxNavigation.setNavigationRoutes(routes) + navigationCamera.requestNavigationCameraToFollowing() + binding.buttonFinishActiveGuidance.apply { + visibility = View.VISIBLE + setOnClickListener { + visibility = View.GONE + // Set an empty list to finish active guidance and switch back to free drive state. + mapboxNavigation.setNavigationRoutes(emptyList()) + } + } + } +} diff --git a/app/src/main/java/com/mapbox/navigation/examples/preview/res/drawable/mapbox_route_preview.jpg b/app/src/main/java/com/mapbox/navigation/examples/preview/res/drawable/mapbox_route_preview.jpg new file mode 100644 index 00000000..094805cb Binary files /dev/null and b/app/src/main/java/com/mapbox/navigation/examples/preview/res/drawable/mapbox_route_preview.jpg differ diff --git a/app/src/main/java/com/mapbox/navigation/examples/preview/res/layout/activity_preview.xml b/app/src/main/java/com/mapbox/navigation/examples/preview/res/layout/activity_preview.xml new file mode 100644 index 00000000..454bd615 --- /dev/null +++ b/app/src/main/java/com/mapbox/navigation/examples/preview/res/layout/activity_preview.xml @@ -0,0 +1,40 @@ + + + + + +