From 6f301366b236e73f3805f93b95551262550b6bf2 Mon Sep 17 00:00:00 2001 From: Kyle Madsen <> Date: Mon, 13 Apr 2020 19:21:30 -0700 Subject: [PATCH 1/2] Replay route with history player --- .../examples/core/ReplayActivity.kt | 54 +++----- .../core/replay/history/ReplayRouteMapper.kt | 115 ++++++++++++++++++ 2 files changed, 134 insertions(+), 35 deletions(-) create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/history/ReplayRouteMapper.kt diff --git a/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt b/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt index e1bc8832a3f..7e33bf351c2 100644 --- a/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt +++ b/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt @@ -6,10 +6,8 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT -import com.mapbox.android.core.location.LocationEngine import com.mapbox.android.core.location.LocationEngineCallback import com.mapbox.android.core.location.LocationEngineProvider -import com.mapbox.android.core.location.LocationEngineRequest import com.mapbox.android.core.location.LocationEngineResult import com.mapbox.api.directions.v5.DirectionsCriteria import com.mapbox.api.directions.v5.models.DirectionsRoute @@ -23,7 +21,9 @@ import com.mapbox.navigation.base.extensions.applyDefaultParams import com.mapbox.navigation.base.extensions.coordinates import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.directions.session.RoutesRequestCallback -import com.mapbox.navigation.core.replay.route.ReplayRouteLocationEngine +import com.mapbox.navigation.core.replay.history.ReplayHistoryLocationEngine +import com.mapbox.navigation.core.replay.history.ReplayHistoryPlayer +import com.mapbox.navigation.core.replay.history.ReplayRouteMapper import com.mapbox.navigation.examples.R import com.mapbox.navigation.examples.utils.Utils import com.mapbox.navigation.examples.utils.extensions.toPoint @@ -39,17 +39,14 @@ import timber.log.Timber */ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { - companion object { - const val DEFAULT_INTERVAL_IN_MILLISECONDS = 1000L - const val DEFAULT_MAX_WAIT_TIME = DEFAULT_INTERVAL_IN_MILLISECONDS * 5 - } - - private var mapboxMap: MapboxMap? = null - private var locationEngine: LocationEngine? = null + internal var mapboxMap: MapboxMap? = null private var mapboxNavigation: MapboxNavigation? = null private var navigationMapboxMap: NavigationMapboxMap? = null private var mapInstanceState: NavigationMapboxMapInstanceState? = null - private val replayRouteLocationEngine = ReplayRouteLocationEngine() + private val firstLocationCallback = FirstLocationCallback(this) + + private val replayRouteMapper = ReplayRouteMapper() + private val replayHistoryPlayer = ReplayHistoryPlayer() @SuppressLint("MissingPermission") override fun onCreate(savedInstanceState: Bundle?) { @@ -66,7 +63,7 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { applicationContext, Utils.getMapboxAccessToken(this), mapboxNavigationOptions, - locationEngine = replayRouteLocationEngine + locationEngine = ReplayHistoryLocationEngine(replayHistoryPlayer) ) initListeners() mapView.getMapAsync(this) @@ -82,7 +79,7 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { mapInstanceState?.let { state -> navigationMapboxMap?.restoreFrom(state) } - initLocationEngine() + initializeFirstLocation() } mapboxMap.addOnMapLongClickListener { latLng -> mapboxMap.locationComponent.lastKnownLocation?.let { originLocation -> @@ -100,21 +97,11 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { } } - fun initLocationEngine() { - val requestLocationUpdateRequest = - LocationEngineRequest.Builder(DEFAULT_INTERVAL_IN_MILLISECONDS) - .setPriority(LocationEngineRequest.PRIORITY_NO_POWER) - .setMaxWaitTime(DEFAULT_MAX_WAIT_TIME) - .build() - - locationEngine?.requestLocationUpdates( - requestLocationUpdateRequest, - locationListenerCallback, - mainLooper - ) + private fun initializeFirstLocation() { // Center the map at current location. Using LocationEngineProvider because the // replay engine won't have your last location. - LocationEngineProvider.getBestLocationEngine(this).getLastLocation(locationListenerCallback) + LocationEngineProvider.getBestLocationEngine(this) + .getLastLocation(firstLocationCallback) } private val routesReqCallback = object : RoutesRequestCallback { @@ -122,7 +109,10 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { Timber.d("route request success %s", routes.toString()) if (routes.isNotEmpty()) { navigationMapboxMap?.drawRoute(routes[0]) - replayRouteLocationEngine.assign(routes[0]) + + val updateLocations = replayRouteMapper.mapToUpdateLocations(routes[0]) + replayHistoryPlayer.pushEvents(updateLocations) + replayHistoryPlayer.seekTo(updateLocations.first()) startNavigation.visibility = View.VISIBLE } else { startNavigation.visibility = View.GONE @@ -149,6 +139,7 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { } mapboxNavigation?.startTripSession() startNavigation.visibility = View.GONE + replayHistoryPlayer.play(this) } } @@ -170,7 +161,6 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { override fun onStop() { super.onStop() - stopLocationUpdates() navigationMapboxMap?.onStop() mapView.onStop() } @@ -187,9 +177,7 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { mapView.onLowMemory() } - private val locationListenerCallback = MyLocationEngineCallback(this) - - private class MyLocationEngineCallback(activity: ReplayActivity) : + private class FirstLocationCallback(activity: ReplayActivity) : LocationEngineCallback { private val activityRef = WeakReference(activity) @@ -203,8 +191,4 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { override fun onFailure(exception: Exception) { } } - - private fun stopLocationUpdates() { - mapboxNavigation?.locationEngine?.removeLocationUpdates(locationListenerCallback) - } } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/history/ReplayRouteMapper.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/history/ReplayRouteMapper.kt new file mode 100644 index 00000000000..547a9d9aa23 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/history/ReplayRouteMapper.kt @@ -0,0 +1,115 @@ +package com.mapbox.navigation.core.replay.history + +import android.location.Location +import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.geojson.LineString +import com.mapbox.geojson.Point +import com.mapbox.turf.TurfConstants +import com.mapbox.turf.TurfMeasurement +import java.util.Collections + +/** + * This class converts a directions rout into events that can be + * replayed using the [ReplayHistoryPlayer] to navigate a route. + */ +class ReplayRouteMapper( + /** + * Determines the spacing of the replay locations by kilometers per hour + */ + private var speedKph: Int = DEFAULT_REPLAY_SPEED_KPH +) { + private val distancePerSecondMps by lazy { + val oneKmInMeters = 1000.0 + val oneHourInSeconds = 3600 + speedKph.toDouble() * oneKmInMeters / oneHourInSeconds + } + + /** + * Map a directions route into replay events. + * + * @param directionsRoute that is converted in + * @return replay events for the [ReplayHistoryPlayer] + */ + fun mapToUpdateLocations(directionsRoute: DirectionsRoute): List { + val geometry = directionsRoute.geometry() ?: return emptyList() + val lineString = LineString.fromPolyline(geometry, 6) ?: return emptyList() + val startTime = 0.0 + val updateLocationEvents = mutableListOf() + var lastPoint = lineString.coordinates().first() + sliceRoute(lineString).fold(startTime) { replayTimeSecond, point -> + val distance = TurfMeasurement.distance(lastPoint, point, TurfConstants.UNIT_METERS) + val bearing = if (distance < 0.1) null else TurfMeasurement.bearing(lastPoint, point) + val deltaSeconds = 1.0 + val speed = if (distance < 0.1) 0.0 else distance / deltaSeconds + val replayAtTimeSecond = replayTimeSecond + deltaSeconds + val updateLocationEvent = ReplayEventUpdateLocation( + eventTimestamp = replayAtTimeSecond, + location = ReplayEventLocation( + lon = point.longitude(), + lat = point.latitude(), + provider = LOCATION_PROVIDER_REPLAY_ROUTE, + time = replayAtTimeSecond, + altitude = null, + accuracyHorizontal = null, + bearing = bearing, + speed = speed + ) + ) + updateLocationEvents.add(updateLocationEvent) + lastPoint = point + replayAtTimeSecond + } + + return updateLocationEvents.toList() + } + + // Evenly distributes the route into points that assume a constant speed throughout the route. + // This method causes known issues because actual navigation does not have constant speed. + // Alternatives for varying speed depending on maneuvers is not yet supported. + private fun sliceRoute(lineString: LineString): List { + val distanceMeters = TurfMeasurement.length(lineString, TurfConstants.UNIT_METERS) + if (distanceMeters <= 0) { + return emptyList() + } + + val points = ArrayList() + var i = 0.0 + while (i < distanceMeters) { + val point = TurfMeasurement.along(lineString, i, TurfConstants.UNIT_METERS) + points.add(point) + i += distancePerSecondMps + } + return points + } + + /** + * Map a Android location into a replay event. + * + * @param location Android location to be replayed + * @return a singleton list to be replayed, otherwise an empty list if the location cannot be replayed. + */ + fun mapToUpdateLocation(location: Location?): List { + location ?: return emptyList() + val replayAtTimeMillis = 0 + val replayAtTimeSecond = replayAtTimeMillis * 1e-4 + val updateLocationEvent = ReplayEventUpdateLocation( + eventTimestamp = replayAtTimeSecond, + location = ReplayEventLocation( + lat = location.longitude, + lon = location.latitude, + provider = LOCATION_PROVIDER_REPLAY_ROUTE, + time = replayAtTimeSecond, + altitude = location.altitude, + accuracyHorizontal = if (location.hasAccuracy()) location.accuracy.toDouble() else null, + bearing = if (location.hasBearing()) location.bearing.toDouble() else null, + speed = if (location.hasSpeed()) location.speed.toDouble() else null + ) + ) + return Collections.singletonList(updateLocationEvent) + } + + companion object { + private const val DEFAULT_REPLAY_SPEED_KPH = 45 + private const val LOCATION_PROVIDER_REPLAY_ROUTE = "ReplayRouteMapper" + } +} From bda5f74e0ceaefbb1ba6ceec7ad0934c8cf8a3dd Mon Sep 17 00:00:00 2001 From: Kyle Madsen Date: Fri, 17 Apr 2020 10:22:20 -0700 Subject: [PATCH 2/2] Update examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt Co-Authored-By: Pablo Guardiola --- .../java/com/mapbox/navigation/examples/core/ReplayActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt b/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt index 7e33bf351c2..da5626d7baf 100644 --- a/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt +++ b/examples/src/main/java/com/mapbox/navigation/examples/core/ReplayActivity.kt @@ -39,7 +39,7 @@ import timber.log.Timber */ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback { - internal var mapboxMap: MapboxMap? = null + private var mapboxMap: MapboxMap? = null private var mapboxNavigation: MapboxNavigation? = null private var navigationMapboxMap: NavigationMapboxMap? = null private var mapInstanceState: NavigationMapboxMapInstanceState? = null