-
Notifications
You must be signed in to change notification settings - Fork 318
/
ReplayHistoryActivity.kt
355 lines (310 loc) · 13.5 KB
/
ReplayHistoryActivity.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
package com.mapbox.navigation.examples.core
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.SeekBar
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.android.core.location.LocationEngine
import com.mapbox.android.core.location.LocationEngineCallback
import com.mapbox.android.core.location.LocationEngineResult
import com.mapbox.api.directions.v5.DirectionsCriteria
import com.mapbox.api.directions.v5.models.DirectionsRoute
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.base.common.logger.model.Message
import com.mapbox.common.logger.MapboxLogger
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.location.modes.RenderMode
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.navigation.base.internal.extensions.applyDefaultParams
import com.mapbox.navigation.base.internal.extensions.coordinates
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesRequestCallback
import com.mapbox.navigation.core.fasterroute.FasterRouteObserver
import com.mapbox.navigation.core.replay.MapboxReplayer
import com.mapbox.navigation.core.replay.ReplayLocationEngine
import com.mapbox.navigation.core.replay.history.CustomEventMapper
import com.mapbox.navigation.core.replay.history.ReplayEventBase
import com.mapbox.navigation.core.replay.history.ReplayEventsObserver
import com.mapbox.navigation.core.replay.history.ReplayHistoryMapper
import com.mapbox.navigation.examples.R
import com.mapbox.navigation.examples.history.HistoryFilesActivity
import com.mapbox.navigation.examples.utils.Utils
import com.mapbox.navigation.examples.utils.extensions.toPoint
import com.mapbox.navigation.ui.camera.NavigationCamera
import com.mapbox.navigation.ui.map.NavigationMapboxMap
import java.io.IOException
import java.io.InputStream
import java.lang.ref.WeakReference
import java.nio.charset.Charset.forName
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.android.synthetic.main.activity_replay_history_layout.*
import kotlinx.android.synthetic.main.activity_trip_service.mapView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* This activity shows how to use replay ride history.
*/
class ReplayHistoryActivity : AppCompatActivity() {
private var navigationContext: ReplayNavigationContext? = null
// You choose your loading mechanism. Use Coroutines, ViewModels, RxJava, Threads, etc..
private var loadNavigationJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_replay_history_layout)
mapView.onCreate(savedInstanceState)
selectHistoryButton.setOnClickListener {
val activityIntent = Intent(this, HistoryFilesActivity::class.java)
startActivity(activityIntent)
finish()
}
getNavigationAsync {
navigationContext = it
it.onNavigationReady()
it.mapboxMap.moveCamera(CameraUpdateFactory.zoomTo(15.0))
it.navigationMapboxMap.updateCameraTrackingMode(NavigationCamera.NAVIGATION_TRACKING_MODE_GPS)
it.locationEngine.getLastLocation(FirstLocationCallback(it))
}
}
private fun getNavigationAsync(callback: (ReplayNavigationContext) -> Unit) {
loadNavigationJob = CoroutineScope(Dispatchers.Main).launch {
// Load map and style on Main dispatchers
val deferredMapboxWithStyle = async { loadMapWithStyle() }
val replayEvents = loadReplayHistory()
val mapboxReplay = MapboxReplayer()
.pushEvents(replayEvents)
if (!isActive) return@launch
val locationEngine = ReplayLocationEngine(mapboxReplay)
// Await the map and we're ready for navigation
val mapboxNavigation = createMapboxNavigation(locationEngine)
val (mapboxMap, style) = deferredMapboxWithStyle.await()
if (!isActive) return@launch
val navigationMapboxMap = NavigationMapboxMap(mapView, mapboxMap, this@ReplayHistoryActivity, true)
val navigationContext = ReplayNavigationContext(
locationEngine,
mapboxMap,
style,
mapboxNavigation,
navigationMapboxMap,
mapboxReplay
)
callback(navigationContext)
}
}
private suspend fun loadMapWithStyle(): Pair<MapboxMap, Style> = suspendCoroutine { cont ->
mapView.getMapAsync { mapboxMap ->
mapboxMap.setStyle(Style.MAPBOX_STREETS) { style ->
cont.resume(Pair(mapboxMap, style))
}
}
}
private suspend fun loadReplayHistory(): List<ReplayEventBase> = withContext(Dispatchers.IO) {
HistoryFilesActivity.selectedHistory?.let {
val replayHistoryMapper = ReplayHistoryMapper(ReplayCustomEventMapper(), MapboxLogger)
replayHistoryMapper.mapToReplayEvents(it)
} ?: loadDefaultReplayHistory()
}
private suspend fun loadDefaultReplayHistory(): List<ReplayEventBase> = withContext(Dispatchers.IO) {
val replayHistoryMapper = ReplayHistoryMapper(ReplayCustomEventMapper(), MapboxLogger)
val rideHistoryExample = loadHistoryJsonFromAssets(this@ReplayHistoryActivity, "replay-history-activity.json")
replayHistoryMapper.mapToReplayEvents(rideHistoryExample)
}
private fun createMapboxNavigation(locationEngine: LocationEngine): MapboxNavigation {
val mapboxNavigationOptions = MapboxNavigation
.defaultNavigationOptionsBuilder(this, Utils.getMapboxAccessToken(this))
.locationEngine(locationEngine)
.build()
return MapboxNavigation(mapboxNavigationOptions)
}
/**
* After the map, style, and replay history is all loaded. Connect the view.
*/
@SuppressLint("MissingPermission")
private fun ReplayNavigationContext.onNavigationReady() {
setupReplayControls()
navigationMapboxMap.addProgressChangeListener(mapboxNavigation)
mapboxReplayer.playFirstLocation()
mapboxMap.addOnMapLongClickListener { latLng ->
selectMapLocation(latLng)
true
}
mapboxReplayer.registerObserver(object : ReplayEventsObserver {
override fun replayEvents(events: List<ReplayEventBase>) {
events.forEach { event ->
when (event) {
is ReplayEventInitialRoute -> {
event.coordinates.lastOrNull()?.let { latLng ->
selectMapLocation(latLng)
}
}
}
}
}
})
mapboxNavigation.attachFasterRouteObserver(object : FasterRouteObserver {
override fun onFasterRoute(currentRoute: DirectionsRoute, alternatives: List<DirectionsRoute>, isAlternativeFaster: Boolean) {
navigationContext?.navigationMapboxMap?.drawRoutes(alternatives)
navigationContext?.mapboxNavigation?.setRoutes(alternatives)
}
})
playReplay.setOnClickListener {
mapboxReplayer.play()
mapboxNavigation.startTripSession()
}
}
private fun ReplayNavigationContext.setupReplayControls() {
seekBar.max = 8
seekBar.progress = 1
seekBarText.text = getString(R.string.replay_playback_speed_seekbar, seekBar.progress)
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
mapboxReplayer.playbackSpeed(progress.toDouble())
seekBarText.text = getString(R.string.replay_playback_speed_seekbar, progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) { }
override fun onStopTrackingTouch(seekBar: SeekBar) { }
})
}
private fun ReplayNavigationContext.selectMapLocation(latLng: LatLng) {
mapboxMap.locationComponent.lastKnownLocation?.let { originLocation ->
mapboxNavigation.requestRoutes(
RouteOptions.builder().applyDefaultParams()
.accessToken(Utils.getMapboxAccessToken(applicationContext))
.coordinates(originLocation.toPoint(), null, latLng.toPoint())
.alternatives(true)
.profile(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC)
.build(),
routesReqCallback
)
}
}
@SuppressLint("MissingPermission")
private fun ReplayNavigationContext.startNavigation() {
if (mapboxNavigation.getRoutes().isNotEmpty()) {
navigationMapboxMap.updateLocationLayerRenderMode(RenderMode.GPS)
navigationMapboxMap.updateCameraTrackingMode(NavigationCamera.NAVIGATION_TRACKING_MODE_GPS)
navigationMapboxMap.startCamera(mapboxNavigation.getRoutes()[0])
}
mapboxNavigation.startTripSession()
}
private class FirstLocationCallback(navigationContext: ReplayNavigationContext) :
LocationEngineCallback<LocationEngineResult> {
private val navigationContextRef = WeakReference(navigationContext)
override fun onSuccess(result: LocationEngineResult?) {
result?.locations?.firstOrNull()?.let {
navigationContextRef.get()?.navigationMapboxMap?.updateLocation(result.lastLocation)
}
}
override fun onFailure(exception: Exception) {
}
}
private val routesReqCallback = object : RoutesRequestCallback {
override fun onRoutesReady(routes: List<DirectionsRoute>) {
MapboxLogger.d(Message("route request success $routes"))
if (routes.isNotEmpty()) {
navigationContext?.navigationMapboxMap?.drawRoutes(routes)
navigationContext?.startNavigation()
}
}
override fun onRoutesRequestFailure(throwable: Throwable, routeOptions: RouteOptions) {
MapboxLogger.e(
Message("route request failure"),
throwable
)
}
override fun onRoutesRequestCanceled(routeOptions: RouteOptions) {
MapboxLogger.d(Message("route request canceled"))
}
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
public override fun onResume() {
super.onResume()
mapView.onResume()
}
public override fun onPause() {
super.onPause()
mapView.onPause()
}
override fun onStop() {
super.onStop()
mapView.onStop()
}
override fun onDestroy() {
super.onDestroy()
loadNavigationJob?.cancelChildren()
navigationContext?.apply {
mapboxReplayer.finish()
mapboxNavigation.stopTripSession()
mapboxNavigation.onDestroy()
}
mapView.onDestroy()
}
override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
}
private data class ReplayNavigationContext(
val locationEngine: LocationEngine,
val mapboxMap: MapboxMap,
val style: Style,
val mapboxNavigation: MapboxNavigation,
val navigationMapboxMap: NavigationMapboxMap,
val mapboxReplayer: MapboxReplayer
)
private fun loadHistoryJsonFromAssets(context: Context, fileName: String): String {
return try {
val inputStream: InputStream = context.assets.open(fileName)
val size: Int = inputStream.available()
val buffer = ByteArray(size)
inputStream.read(buffer)
inputStream.close()
String(buffer, forName("UTF-8"))
} catch (e: IOException) {
MapboxLogger.e(
Message("Your history file failed to open $fileName"),
e
)
throw e
}
}
private class ReplayCustomEventMapper : CustomEventMapper {
override fun map(eventType: String, properties: Map<*, *>): ReplayEventBase? {
return when (eventType) {
"start_transit" -> ReplayEventStartTransit(
eventTimestamp = properties["event_timestamp"] as Double,
properties = properties["properties"] as Double)
"initial_route" -> {
val eventProperties = properties["properties"] as Map<*, *>
val routeOptions = eventProperties["routeOptions"] as Map<*, *>
val coordinates = routeOptions["coordinates"] as List<List<Double>>
val coordinatesLatLng = coordinates.map { LatLng(it[1], it[0]) }
ReplayEventInitialRoute(
eventTimestamp = properties["event_timestamp"] as Double,
coordinates = coordinatesLatLng
)
}
else -> null
}
}
}
private data class ReplayEventStartTransit(
override val eventTimestamp: Double,
val properties: Double
) : ReplayEventBase
private data class ReplayEventInitialRoute(
override val eventTimestamp: Double,
val coordinates: List<LatLng>
) : ReplayEventBase