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