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