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.CopyOnWriteArraySet
10
+ import kotlin.math.*
11
+
12
+ /* *
13
+ * A custom location provider implementation that allows to play location updates at constant speed.
14
+ */
15
+ @MapboxExperimental
16
+ class CustomLocationProvider : LocationProvider {
17
+ private var locationConsumers = CopyOnWriteArraySet <LocationConsumer >()
18
+ private var bearingAnimateDuration = 100L
19
+ private var isPlaying = false
20
+ private val handler = Handler (Looper .getMainLooper())
21
+ private val locationList = ConcurrentLinkedQueue <Triple <Point , Double , Long >>()
22
+ private var lastLocation: Point ? = null
23
+
24
+ /* *
25
+ * Playback speed in m/s.
26
+ */
27
+ var speed = 60.0
28
+
29
+ /* *
30
+ * Return the remaining locations in the queue.
31
+ */
32
+ val remainingLocationsInQueue: List <Point >
33
+ get() {
34
+ with (locationList) {
35
+ return this .map { it.first }
36
+ }
37
+ }
38
+
39
+
40
+ /* *
41
+ * Queue a list of geo locations to be played at constant speed.
42
+ */
43
+ fun queueLocationUpdates (locations : List <Point >) {
44
+ locations.forEach {
45
+ queueLocationUpdate(it)
46
+ }
47
+ }
48
+
49
+ /* *
50
+ * Queue a new location update event to be played at constant speed.
51
+ */
52
+ fun queueLocationUpdate (
53
+ location : Point
54
+ ) {
55
+ val bearing = (locationList.lastOrNull()?.first ? : lastLocation)?.let {
56
+ bearing(it, location)
57
+ } ? : 0.0
58
+ val animationDurationMs = (locationList.lastOrNull()?.first ? : lastLocation)?.let {
59
+ (distanceInMeter(it, location) / speed) * 1000.0
60
+ } ? : 1000L
61
+ locationList.add(Triple (location, bearing, animationDurationMs.toLong()))
62
+ if (locationList.size == 1 && isPlaying) {
63
+ drainQueue()
64
+ }
65
+ }
66
+
67
+ /* *
68
+ * Start the playback, any incoming location updates will be queued and played sequentially.
69
+ */
70
+ fun startPlayback () {
71
+ isPlaying = true
72
+ }
73
+
74
+ /* *
75
+ * Cancel any ongoing playback, new incoming location updates will be queued but not played.
76
+ */
77
+ fun pausePlayback () {
78
+ isPlaying = false
79
+ handler.removeCallbacksAndMessages(null )
80
+ }
81
+
82
+ /* *
83
+ * Cancel any ongoing playback and clear remaining location queue.
84
+ * New incoming location updates will be queued but not played.
85
+ */
86
+ fun cancelPlayback () {
87
+ isPlaying = false
88
+ locationList.clear()
89
+ handler.removeCallbacksAndMessages(null )
90
+ }
91
+
92
+ private fun drainQueue () {
93
+ locationList.peek()?.let {
94
+ emitLocationUpdated(it.first, it.second, it.third) {
95
+ lastLocation = locationList.poll()?.first
96
+ drainQueue()
97
+ }
98
+ }
99
+ }
100
+
101
+ private fun emitLocationUpdated (
102
+ location : Point ,
103
+ bearing : Double ,
104
+ animationDuration : Long ,
105
+ finished : () -> Unit
106
+ ) {
107
+ locationConsumers.forEach {
108
+ it.onBearingUpdated(bearing) {
109
+ duration = bearingAnimateDuration
110
+ }
111
+ it.onLocationUpdated(location) {
112
+ duration = animationDuration
113
+ interpolator = LinearInterpolator ()
114
+ }
115
+ }
116
+ handler.postDelayed(finished, animationDuration)
117
+ }
118
+
119
+ override fun registerLocationConsumer (locationConsumer : LocationConsumer ) {
120
+ this .locationConsumers.add(locationConsumer)
121
+ }
122
+
123
+ override fun unRegisterLocationConsumer (locationConsumer : LocationConsumer ) {
124
+ this .locationConsumers.remove(locationConsumer)
125
+ }
126
+
127
+ private companion object {
128
+ /* *
129
+ * Takes two [Point] and finds the geographic bearing between them.
130
+ *
131
+ * @param point1 first point used for calculating the bearing
132
+ * @param point2 second point used for calculating the bearing
133
+ * @return bearing in decimal degrees
134
+ */
135
+ fun bearing (point1 : Point , point2 : Point ): Double {
136
+ val lon1: Double = degreesToRadians(point1.longitude())
137
+ val lon2: Double = degreesToRadians(point2.longitude())
138
+ val lat1: Double = degreesToRadians(point1.latitude())
139
+ val lat2: Double = degreesToRadians(point2.latitude())
140
+ val value1 = sin(lon2 - lon1) * cos(lat2)
141
+ val value2 = cos(lat1) * sin(lat2) - (sin(lat1) * cos(lat2) * cos(lon2 - lon1))
142
+ return radiansToDegrees(atan2(value1, value2))
143
+ }
144
+
145
+ fun radiansToDegrees (radians : Double ): Double {
146
+ val degrees = radians % (2 * Math .PI )
147
+ return degrees * 180 / Math .PI
148
+ }
149
+
150
+ fun degreesToRadians (degrees : Double ): Double {
151
+ val radians = degrees % 360
152
+ return radians * Math .PI / 180
153
+ }
154
+
155
+ fun distanceInMeter (point1 : Point , point2 : Point ): Double {
156
+ val radius = 6370000.0
157
+ val lat = degreesToRadians(point2.latitude() - point1.latitude())
158
+ val lon = degreesToRadians(point2.longitude() - point1.longitude())
159
+ val a = sin(lat / 2 ) * sin(lat / 2 ) + cos(degreesToRadians(point1.latitude())) * cos(
160
+ degreesToRadians(point2.latitude())
161
+ ) * sin(lon / 2 ) * sin(lon / 2 )
162
+ val c = 2 * atan2(sqrt(a), sqrt(1 - a))
163
+ return abs(radius * c)
164
+ }
165
+ }
166
+ }
0 commit comments