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