From 01fbf32042b208e33a1a13b0ecb008b9d7d0cfe6 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Tue, 23 Jul 2024 00:55:59 +0200 Subject: [PATCH] feat!: Application outline --- .../java/eu/ztsh/garmin/ExampleActivity.kt | 692 ++++++++++++++++++ .../main/java/eu/ztsh/garmin/MainActivity.kt | 94 +-- app/src/main/java/eu/ztsh/garmin/UI.kt | 59 ++ .../eu/ztsh/garmin/mapbox/LocationObserver.kt | 46 ++ .../java/eu/ztsh/garmin/mapbox/MapControl.kt | 203 +++-- .../ztsh/garmin/mapbox/NavigationObserver.kt | 46 -- .../eu/ztsh/garmin/mapbox/RouteControl.kt | 306 ++++++++ .../eu/ztsh/garmin/mapbox/VoiceControl.kt | 111 +++ .../eu/ztsh/garmin/mock/ReplayResources.kt | 73 ++ 9 files changed, 1440 insertions(+), 190 deletions(-) create mode 100644 app/src/main/java/eu/ztsh/garmin/ExampleActivity.kt create mode 100644 app/src/main/java/eu/ztsh/garmin/UI.kt create mode 100644 app/src/main/java/eu/ztsh/garmin/mapbox/LocationObserver.kt delete mode 100644 app/src/main/java/eu/ztsh/garmin/mapbox/NavigationObserver.kt create mode 100644 app/src/main/java/eu/ztsh/garmin/mapbox/RouteControl.kt create mode 100644 app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt create mode 100644 app/src/main/java/eu/ztsh/garmin/mock/ReplayResources.kt diff --git a/app/src/main/java/eu/ztsh/garmin/ExampleActivity.kt b/app/src/main/java/eu/ztsh/garmin/ExampleActivity.kt new file mode 100644 index 0000000..91a77f4 --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/ExampleActivity.kt @@ -0,0 +1,692 @@ +//package eu.ztsh.garmin +// +//import android.annotation.SuppressLint +//import android.content.res.Configuration +//import android.content.res.Resources +//import android.os.Bundle +//import android.view.View +//import android.widget.Toast +//import androidx.appcompat.app.AppCompatActivity +//import com.mapbox.api.directions.v5.models.Bearing +//import com.mapbox.api.directions.v5.models.DirectionsRoute +//import com.mapbox.api.directions.v5.models.RouteOptions +//import com.mapbox.bindgen.Expected +//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.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.ExperimentalPreviewMapboxNavigationAPI +//import com.mapbox.navigation.base.TimeFormat +//import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions +//import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions +//import com.mapbox.navigation.base.formatter.DistanceFormatterOptions +//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.core.MapboxNavigation +//import com.mapbox.navigation.core.directions.session.RoutesObserver +//import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter +//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.core.trip.session.VoiceInstructionsObserver +//import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi +//import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi +//import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter +//import com.mapbox.navigation.tripdata.progress.model.EstimatedTimeToArrivalFormatter +//import com.mapbox.navigation.tripdata.progress.model.PercentDistanceTraveledFormatter +//import com.mapbox.navigation.tripdata.progress.model.TimeRemainingFormatter +//import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateFormatter +//import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer +//import com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView +//import com.mapbox.navigation.ui.components.tripprogress.view.MapboxTripProgressView +//import com.mapbox.navigation.ui.maps.NavigationStyles +//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.arrow.api.MapboxRouteArrowApi +//import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView +//import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions +//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 com.mapbox.navigation.voice.api.MapboxSpeechApi +//import com.mapbox.navigation.voice.api.MapboxVoiceInstructionsPlayer +//import com.mapbox.navigation.voice.model.SpeechAnnouncement +//import com.mapbox.navigation.voice.model.SpeechError +//import com.mapbox.navigation.voice.model.SpeechValue +//import com.mapbox.navigation.voice.model.SpeechVolume +//import eu.ztsh.garmin.databinding.ActivityMainBinding +//import java.util.Date +//import java.util.Locale +// +///** +// * This example demonstrates a basic turn-by-turn navigation experience by putting together some UI elements to showcase +// * navigation camera transitions, guidance instructions banners and playback, and progress along the route. +// * +// * 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. The example also uses replay +// * location engine to facilitate navigation without actually physically moving. +// * +// * How to use this example: +// * - You can long-click the map to select a destination. +// * - The guidance will start to the selected destination while simulating location updates. +// * You can disable simulation by commenting out the [replayLocationEngine] setter in [NavigationOptions]. +// * Then, the device's real location will be used. +// * - At any point in time you can finish guidance or select a new destination. +// * - You can use buttons to mute/unmute voice instructions, recenter the camera, or show the route overview. +// */ +//@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +//class ExampleActivity { +// +// private companion object { +// private const val BUTTON_ANIMATION_DURATION = 1500L +// } +// +// /** +// * Debug observer that makes sure the replayer has always an up-to-date information to generate mock updates. +// */ +// private lateinit var replayProgressObserver: ReplayProgressObserver +// +// /** +// * Debug object that converts a route into events that can be replayed to navigate a route. +// */ +// private val replayRouteMapper = ReplayRouteMapper() +// +// /** +// * Bindings to the example layout. +// */ +// private lateinit var binding: ActivityMainBinding +// +// /** +// * 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 landscapeOverviewPadding: EdgeInsets by lazy { +// EdgeInsets( +// 30.0 * pixelDensity, +// 380.0 * pixelDensity, +// 110.0 * pixelDensity, +// 20.0 * pixelDensity +// ) +// } +// private val followingPadding: EdgeInsets by lazy { +// EdgeInsets( +// 180.0 * pixelDensity, +// 40.0 * pixelDensity, +// 150.0 * pixelDensity, +// 40.0 * pixelDensity +// ) +// } +// private val landscapeFollowingPadding: EdgeInsets by lazy { +// EdgeInsets( +// 30.0 * pixelDensity, +// 380.0 * pixelDensity, +// 110.0 * pixelDensity, +// 40.0 * pixelDensity +// ) +// } +// +// /** +// * Generates updates for the [MapboxManeuverView] to display the upcoming maneuver instructions +// * and remaining distance to the maneuver point. +// */ +// private lateinit var maneuverApi: MapboxManeuverApi +// +// /** +// * Generates updates for the [MapboxTripProgressView] that include remaining time and distance to the destination. +// */ +// private lateinit var tripProgressApi: MapboxTripProgressApi +// +// /** +// * 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 +// +// /** +// * Generates updates for the [routeArrowView] with the geometries and properties of maneuver arrows that should be drawn on the map. +// */ +// private val routeArrowApi: MapboxRouteArrowApi = MapboxRouteArrowApi() +// +// /** +// * Draws maneuver arrows on the map based on the data [routeArrowApi]. +// */ +// private lateinit var routeArrowView: MapboxRouteArrowView +// +// /** +// * Stores and updates the state of whether the voice instructions should be played as they come or muted. +// */ +// private var isVoiceInstructionsMuted = false +// set(value) { +// field = value +// if (value) { +// binding.soundButton.muteAndExtend(BUTTON_ANIMATION_DURATION) +// voiceInstructionsPlayer.volume(SpeechVolume(0f)) +// } else { +// binding.soundButton.unmuteAndExtend(BUTTON_ANIMATION_DURATION) +// voiceInstructionsPlayer.volume(SpeechVolume(1f)) +// } +// } +// +// /** +// * Extracts message that should be communicated to the driver about the upcoming maneuver. +// * When possible, downloads a synthesized audio file that can be played back to the driver. +// */ +// private lateinit var speechApi: MapboxSpeechApi +// +// /** +// * Plays the synthesized audio files with upcoming maneuver instructions +// * or uses an on-device Text-To-Speech engine to communicate the message to the driver. +// * NOTE: do not use lazy initialization for this class since it takes some time to initialize +// * the system services required for on-device speech synthesis. With lazy initialization +// * there is a high risk that said services will not be available when the first instruction +// * has to be played. [MapboxVoiceInstructionsPlayer] should be instantiated in +// * `Activity#onCreate`. +// */ +// private lateinit var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer +// +// /** +// * Observes when a new voice instruction should be played. +// */ +// private val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions -> +// speechApi.generate(voiceInstructions, speechCallback) +// } +// +// /** +// * Based on whether the synthesized audio file is available, the callback plays the file +// * or uses the fall back which is played back using the on-device Text-To-Speech engine. +// */ +// private val speechCallback = +// MapboxNavigationConsumer> { expected -> +// expected.fold( +// { error -> +// // play the instruction via fallback text-to-speech engine +// voiceInstructionsPlayer.play( +// error.fallback, +// voiceInstructionsPlayerCallback +// ) +// }, +// { value -> +// // play the sound file from the external generator +// voiceInstructionsPlayer.play( +// value.announcement, +// voiceInstructionsPlayerCallback +// ) +// } +// ) +// } +// +// /** +// * When a synthesized audio file was downloaded, this callback cleans up the disk after it was played. +// */ +// private val voiceInstructionsPlayerCallback = +// MapboxNavigationConsumer { value -> +// // remove already consumed file to free-up space +// speechApi.clean(value) +// } +// +// /** +// * [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() +// +// // draw the upcoming maneuver arrow on the map +// val style = binding.mapView.mapboxMap.style +// if (style != null) { +// val maneuverArrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress) +// routeArrowView.renderManeuverUpdate(style, maneuverArrowResult) +// } +// +// // update top banner with maneuver instructions +// val maneuvers = maneuverApi.getManeuvers(routeProgress) +// maneuvers.fold( +// { error -> +// Toast.makeText( +// this@ExampleActivity, +// error.errorMessage, +// Toast.LENGTH_SHORT +// ).show() +// }, +// { +// binding.maneuverView.visibility = View.VISIBLE +// binding.maneuverView.renderManeuvers(maneuvers) +// } +// ) +// +// // update bottom trip progress summary +// binding.tripProgressView.render( +// tripProgressApi.getTripProgress(routeProgress) +// ) +// } +// +// /** +// * 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 -> +// if (routeUpdateResult.navigationRoutes.isNotEmpty()) { +// // generate route geometries asynchronously and render them +// routeLineApi.setNavigationRoutes( +// routeUpdateResult.navigationRoutes +// ) { value -> +// binding.mapView.mapboxMap.style?.apply { +// routeLineView.renderRouteDrawData(this, value) +// } +// } +// +// // update the camera position to account for the new route +// viewportDataSource.onRouteChanged(routeUpdateResult.navigationRoutes.first()) +// viewportDataSource.evaluate() +// } else { +// // remove the route line and route arrow from the map +// val style = binding.mapView.mapboxMap.style +// if (style != null) { +// routeLineApi.clearRouteLine { value -> +// routeLineView.renderClearRouteLineValue( +// style, +// value +// ) +// } +// routeArrowView.render(style, routeArrowApi.clearArrows()) +// } +// +// // 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.registerLocationObserver(locationObserver) +// mapboxNavigation.registerRouteProgressObserver(routeProgressObserver) +// mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver) +// +// 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.unregisterLocationObserver(locationObserver) +// mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver) +// mapboxNavigation.unregisterRouteProgressObserver(replayProgressObserver) +// mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver) +// mapboxNavigation.mapboxReplayer.finish() +// } +// }, +// onInitialize = this::initNavigation +// ) +// +// @SuppressLint("MissingPermission") +// override fun onCreate(savedInstanceState: Bundle?) { +// super.onCreate(savedInstanceState) +// binding = ActivityMainBinding.inflate(layoutInflater) +// setContentView(binding.root) +// +// // initialize Navigation Camera +// viewportDataSource = MapboxNavigationViewportDataSource(binding.mapView.mapboxMap) +// navigationCamera = NavigationCamera( +// binding.mapView.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.recenter.visibility = View.INVISIBLE +// NavigationCameraState.TRANSITION_TO_OVERVIEW, +// NavigationCameraState.OVERVIEW, +// NavigationCameraState.IDLE -> binding.recenter.visibility = View.VISIBLE +// } +// } +// // set the padding values depending on screen orientation and visible view layout +// if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { +// viewportDataSource.overviewPadding = landscapeOverviewPadding +// } else { +// viewportDataSource.overviewPadding = overviewPadding +// } +// if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { +// viewportDataSource.followingPadding = landscapeFollowingPadding +// } else { +// viewportDataSource.followingPadding = followingPadding +// } +// +// // make sure to use the same DistanceFormatterOptions across different features +// val distanceFormatterOptions = DistanceFormatterOptions.Builder(this).build() +// +// // initialize maneuver api that feeds the data to the top banner maneuver view +// maneuverApi = MapboxManeuverApi( +// MapboxDistanceFormatter(distanceFormatterOptions) +// ) +// +// // initialize bottom progress view +// tripProgressApi = MapboxTripProgressApi( +// TripProgressUpdateFormatter.Builder(this) +// .distanceRemainingFormatter( +// DistanceRemainingFormatter(distanceFormatterOptions) +// ) +// .timeRemainingFormatter( +// TimeRemainingFormatter(this) +// ) +// .percentRouteTraveledFormatter( +// PercentDistanceTraveledFormatter() +// ) +// .estimatedTimeToArrivalFormatter( +// EstimatedTimeToArrivalFormatter(this, TimeFormat.NONE_SPECIFIED) +// ) +// .build() +// ) +// +// // initialize voice instructions api and the voice instruction player +// speechApi = MapboxSpeechApi( +// this, +// Locale("pl").language +// ) +// voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer( +// this, +// Locale("pl").language +// ) +// +// // 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 mapboxRouteLineViewOptions = MapboxRouteLineViewOptions.Builder(this) +// .routeLineBelowLayerId("road-label-navigation") +// .build() +// +// routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build()) +// routeLineView = MapboxRouteLineView(mapboxRouteLineViewOptions) +// +// // initialize maneuver arrow view to draw arrows on the map +// val routeArrowOptions = RouteArrowOptions.Builder(this).build() +// routeArrowView = MapboxRouteArrowView(routeArrowOptions) +// +// // load map style +// binding.mapView.mapboxMap.loadStyle(NavigationStyles.NAVIGATION_DAY_STYLE) { +// // Ensure that the route line related layers are present before the route arrow +// routeLineView.initializeLayers(it) +// +// // add long click listener that search for a route to the clicked destination +// binding.mapView.gestures.addOnMapLongClickListener { point -> +// findRoute(point) +// true +// } +// } +// +// // initialize view interactions +// binding.stop.setOnClickListener { +// clearRouteAndStopNavigation() +// } +// binding.recenter.setOnClickListener { +// navigationCamera.requestNavigationCameraToFollowing() +// binding.routeOverview.showTextAndExtend(BUTTON_ANIMATION_DURATION) +// } +// binding.routeOverview.setOnClickListener { +// navigationCamera.requestNavigationCameraToOverview() +// binding.recenter.showTextAndExtend(BUTTON_ANIMATION_DURATION) +// } +// binding.soundButton.setOnClickListener { +// // mute/unmute voice instructions +// isVoiceInstructionsMuted = !isVoiceInstructionsMuted +// } +// +// // set initial sounds button state +// binding.soundButton.unmute() +// } +// +// override fun onDestroy() { +// super.onDestroy() +// maneuverApi.cancel() +// routeLineApi.cancel() +// routeLineView.cancel() +// speechApi.cancel() +// voiceInstructionsPlayer.shutdown() +// } +// +// private fun initNavigation() { +// MapboxNavigationApp.setup( +// NavigationOptions.Builder(this) +// .build() +// ) +// +// // initialize location puck +// binding.mapView.location.apply { +// setLocationProvider(navigationLocationProvider) +// this.locationPuck = LocationPuck2D( +// bearingImage = ImageHolder.Companion.from( +// com.mapbox.navigation.ui.maps.R.drawable.mapbox_navigation_puck_icon +// ) +// ) +// puckBearingEnabled = true +// enabled = true +// } +// +// replayOriginLocation() +// } +// +// private fun replayOriginLocation() { +// with(mapboxNavigation.mapboxReplayer) { +// play() +// pushEvents( +// listOf( +// ReplayRouteMapper.mapToUpdateLocation( +// Date().time.toDouble(), +// Point.fromLngLat(-122.39726512303575, 37.785128345296805) +// ) +// ) +// ) +// playFirstLocation() +// } +// } +// +// private fun findRoute(destination: Point) { +// val originLocation = navigationLocationProvider.lastLocation ?: return +// val originPoint = Point.fromLngLat(originLocation.longitude, originLocation.latitude) +// +// // 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)) +// .apply { +// // provide the bearing for the origin of the request to ensure +// // that the returned route faces in the direction of the current user movement +// originLocation.bearing?.let { bearing -> +// bearingsList( +// listOf( +// Bearing.builder() +// .angle(bearing) +// .degrees(45.0) +// .build(), +// null +// ) +// ) +// } +// } +// .layersList(listOf(mapboxNavigation.getZLevel(), null)) +// .build(), +// object : NavigationRouterCallback { +// override fun onCanceled(routeOptions: RouteOptions, routerOrigin: String) { +// // no impl +// } +// +// override fun onFailure(reasons: List, routeOptions: RouteOptions) { +// // no impl +// } +// +// override fun onRoutesReady( +// routes: List, +// routerOrigin: String +// ) { +// setRouteAndStartNavigation(routes) +// } +// } +// ) +// } +// +// private fun setRouteAndStartNavigation(routes: List) { +// // set routes, where the first route in the list is the primary route that +// // will be used for active guidance +// mapboxNavigation.setNavigationRoutes(routes) +// +// // show UI elements +// binding.soundButton.visibility = View.VISIBLE +// binding.routeOverview.visibility = View.VISIBLE +// binding.tripProgressCard.visibility = View.VISIBLE +// +// // move the camera to overview when new route is available +// navigationCamera.requestNavigationCameraToOverview() +// +// // start simulation +// startSimulation(routes.first().directionsRoute) +// } +// +// private fun clearRouteAndStopNavigation() { +// // clear +// mapboxNavigation.setNavigationRoutes(listOf()) +// +// // stop simulation +// stopSimulation() +// +// // hide UI elements +// binding.soundButton.visibility = View.INVISIBLE +// binding.maneuverView.visibility = View.INVISIBLE +// binding.routeOverview.visibility = View.INVISIBLE +// binding.tripProgressCard.visibility = View.INVISIBLE +// } +// +// private fun startSimulation(route: DirectionsRoute) { +// mapboxNavigation.mapboxReplayer.stop() +// mapboxNavigation.mapboxReplayer.clearEvents() +// val replayData = replayRouteMapper.mapDirectionsRouteGeometry(route) +// mapboxNavigation.mapboxReplayer.pushEvents(replayData) +// mapboxNavigation.mapboxReplayer.seekTo(replayData[0]) +// mapboxNavigation.mapboxReplayer.play() +// } +// +// private fun stopSimulation() { +// mapboxNavigation.mapboxReplayer.stop() +// mapboxNavigation.mapboxReplayer.clearEvents() +// } +//} \ No newline at end of file diff --git a/app/src/main/java/eu/ztsh/garmin/MainActivity.kt b/app/src/main/java/eu/ztsh/garmin/MainActivity.kt index 1e39f57..540888d 100644 --- a/app/src/main/java/eu/ztsh/garmin/MainActivity.kt +++ b/app/src/main/java/eu/ztsh/garmin/MainActivity.kt @@ -10,18 +10,20 @@ import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.util.Log +import android.view.WindowManager import android.widget.Toast import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner import com.mapbox.navigation.base.options.NavigationOptions +import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver +import com.mapbox.navigation.core.lifecycle.requireMapboxNavigation import eu.ztsh.garmin.databinding.ActivityMainBinding import eu.ztsh.garmin.mapbox.MapControl -import eu.ztsh.garmin.mapbox.NavigationObserver import eu.ztsh.garmin.util.PermissionsHelper import java.lang.ref.WeakReference @@ -32,23 +34,23 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var mapControl: MapControl - private lateinit var navigationObserver: NavigationObserver - private lateinit var initThread: Thread - val permissionsHelper = PermissionsHelper(WeakReference(this)) - init { - lifecycle.addObserver(object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - MapboxNavigationApp.attach(owner) - MapboxNavigationApp.registerObserver(navigationObserver) + private val permissionsHelper = PermissionsHelper(WeakReference(this)) + + val mapboxNavigation: MapboxNavigation by requireMapboxNavigation( + onResumedObserver = object : DefaultLifecycleObserver, MapboxNavigationObserver { + override fun onAttached(mapboxNavigation: MapboxNavigation) { + mapControl.onAttached(mapboxNavigation) } - override fun onPause(owner: LifecycleOwner) { - MapboxNavigationApp.detach(owner) - MapboxNavigationApp.unregisterObserver(navigationObserver) + override fun onDetached(mapboxNavigation: MapboxNavigation) { + mapControl.onDetached(mapboxNavigation) } - }) - } + }, + onInitialize = fun() { + mapControl.initNavigation() + } + ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -56,68 +58,24 @@ class MainActivity : AppCompatActivity() { setContentView(binding.root) binding.mapView permissionsHelper.checkPermissions { - if (!MapboxNavigationApp.isSetup()) { - MapboxNavigationApp.setup { - NavigationOptions.Builder(applicationContext) - // .accessToken(BuildConfig.MAPBOX_DOWNLOADS_TOKEN) - .build() - } - } - mapControl = MapControl(binding.mapView, resources) + mapControl = MapControl(this, UI(binding), resources) mapControl.init() - navigationObserver = NavigationObserver(mapControl) + + MapboxNavigationApp.setup( + NavigationOptions.Builder(applicationContext) + .build() + ) } - initThread = Thread { - while (true) { - if (MapboxNavigationApp.current() != null) { - MapboxNavigationApp.current()!!.startTripSession() - threadCallback() - } - Thread.sleep(100) - } - } - initThread.start() bluetoothInit() - } - - private fun threadCallback() { - initThread.join() - } - - override fun onStart() { - super.onStart() -// MapboxNavigationApp.current()?.registerRouteProgressObserver(routeProgressObserver) - } - - override fun onStop() { - super.onStop() -// MapboxNavigationApp.current()?.unregisterRouteProgressObserver(routeProgressObserver) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } override fun onDestroy() { super.onDestroy() - MapboxNavigationApp.current()?.stopTripSession() -// maneuverApi.cancel() + mapControl.onDestroy() + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } -// // Define distance formatter options -// private val distanceFormatter: DistanceFormatterOptions by lazy { -// DistanceFormatterOptions.Builder(this).build() -// } -// // Create an instance of the Maneuver API -// private val maneuverApi: MapboxManeuverApi by lazy { -// MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatter)) -// } -// -// private val routeProgressObserver = -// RouteProgressObserver { routeProgress -> -// maneuverApi.getManeuvers(routeProgress).value?.apply { -// garmin.process( -// this[0] -// ) -// } -// } - private fun bluetoothInit() { val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java) val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter diff --git a/app/src/main/java/eu/ztsh/garmin/UI.kt b/app/src/main/java/eu/ztsh/garmin/UI.kt new file mode 100644 index 0000000..c27faff --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/UI.kt @@ -0,0 +1,59 @@ +package eu.ztsh.garmin + +import android.content.res.Resources +import com.mapbox.maps.EdgeInsets +import eu.ztsh.garmin.databinding.ActivityMainBinding + +class UI(b: ActivityMainBinding) { + + companion object { + + const val BUTTON_ANIMATION_DURATION = 1500L + + private val pixelDensity = Resources.getSystem().displayMetrics.density + val overviewPadding: EdgeInsets by lazy { + EdgeInsets( + 140.0 * pixelDensity, + 40.0 * pixelDensity, + 120.0 * pixelDensity, + 40.0 * pixelDensity + ) + } + val landscapeOverviewPadding: EdgeInsets by lazy { + EdgeInsets( + 30.0 * pixelDensity, + 380.0 * pixelDensity, + 110.0 * pixelDensity, + 20.0 * pixelDensity + ) + } + val followingPadding: EdgeInsets by lazy { + EdgeInsets( + 180.0 * pixelDensity, + 40.0 * pixelDensity, + 150.0 * pixelDensity, + 40.0 * pixelDensity + ) + } + val landscapeFollowingPadding: EdgeInsets by lazy { + EdgeInsets( + 30.0 * pixelDensity, + 380.0 * pixelDensity, + 110.0 * pixelDensity, + 40.0 * pixelDensity + ) + } + + } + + val mapView = b.mapView + val maneuverView = b.maneuverView + val tripProgressView = b.tripProgressView + val tripProgressCard = b.tripProgressCard + + val recenter = b.recenter + val soundButton = b.soundButton + val routeOverview = b.routeOverview + val stop = b.stop + +} \ No newline at end of file diff --git a/app/src/main/java/eu/ztsh/garmin/mapbox/LocationObserver.kt b/app/src/main/java/eu/ztsh/garmin/mapbox/LocationObserver.kt new file mode 100644 index 0000000..998eebe --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/mapbox/LocationObserver.kt @@ -0,0 +1,46 @@ +package eu.ztsh.garmin.mapbox + +import com.mapbox.common.location.Location +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import com.mapbox.navigation.core.trip.session.LocationObserver +import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions + +class LocationObserver(private val mapControl: MapControl) : LocationObserver { + + /** + * 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 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 + mapControl.navigationLocationProvider.changePosition( + location = enhancedLocation, + keyPoints = locationMatcherResult.keyPoints, + ) + + // update camera position to account for new location + mapControl.viewportDataSource.onLocationChanged(enhancedLocation) + mapControl.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 + mapControl.navigationCamera.requestNavigationCameraToOverview( + stateTransitionOptions = NavigationCameraTransitionOptions.Builder() + .maxDuration(0) // instant transition + .build() + ) + } + } + +} diff --git a/app/src/main/java/eu/ztsh/garmin/mapbox/MapControl.kt b/app/src/main/java/eu/ztsh/garmin/mapbox/MapControl.kt index e00814f..5ae5688 100644 --- a/app/src/main/java/eu/ztsh/garmin/mapbox/MapControl.kt +++ b/app/src/main/java/eu/ztsh/garmin/mapbox/MapControl.kt @@ -1,102 +1,153 @@ package eu.ztsh.garmin.mapbox +import android.content.res.Configuration import android.content.res.Resources -import android.util.Log -import com.mapbox.api.directions.v5.models.RouteOptions -import com.mapbox.maps.EdgeInsets -import com.mapbox.maps.MapView -import com.mapbox.maps.Style -import com.mapbox.maps.plugin.gestures.gestures +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import com.mapbox.maps.ImageHolder +import com.mapbox.maps.plugin.LocationPuck2D +import com.mapbox.maps.plugin.animation.camera import com.mapbox.maps.plugin.locationcomponent.location -import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions -import com.mapbox.maps.plugin.viewport.state.FollowPuckViewportState -import com.mapbox.maps.plugin.viewport.viewport -import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions -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.base.options.NavigationOptions +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.trip.session.LocationObserver +import com.mapbox.navigation.core.trip.session.RouteProgressObserver +import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver +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.location.NavigationLocationProvider -import com.mapbox.navigation.utils.internal.toPoint +import eu.ztsh.garmin.MainActivity +import eu.ztsh.garmin.UI +import eu.ztsh.garmin.mock.ReplayResources -class MapControl(val mapView: MapView, private val resources: Resources) { +class MapControl( + val context: AppCompatActivity, + val ui: UI, + private val resources: Resources +) : MapboxNavigationObserver { + /** + * 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. + */ + lateinit var navigationCamera: NavigationCamera + + /** + * Produces the camera frames based on the location and routing data for the [navigationCamera] to execute. + */ + lateinit var viewportDataSource: MapboxNavigationViewportDataSource + + /** + * [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. + */ val navigationLocationProvider = NavigationLocationProvider() - val routesRequestCallback = object : NavigationRouterCallback { - override fun onRoutesReady(routes: List, @RouterOrigin routerOrigin: String) { - MapboxNavigationApp.current()?.setNavigationRoutes(routes) - } - - override fun onFailure(reasons: List, routeOptions: RouteOptions) { - Log.e(TAG, "onFailure: ") - } - - override fun onCanceled(routeOptions: RouteOptions, @RouterOrigin routerOrigin: String) { - Log.w(TAG, "onCanceled: ") - } - } + val replay = ReplayResources(this) + // Observers + private lateinit var routeControl: RouteControl + private lateinit var voiceControl: VoiceControl + private lateinit var routesObserver: RoutesObserver + private lateinit var locationObserver: LocationObserver + private lateinit var routeProgressObserver: RouteProgressObserver + private lateinit var voiceInstructionsObserver: VoiceInstructionsObserver fun init() { - mapView.mapboxMap.loadStyle(Style.TRAFFIC_DAY) // TODO: base on sun position + viewportDataSource = MapboxNavigationViewportDataSource(ui.mapView.mapboxMap) + if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + viewportDataSource.overviewPadding = UI.landscapeOverviewPadding + } else { + viewportDataSource.overviewPadding = UI.overviewPadding + } + if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + viewportDataSource.followingPadding = UI.landscapeFollowingPadding + } else { + viewportDataSource.followingPadding = UI.followingPadding + } - mapView.location.apply { -// locationProvider = this.getLocationProvider() -// setLocationProvider(navigationLocationProvider) + navigationCamera = NavigationCamera( + ui.mapView.mapboxMap, + ui.mapView.camera, + viewportDataSource + ) + // set the animations lifecycle listener to ensure the NavigationCamera stops + // automatically following the user location when the map is interacted with + ui.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 -> ui.recenter.visibility = View.INVISIBLE + + NavigationCameraState.TRANSITION_TO_OVERVIEW, + NavigationCameraState.OVERVIEW, + NavigationCameraState.IDLE -> ui.recenter.visibility = View.VISIBLE + } + } + + routeControl = RouteControl(this, ui, context) + voiceControl = VoiceControl(ui, context) + + routesObserver = routeControl.routesObserver + locationObserver = LocationObserver(this) + routeProgressObserver = routeControl.routeProgressObserver + voiceInstructionsObserver = voiceControl.voiceInstructionsObserver + } + + fun initNavigation() { + MapboxNavigationApp.setup( + NavigationOptions.Builder(context) + .build() + ) + + // initialize location puck + ui.mapView.location.apply { + setLocationProvider(navigationLocationProvider) + this.locationPuck = LocationPuck2D( + bearingImage = ImageHolder.Companion.from( + com.mapbox.navigation.ui.maps.R.drawable.mapbox_navigation_puck_icon + ) + ) puckBearingEnabled = true enabled = true } - follow(true) - setGestures(mapView) + replay.replayOriginLocation() } - fun follow(immediately: Boolean = false) { - mapView.viewport.apply { - // transition to followPuckViewportState with default transition - val followPuckViewportState: FollowPuckViewportState = this.makeFollowPuckViewportState( - FollowPuckViewportStateOptions.Builder() -// .bearing(FollowPuckViewportStateBearing.Constant(0.0)) - .padding(EdgeInsets(200.0 * resources.displayMetrics.density, 0.0, 0.0, 0.0)) - .build() - ) - if (immediately) { - val immediateTransition = this.makeImmediateViewportTransition() - this.transitionTo(followPuckViewportState, immediateTransition) { success -> - Log.d(TAG, "follow: $success") - } - } else { - this.transitionTo(followPuckViewportState) { success -> - Log.d(TAG, "follow: $success") - } - } - } + fun mapboxNavigation(): MapboxNavigation { + return (context as MainActivity).mapboxNavigation } - private fun setGestures(mapView: MapView) { - mapView.gestures.apply { - addOnMapClickListener { point -> - mapView.location.isLocatedAt(point) { isPuckLocatedAtPoint -> - if (isPuckLocatedAtPoint) { - follow() - } - } - true - } - addOnMapLongClickListener { point -> - MapboxNavigationApp.current()?.requestRoutes( - RouteOptions.builder() - .applyDefaultNavigationOptions() - .coordinatesList(mutableListOf(navigationLocationProvider.lastLocation!!.toPoint(), point)) - .build(), - routesRequestCallback - ) - true - } - } + override fun onAttached(mapboxNavigation: MapboxNavigation) { + mapboxNavigation.registerRoutesObserver(routesObserver) + mapboxNavigation.registerLocationObserver(locationObserver) + mapboxNavigation.registerRouteProgressObserver(routeProgressObserver) + mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver) + + replay.onAttached(mapboxNavigation) + } + + override fun onDetached(mapboxNavigation: MapboxNavigation) { + mapboxNavigation.unregisterRoutesObserver(routesObserver) + mapboxNavigation.unregisterLocationObserver(locationObserver) + mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver) + mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver) + replay.onDetached(mapboxNavigation) + } + + fun onDestroy() { + routeControl.cancel() + voiceControl.cancel() } companion object { diff --git a/app/src/main/java/eu/ztsh/garmin/mapbox/NavigationObserver.kt b/app/src/main/java/eu/ztsh/garmin/mapbox/NavigationObserver.kt deleted file mode 100644 index 8d892ab..0000000 --- a/app/src/main/java/eu/ztsh/garmin/mapbox/NavigationObserver.kt +++ /dev/null @@ -1,46 +0,0 @@ -package eu.ztsh.garmin.mapbox - -import android.util.Log -import com.mapbox.common.location.Location -import com.mapbox.navigation.core.MapboxNavigation -import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver -import com.mapbox.navigation.core.trip.session.LocationMatcherResult -import com.mapbox.navigation.core.trip.session.LocationObserver -import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource - -class NavigationObserver(private val mapControl: MapControl) : MapboxNavigationObserver { - - private val viewportDataSource = MapboxNavigationViewportDataSource(mapControl.mapView.mapboxMap) - - private val locationObserver = object : LocationObserver { - - override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) { - mapControl.navigationLocationProvider.changePosition( - location = locationMatcherResult.enhancedLocation, - keyPoints = locationMatcherResult.keyPoints - ) - viewportDataSource.onLocationChanged(locationMatcherResult.enhancedLocation) - viewportDataSource.evaluate() - } - - override fun onNewRawLocation(rawLocation: Location) { - println() - } - - } - - override fun onAttached(mapboxNavigation: MapboxNavigation) { - mapboxNavigation.registerLocationObserver(locationObserver) - Log.d(TAG, "Attached") - } - - override fun onDetached(mapboxNavigation: MapboxNavigation) { - mapboxNavigation.unregisterLocationObserver(locationObserver) - Log.d(TAG, "Detached") - } - - companion object { - const val TAG = "MBOXOBS" - } - -} diff --git a/app/src/main/java/eu/ztsh/garmin/mapbox/RouteControl.kt b/app/src/main/java/eu/ztsh/garmin/mapbox/RouteControl.kt new file mode 100644 index 0000000..cf59579 --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/mapbox/RouteControl.kt @@ -0,0 +1,306 @@ +package eu.ztsh.garmin.mapbox + +import android.content.Context +import android.view.View +import android.widget.Toast +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.plugin.gestures.gestures +import com.mapbox.navigation.base.TimeFormat +import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions +import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions +import com.mapbox.navigation.base.formatter.DistanceFormatterOptions +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.core.MapboxNavigation +import com.mapbox.navigation.core.directions.session.RoutesObserver +import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter +import com.mapbox.navigation.core.trip.session.RouteProgressObserver +import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi +import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi +import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter +import com.mapbox.navigation.tripdata.progress.model.EstimatedTimeToArrivalFormatter +import com.mapbox.navigation.tripdata.progress.model.PercentDistanceTraveledFormatter +import com.mapbox.navigation.tripdata.progress.model.TimeRemainingFormatter +import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateFormatter +import com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView +import com.mapbox.navigation.ui.components.tripprogress.view.MapboxTripProgressView +import com.mapbox.navigation.ui.maps.NavigationStyles +import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowApi +import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView +import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions +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 eu.ztsh.garmin.UI + +class RouteControl(private val mapControl: MapControl, ui: UI, private val context: Context) { + + /** + * Generates updates for the [MapboxManeuverView] to display the upcoming maneuver instructions + * and remaining distance to the maneuver point. + */ + private lateinit var maneuverApi: MapboxManeuverApi + + /** + * Generates updates for the [MapboxTripProgressView] that include remaining time and distance to the destination. + */ + private lateinit var tripProgressApi: MapboxTripProgressApi + + /** + * 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 + + /** + * Generates updates for the [routeArrowView] with the geometries and properties of maneuver arrows that should be drawn on the map. + */ + private val routeArrowApi: MapboxRouteArrowApi = MapboxRouteArrowApi() + + /** + * Draws maneuver arrows on the map based on the data [routeArrowApi]. + */ + private lateinit var routeArrowView: MapboxRouteArrowView + + init { + // make sure to use the same DistanceFormatterOptions across different features + val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build() + maneuverApi = MapboxManeuverApi( + MapboxDistanceFormatter(distanceFormatterOptions) + ) + + // initialize bottom progress view + tripProgressApi = MapboxTripProgressApi( + TripProgressUpdateFormatter.Builder(context) + .distanceRemainingFormatter( + DistanceRemainingFormatter(distanceFormatterOptions) + ) + .timeRemainingFormatter( + TimeRemainingFormatter(context) + ) + .percentRouteTraveledFormatter( + PercentDistanceTraveledFormatter() + ) + .estimatedTimeToArrivalFormatter( + EstimatedTimeToArrivalFormatter(context, TimeFormat.NONE_SPECIFIED) + ) + .build() + ) + // 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 mapboxRouteLineViewOptions = MapboxRouteLineViewOptions.Builder(context) + .routeLineBelowLayerId("road-label-navigation") + .build() + + routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build()) + routeLineView = MapboxRouteLineView(mapboxRouteLineViewOptions) + + // initialize maneuver arrow view to draw arrows on the map + val routeArrowOptions = RouteArrowOptions.Builder(context).build() + routeArrowView = MapboxRouteArrowView(routeArrowOptions) + + // load map style + ui.mapView.mapboxMap.loadStyle(NavigationStyles.NAVIGATION_DAY_STYLE) { + // Ensure that the route line related layers are present before the route arrow + routeLineView.initializeLayers(it) + + // add long click listener that search for a route to the clicked destination + ui.mapView.gestures.addOnMapLongClickListener { point -> + findRoute(point) + true + } + } + + // initialize view interactions + ui.stop.setOnClickListener { + clearRouteAndStopNavigation() + } + ui.recenter.setOnClickListener { + mapControl.navigationCamera.requestNavigationCameraToFollowing() + ui.routeOverview.showTextAndExtend(UI.BUTTON_ANIMATION_DURATION) + } + ui.routeOverview.setOnClickListener { + mapControl.navigationCamera.requestNavigationCameraToOverview() + ui.recenter.showTextAndExtend(UI.BUTTON_ANIMATION_DURATION) + } + + // set initial sounds button state + ui.soundButton.unmute() + } + + /** + * Gets notified with progress along the currently active route. + */ + val routeProgressObserver = RouteProgressObserver { routeProgress -> + // update the camera position to account for the progressed fragment of the route + mapControl.viewportDataSource.onRouteProgressChanged(routeProgress) + mapControl.viewportDataSource.evaluate() + + // draw the upcoming maneuver arrow on the map + val style = mapControl.ui.mapView.mapboxMap.style + if (style != null) { + val maneuverArrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress) + routeArrowView.renderManeuverUpdate(style, maneuverArrowResult) + } + + // update top banner with maneuver instructions + val maneuvers = maneuverApi.getManeuvers(routeProgress) + maneuvers.fold( + { error -> + Toast.makeText( + mapControl.context, + error.errorMessage, + Toast.LENGTH_SHORT + ).show() + }, + { + mapControl.ui.maneuverView.visibility = View.VISIBLE + mapControl.ui.maneuverView.renderManeuvers(maneuvers) + } + ) + + // update bottom trip progress summary + mapControl.ui.tripProgressView.render( + tripProgressApi.getTripProgress(routeProgress) + ) + } + + /** + * 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 + */ + val routesObserver = RoutesObserver { routeUpdateResult -> + if (routeUpdateResult.navigationRoutes.isNotEmpty()) { + // generate route geometries asynchronously and render them + routeLineApi.setNavigationRoutes( + routeUpdateResult.navigationRoutes + ) { value -> + mapControl.ui.mapView.mapboxMap.style?.apply { + routeLineView.renderRouteDrawData(this, value) + } + } + + // update the camera position to account for the new route + mapControl.viewportDataSource.onRouteChanged(routeUpdateResult.navigationRoutes.first()) + mapControl.viewportDataSource.evaluate() + } else { + // remove the route line and route arrow from the map + val style = mapControl.ui.mapView.mapboxMap.style + if (style != null) { + routeLineApi.clearRouteLine { value -> + routeLineView.renderClearRouteLineValue( + style, + value + ) + } + routeArrowView.render(style, routeArrowApi.clearArrows()) + } + + // remove the route reference from camera position evaluations + mapControl.viewportDataSource.clearRouteData() + mapControl.viewportDataSource.evaluate() + } + } + + private fun findRoute(destination: Point) { + val originLocation = mapControl.navigationLocationProvider.lastLocation ?: return + val originPoint = Point.fromLngLat(originLocation.longitude, originLocation.latitude) + + // 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 + mapControl.mapboxNavigation().requestRoutes( + RouteOptions.builder() + .applyDefaultNavigationOptions() + .applyLanguageAndVoiceUnitOptions(context) + .coordinatesList(listOf(originPoint, destination)) + .apply { + // provide the bearing for the origin of the request to ensure + // that the returned route faces in the direction of the current user movement + originLocation.bearing?.let { bearing -> + bearingsList( + listOf( + Bearing.builder() + .angle(bearing) + .degrees(45.0) + .build(), + null + ) + ) + } + } + .layersList(listOf(mapControl.mapboxNavigation().getZLevel(), null)) + .build(), + object : NavigationRouterCallback { + override fun onCanceled(routeOptions: RouteOptions, routerOrigin: String) { + // no impl + } + + override fun onFailure(reasons: List, routeOptions: RouteOptions) { + // no impl + } + + override fun onRoutesReady( + routes: List, + routerOrigin: String + ) { + setRouteAndStartNavigation(routes) + } + } + ) + } + + private fun setRouteAndStartNavigation(routes: List) { + // set routes, where the first route in the list is the primary route that + // will be used for active guidance + mapControl.mapboxNavigation().setNavigationRoutes(routes) + + // show UI elements + mapControl.ui.soundButton.visibility = View.VISIBLE + mapControl.ui.routeOverview.visibility = View.VISIBLE + mapControl.ui.tripProgressCard.visibility = View.VISIBLE + + // move the camera to overview when new route is available + mapControl.navigationCamera.requestNavigationCameraToOverview() + + // start simulation + mapControl.replay.startSimulation(routes.first().directionsRoute) + } + + private fun clearRouteAndStopNavigation() { + // clear + mapControl.mapboxNavigation().setNavigationRoutes(listOf()) + + // stop simulation + mapControl.replay.stopSimulation() + + // hide UI elements + mapControl.ui.soundButton.visibility = View.INVISIBLE + mapControl.ui.maneuverView.visibility = View.INVISIBLE + mapControl.ui.routeOverview.visibility = View.INVISIBLE + mapControl.ui.tripProgressCard.visibility = View.INVISIBLE + } + + fun cancel() { + maneuverApi.cancel() + routeLineApi.cancel() + routeLineView.cancel() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt b/app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt new file mode 100644 index 0000000..aa9578f --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt @@ -0,0 +1,111 @@ +package eu.ztsh.garmin.mapbox + +import android.content.Context +import com.mapbox.bindgen.Expected +import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver +import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer +import com.mapbox.navigation.voice.api.MapboxSpeechApi +import com.mapbox.navigation.voice.api.MapboxVoiceInstructionsPlayer +import com.mapbox.navigation.voice.model.SpeechAnnouncement +import com.mapbox.navigation.voice.model.SpeechError +import com.mapbox.navigation.voice.model.SpeechValue +import com.mapbox.navigation.voice.model.SpeechVolume +import eu.ztsh.garmin.UI +import java.util.* + +class VoiceControl(private val ui: UI, context: Context) { + + /** + * Extracts message that should be communicated to the driver about the upcoming maneuver. + * When possible, downloads a synthesized audio file that can be played back to the driver. + */ + private lateinit var speechApi: MapboxSpeechApi + + /** + * Plays the synthesized audio files with upcoming maneuver instructions + * or uses an on-device Text-To-Speech engine to communicate the message to the driver. + * NOTE: do not use lazy initialization for this class since it takes some time to initialize + * the system services required for on-device speech synthesis. With lazy initialization + * there is a high risk that said services will not be available when the first instruction + * has to be played. [MapboxVoiceInstructionsPlayer] should be instantiated in + * `Activity#onCreate`. + */ + private lateinit var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer + + init { + speechApi = MapboxSpeechApi( + context, + Locale("pl").language + ) + voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer( + context, + Locale("pl").language + ) + ui.soundButton.setOnClickListener { + // mute/unmute voice instructions + isVoiceInstructionsMuted = !isVoiceInstructionsMuted + } + } + + /** + * Stores and updates the state of whether the voice instructions should be played as they come or muted. + */ + private var isVoiceInstructionsMuted = false + set(value) { + field = value + if (value) { + ui.soundButton.muteAndExtend(UI.BUTTON_ANIMATION_DURATION) + voiceInstructionsPlayer.volume(SpeechVolume(0f)) + } else { + ui.soundButton.unmuteAndExtend(UI.BUTTON_ANIMATION_DURATION) + voiceInstructionsPlayer.volume(SpeechVolume(1f)) + } + } + + /** + * Observes when a new voice instruction should be played. + */ + val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions -> + speechApi.generate(voiceInstructions, speechCallback) + } + + /** + * Based on whether the synthesized audio file is available, the callback plays the file + * or uses the fall back which is played back using the on-device Text-To-Speech engine. + */ + private val speechCallback = + MapboxNavigationConsumer> { expected -> + expected.fold( + { error -> + // play the instruction via fallback text-to-speech engine + voiceInstructionsPlayer.play( + error.fallback, + voiceInstructionsPlayerCallback + ) + }, + { value -> + // play the sound file from the external generator + voiceInstructionsPlayer.play( + value.announcement, + voiceInstructionsPlayerCallback + ) + } + ) + } + + /** + * When a synthesized audio file was downloaded, this callback cleans up the disk after it was played. + */ + private val voiceInstructionsPlayerCallback = + MapboxNavigationConsumer { value -> + // remove already consumed file to free-up space + speechApi.clean(value) + } + + fun cancel() { + speechApi.cancel() + voiceInstructionsPlayer.shutdown() + + } + +} diff --git a/app/src/main/java/eu/ztsh/garmin/mock/ReplayResources.kt b/app/src/main/java/eu/ztsh/garmin/mock/ReplayResources.kt new file mode 100644 index 0000000..25c4e40 --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/mock/ReplayResources.kt @@ -0,0 +1,73 @@ +package eu.ztsh.garmin.mock + +import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.replay.route.ReplayProgressObserver +import com.mapbox.navigation.core.replay.route.ReplayRouteMapper +import eu.ztsh.garmin.mapbox.MapControl +import java.util.* + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +class ReplayResources(private val mapControl: MapControl) { + + /** + * Debug observer that makes sure the replayer has always an up-to-date information to generate mock updates. + */ + private lateinit var replayProgressObserver: ReplayProgressObserver + + /** + * Debug object that converts a route into events that can be replayed to navigate a route. + */ + private val replayRouteMapper = ReplayRouteMapper() + + fun replayOriginLocation() { + with(mapControl.mapboxNavigation().mapboxReplayer) { + play() + pushEvents( + listOf( + ReplayRouteMapper.mapToUpdateLocation( + Date().time.toDouble(), + com.mapbox.geojson.Point.fromLngLat(-122.39726512303575, 37.785128345296805) + ) + ) + ) + playFirstLocation() + } + } + + fun startSimulation(route: DirectionsRoute) { + with(mapControl.mapboxNavigation()) { + mapboxReplayer.stop() + mapboxReplayer.clearEvents() + val replayData = replayRouteMapper.mapDirectionsRouteGeometry(route) + mapboxReplayer.pushEvents(replayData) + mapboxReplayer.seekTo(replayData[0]) + mapboxReplayer.play() + } + } + + fun stopSimulation() { + with(mapControl.mapboxNavigation()) { + mapboxReplayer.stop() + mapboxReplayer.clearEvents() + } + } + + fun onAttached(mapboxNavigation: MapboxNavigation) { + 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() + } + + fun onDetached(mapboxNavigation: MapboxNavigation) { + mapboxNavigation.unregisterRouteProgressObserver(replayProgressObserver) + mapboxNavigation.mapboxReplayer.finish() + } + +} \ No newline at end of file