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