Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
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?) {
Expand All @@ -66,7 +63,7 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback {
applicationContext,
Utils.getMapboxAccessToken(this),
mapboxNavigationOptions,
locationEngine = replayRouteLocationEngine
locationEngine = ReplayHistoryLocationEngine(replayHistoryPlayer)
)
initListeners()
mapView.getMapAsync(this)
Expand All @@ -82,7 +79,7 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback {
mapInstanceState?.let { state ->
navigationMapboxMap?.restoreFrom(state)
}
initLocationEngine()
initializeFirstLocation()
}
mapboxMap.addOnMapLongClickListener { latLng ->
mapboxMap.locationComponent.lastKnownLocation?.let { originLocation ->
Expand All @@ -100,29 +97,22 @@ 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 {
override fun onRoutesReady(routes: List<DirectionsRoute>) {
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
Expand All @@ -149,6 +139,7 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback {
}
mapboxNavigation?.startTripSession()
startNavigation.visibility = View.GONE
replayHistoryPlayer.play(this)
}
}

Expand All @@ -170,7 +161,6 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback {

override fun onStop() {
super.onStop()
stopLocationUpdates()
navigationMapboxMap?.onStop()
mapView.onStop()
}
Expand All @@ -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<LocationEngineResult> {

private val activityRef = WeakReference(activity)
Expand All @@ -203,8 +191,4 @@ class ReplayActivity : AppCompatActivity(), OnMapReadyCallback {
override fun onFailure(exception: Exception) {
}
}

private fun stopLocationUpdates() {
mapboxNavigation?.locationEngine?.removeLocationUpdates(locationListenerCallback)
}
}
Original file line number Diff line number Diff line change
@@ -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(
Comment thread
kmadsen marked this conversation as resolved.
/**
* 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<ReplayEventBase> {
val geometry = directionsRoute.geometry() ?: return emptyList()
val lineString = LineString.fromPolyline(geometry, 6) ?: return emptyList()
val startTime = 0.0
val updateLocationEvents = mutableListOf<ReplayEventBase>()
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<Point> {
val distanceMeters = TurfMeasurement.length(lineString, TurfConstants.UNIT_METERS)
if (distanceMeters <= 0) {
return emptyList()
}

val points = ArrayList<Point>()
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<ReplayEventBase> {
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"
}
}