Skip to content

Commit a453f5a

Browse files
committed
Introduce journey abstraction.
1 parent 8e57d2b commit a453f5a

File tree

4 files changed

+267
-175
lines changed

4 files changed

+267
-175
lines changed

app/src/main/AndroidManifest.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,7 @@
11971197
</activity>
11981198

11991199
<activity
1200-
android:name=".examples.CustomLocationProviderActivity"
1200+
android:name=".examples.CustomJourneyLocationProviderActivity"
12011201
android:description="@string/description_custom_location_provider"
12021202
android:exported="true"
12031203
android:label="@string/activity_custom_location_provider">

app/src/main/java/com/mapbox/maps/testapp/examples/CustomLocationProviderActivity.kt renamed to app/src/main/java/com/mapbox/maps/testapp/examples/CustomJourneyLocationProviderActivity.kt

+10-8
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ import com.mapbox.maps.Style
1212
import com.mapbox.maps.plugin.LocationPuck3D
1313
import com.mapbox.maps.plugin.gestures.OnMapClickListener
1414
import com.mapbox.maps.plugin.gestures.gestures
15-
import com.mapbox.maps.plugin.locationcomponent.CustomLocationProvider
15+
import com.mapbox.maps.plugin.locationcomponent.CustomJourneyLocationProvider
16+
import com.mapbox.maps.plugin.locationcomponent.Journey
1617
import com.mapbox.maps.plugin.locationcomponent.location2
1718

1819
/**
1920
* Example of using custom location provider.
2021
*/
21-
class CustomLocationProviderActivity : AppCompatActivity(), OnMapClickListener {
22+
class CustomJourneyLocationProviderActivity : AppCompatActivity(), OnMapClickListener {
2223

23-
private val customLocationProvider = CustomLocationProvider()
24+
private val journey = Journey()
25+
private val customJourneyLocationProvider = CustomJourneyLocationProvider().apply { loadJourney(journey) }
2426
private lateinit var mapView: MapView
2527

2628
@SuppressLint("SetTextI18n")
@@ -36,7 +38,7 @@ class CustomLocationProviderActivity : AppCompatActivity(), OnMapClickListener {
3638
)
3739
text = "Cancel"
3840
setOnClickListener {
39-
customLocationProvider.cancelPlayback()
41+
journey.pause()
4042
}
4143
}
4244
)
@@ -52,7 +54,7 @@ class CustomLocationProviderActivity : AppCompatActivity(), OnMapClickListener {
5254
loadStyleUri(Style.MAPBOX_STREETS) {
5355
initLocationComponent()
5456
initClickListeners()
55-
customLocationProvider.startPlayback()
57+
journey.start()
5658
}
5759
}
5860
}
@@ -63,11 +65,11 @@ class CustomLocationProviderActivity : AppCompatActivity(), OnMapClickListener {
6365

6466
private fun initLocationComponent() {
6567
val locationComponentPlugin2 = mapView.location2
66-
locationComponentPlugin2.setLocationProvider(customLocationProvider)
68+
locationComponentPlugin2.setLocationProvider(customJourneyLocationProvider)
6769
locationComponentPlugin2.let {
6870
it.locationPuck = LocationPuck3D(
6971
modelUri = "asset://sportcar.glb",
70-
modelScale = listOf(0.1f, 0.1f, 0.1f),
72+
modelScale = listOf(0.5f, 0.5f, 0.5f),
7173
modelTranslation = listOf(0.1f, 0.1f, 0.1f),
7274
modelRotation = listOf(0.0f, 0.0f, 180.0f)
7375
)
@@ -77,7 +79,7 @@ class CustomLocationProviderActivity : AppCompatActivity(), OnMapClickListener {
7779
}
7880

7981
override fun onMapClick(point: Point): Boolean {
80-
customLocationProvider.queueLocationUpdate(point)
82+
journey.queueLocationUpdate(point)
8183
return true
8284
}
8385

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package com.mapbox.maps.plugin.locationcomponent
2+
3+
import android.os.Handler
4+
import android.os.Looper
5+
import android.view.animation.LinearInterpolator
6+
import com.mapbox.geojson.Point
7+
import com.mapbox.maps.MapboxExperimental
8+
import java.util.concurrent.ConcurrentLinkedQueue
9+
import java.util.concurrent.CopyOnWriteArrayList
10+
import java.util.concurrent.CopyOnWriteArraySet
11+
import kotlin.math.*
12+
13+
/**
14+
* A custom location provider implementation that allows to play location updates at constant speed.
15+
*/
16+
@MapboxExperimental
17+
class CustomJourneyLocationProvider : LocationProvider {
18+
private var locationConsumers = CopyOnWriteArraySet<LocationConsumer>()
19+
20+
fun loadJourney(journey: Journey) {
21+
journey.observeJourneyUpdates { point, bearing, locationAnimationDurationMs, bearingAnimationDurationMs ->
22+
emitLocationUpdated(point, bearing, locationAnimationDurationMs, bearingAnimationDurationMs)
23+
true
24+
}
25+
}
26+
27+
private fun emitLocationUpdated(
28+
location: Point,
29+
bearing: Double,
30+
locationAnimationDuration: Long,
31+
bearingAnimateDuration: Long,
32+
) {
33+
locationConsumers.forEach {
34+
it.onBearingUpdated(bearing) {
35+
duration = bearingAnimateDuration
36+
}
37+
it.onLocationUpdated(location) {
38+
duration = locationAnimationDuration
39+
interpolator = LinearInterpolator()
40+
}
41+
}
42+
}
43+
44+
override fun registerLocationConsumer(locationConsumer: LocationConsumer) {
45+
this.locationConsumers.add(locationConsumer)
46+
}
47+
48+
override fun unRegisterLocationConsumer(locationConsumer: LocationConsumer) {
49+
this.locationConsumers.remove(locationConsumer)
50+
}
51+
}
52+
53+
@MapboxExperimental
54+
class Journey(val speed: Double = 100.0, val angularSpeed: Double = 100.0) {
55+
private val locationList = CopyOnWriteArrayList<QueueData>()
56+
private val initialTimeStamp: Long = 0
57+
private val remainingPoints = ConcurrentLinkedQueue<QueueData>()
58+
private var isPlaying = false
59+
private val handler = Handler(Looper.getMainLooper())
60+
61+
private val observers = CopyOnWriteArraySet<JourneyDataObserver>()
62+
63+
/**
64+
* Return the remaining locations in the queue.
65+
*/
66+
val remainingLocationsInQueue: List<Point>
67+
get() {
68+
with(remainingPoints) {
69+
return this.map { it.location }
70+
}
71+
}
72+
73+
fun observeJourneyUpdates(observer: JourneyDataObserver) {
74+
observers.add(observer)
75+
}
76+
77+
/**
78+
* Start the playback, any incoming location updates will be queued and played sequentially.
79+
*/
80+
fun start() {
81+
isPlaying = true
82+
drainQueue()
83+
}
84+
85+
/**
86+
* Cancel any ongoing playback, new incoming location updates will be queued but not played.
87+
*/
88+
fun pause() {
89+
isPlaying = false
90+
handler.removeCallbacksAndMessages(null)
91+
}
92+
93+
/**
94+
* Resume the remaining journey.
95+
*/
96+
fun resume() {
97+
isPlaying = true
98+
drainQueue()
99+
}
100+
101+
/**
102+
* Restart the journey.
103+
*/
104+
fun restart() {
105+
remainingPoints.clear()
106+
remainingPoints.addAll(locationList)
107+
isPlaying = true
108+
}
109+
110+
/**
111+
* Queue a new location update event to be played at constant speed.
112+
*/
113+
fun queueLocationUpdate(
114+
location: Point
115+
) {
116+
val bearing = locationList.lastOrNull()?.location?.let {
117+
bearing(it, location)
118+
} ?: 0.0
119+
val animationDurationMs = locationList.lastOrNull()?.location?.let {
120+
(distanceInMeter(it, location) / speed) * 1000.0
121+
} ?: 1000L
122+
val bearingAnimateDurationMs =
123+
abs(shortestRotation(bearing, locationList.lastOrNull()?.bearing ?: 0.0) / angularSpeed) * 1000.0
124+
125+
val nextData =
126+
QueueData(location, bearing, animationDurationMs.toLong(), bearingAnimateDurationMs.toLong())
127+
locationList.add(nextData)
128+
remainingPoints.add(nextData)
129+
if (remainingPoints.size == 1 && isPlaying) {
130+
drainQueue()
131+
}
132+
}
133+
134+
/**
135+
* Queue a list of geo locations to be played at constant speed.
136+
*/
137+
fun queueLocationUpdates(locations: List<Point>) {
138+
locations.forEach {
139+
queueLocationUpdate(it)
140+
}
141+
}
142+
143+
private fun drainQueue() {
144+
remainingPoints.peek()?.let { data ->
145+
observers.forEach {
146+
if (it.onNewData(
147+
data.location,
148+
data.bearing,
149+
data.locationAnimationDurationMs,
150+
data.bearingAnimateDurationMs
151+
)
152+
) {
153+
if (isPlaying) {
154+
handler.postDelayed(
155+
{
156+
remainingPoints.poll()
157+
drainQueue()
158+
},
159+
max(data.locationAnimationDurationMs, data.bearingAnimateDurationMs)
160+
)
161+
}
162+
} else {
163+
observers.remove(it)
164+
}
165+
}
166+
}
167+
}
168+
169+
private data class QueueData(
170+
val location: Point,
171+
val bearing: Double,
172+
val locationAnimationDurationMs: Long,
173+
val bearingAnimateDurationMs: Long
174+
)
175+
176+
private companion object {
177+
/**
178+
* Takes two [Point] and finds the geographic bearing between them.
179+
*
180+
* @param point1 first point used for calculating the bearing
181+
* @param point2 second point used for calculating the bearing
182+
* @return bearing in decimal degrees
183+
*/
184+
fun bearing(point1: Point, point2: Point): Double {
185+
val lon1: Double = degreesToRadians(point1.longitude())
186+
val lon2: Double = degreesToRadians(point2.longitude())
187+
val lat1: Double = degreesToRadians(point1.latitude())
188+
val lat2: Double = degreesToRadians(point2.latitude())
189+
val value1 = sin(lon2 - lon1) * cos(lat2)
190+
val value2 = cos(lat1) * sin(lat2) - (sin(lat1) * cos(lat2) * cos(lon2 - lon1))
191+
return radiansToDegrees(atan2(value1, value2))
192+
}
193+
194+
fun radiansToDegrees(radians: Double): Double {
195+
val degrees = radians % (2 * Math.PI)
196+
return degrees * 180 / Math.PI
197+
}
198+
199+
fun degreesToRadians(degrees: Double): Double {
200+
val radians = degrees % 360
201+
return radians * Math.PI / 180
202+
}
203+
204+
fun distanceInMeter(point1: Point, point2: Point): Double {
205+
val radius = 6370000.0
206+
val lat = degreesToRadians(point2.latitude() - point1.latitude())
207+
val lon = degreesToRadians(point2.longitude() - point1.longitude())
208+
val a = sin(lat / 2) * sin(lat / 2) + cos(degreesToRadians(point1.latitude())) * cos(
209+
degreesToRadians(point2.latitude())
210+
) * sin(lon / 2) * sin(lon / 2)
211+
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
212+
return abs(radius * c)
213+
}
214+
215+
/**
216+
* Util for finding the shortest path from the current rotated degree to the new degree.
217+
*
218+
* @param targetHeading the new position of the rotation
219+
* @param currentHeading the current position of the rotation
220+
* @return the shortest degree of rotation possible
221+
*/
222+
fun shortestRotation(targetHeading: Double, currentHeading: Double): Double {
223+
val diff = currentHeading - targetHeading
224+
return when {
225+
diff > 180.0f -> {
226+
targetHeading + 360.0f
227+
}
228+
diff < -180.0f -> {
229+
targetHeading - 360.0f
230+
}
231+
else -> {
232+
targetHeading
233+
}
234+
}
235+
}
236+
}
237+
}
238+
239+
fun interface JourneyDataObserver {
240+
/**
241+
* Notifies that new data is available.
242+
*
243+
* @param location the next location update.
244+
* @param bearing the bearing towards the next location update.
245+
* @param locationAnimationDurationMs maximum duration of the animation in ms.
246+
* @param bearingAnimateDurationMs
247+
*
248+
* @return true if new data is needed and stay subscribed. returning false will unsubscribe from further data updates.
249+
*/
250+
fun onNewData(
251+
location: Point,
252+
bearing: Double,
253+
locationAnimationDurationMs: Long,
254+
bearingAnimateDurationMs: Long
255+
): Boolean
256+
}

0 commit comments

Comments
 (0)