feat!: Application outline

This commit is contained in:
Piotr Dec 2024-07-23 00:55:59 +02:00
parent 80dfa6ab4e
commit 01fbf32042
Signed by: stawros
GPG key ID: F89F27AD8F881A91
9 changed files with 1440 additions and 190 deletions

View file

@ -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
// *
// * <?xml version="1.0" encoding="utf-8"?>
// * <resources xmlns:tools="http://schemas.android.com/tools">
// * <string name="mapbox_access_token"><PUT_YOUR_ACCESS_TOKEN_HERE></string>
// * </resources>
// *
// * The example assumes that you have granted location permissions and does not enforce it. However,
// * the permission is essential for proper functioning of this example. The example also uses replay
// * location engine to facilitate navigation without actually physically moving.
// *
// * How to use this example:
// * - 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<SpeechError, SpeechValue>> { 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<SpeechAnnouncement> { 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<RouterFailure>, routeOptions: RouteOptions) {
// // no impl
// }
//
// override fun onRoutesReady(
// routes: List<NavigationRoute>,
// routerOrigin: String
// ) {
// setRouteAndStartNavigation(routes)
// }
// }
// )
// }
//
// private fun setRouteAndStartNavigation(routes: List<NavigationRoute>) {
// // 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()
// }
//}

View file

@ -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

View file

@ -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
}

View file

@ -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()
)
}
}
}

View file

@ -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<NavigationRoute>, @RouterOrigin routerOrigin: String) {
MapboxNavigationApp.current()?.setNavigationRoutes(routes)
}
override fun onFailure(reasons: List<RouterFailure>, 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 {

View file

@ -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"
}
}

View file

@ -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<RouterFailure>, routeOptions: RouteOptions) {
// no impl
}
override fun onRoutesReady(
routes: List<NavigationRoute>,
routerOrigin: String
) {
setRouteAndStartNavigation(routes)
}
}
)
}
private fun setRouteAndStartNavigation(routes: List<NavigationRoute>) {
// 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()
}
}

View file

@ -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<SpeechError, SpeechValue>> { 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<SpeechAnnouncement> { value ->
// remove already consumed file to free-up space
speechApi.clean(value)
}
fun cancel() {
speechApi.cancel()
voiceInstructionsPlayer.shutdown()
}
}

View file

@ -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()
}
}