Skip to content

Commit 3e058ef

Browse files
committed
Initial prototype.
1 parent e1119b3 commit 3e058ef

File tree

5 files changed

+269
-0
lines changed

5 files changed

+269
-0
lines changed

app/src/main/AndroidManifest.xml

+14
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,20 @@
11831183
android:name="android.support.PARENT_ACTIVITY"
11841184
android:value=".ExampleOverviewActivity" />
11851185
</activity>
1186+
1187+
<activity
1188+
android:name=".examples.CustomLocationProviderActivity"
1189+
android:description="@string/description_custom_location_provider"
1190+
android:exported="true"
1191+
android:label="@string/activity_custom_location_provider">
1192+
<meta-data
1193+
android:name="@string/category"
1194+
android:value="@string/category_location" />
1195+
<meta-data
1196+
android:name="android.support.PARENT_ACTIVITY"
1197+
android:value=".ExampleOverviewActivity" />
1198+
</activity>
1199+
11861200
<activity
11871201
android:name=".TestMapActivity"
11881202
android:exported="true" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.mapbox.maps.testapp.examples
2+
3+
import android.annotation.SuppressLint
4+
import android.os.Bundle
5+
import android.view.ViewGroup
6+
import android.widget.Button
7+
import androidx.appcompat.app.AppCompatActivity
8+
import com.mapbox.geojson.Point
9+
import com.mapbox.maps.CameraOptions
10+
import com.mapbox.maps.MapView
11+
import com.mapbox.maps.Style
12+
import com.mapbox.maps.plugin.LocationPuck3D
13+
import com.mapbox.maps.plugin.gestures.OnMapClickListener
14+
import com.mapbox.maps.plugin.gestures.gestures
15+
import com.mapbox.maps.plugin.locationcomponent.CustomLocationProvider
16+
import com.mapbox.maps.plugin.locationcomponent.location2
17+
18+
/**
19+
* Example of using custom location provider.
20+
*/
21+
class CustomLocationProviderActivity : AppCompatActivity(), OnMapClickListener {
22+
23+
private val customLocationProvider = CustomLocationProvider()
24+
private lateinit var mapView: MapView
25+
26+
@SuppressLint("SetTextI18n")
27+
override fun onCreate(savedInstanceState: Bundle?) {
28+
super.onCreate(savedInstanceState)
29+
mapView = MapView(this)
30+
setContentView(mapView)
31+
mapView.addView(
32+
Button(this).apply {
33+
layoutParams = ViewGroup.LayoutParams(
34+
ViewGroup.LayoutParams.WRAP_CONTENT,
35+
ViewGroup.LayoutParams.WRAP_CONTENT
36+
)
37+
text = "Cancel"
38+
setOnClickListener {
39+
customLocationProvider.cancelPlayback()
40+
}
41+
}
42+
)
43+
mapView.getMapboxMap()
44+
.apply {
45+
setCamera(
46+
CameraOptions.Builder()
47+
.center(HELSINKI)
48+
.pitch(40.0)
49+
.zoom(14.0)
50+
.build()
51+
)
52+
loadStyleUri(Style.MAPBOX_STREETS) {
53+
initLocationComponent()
54+
initClickListeners()
55+
customLocationProvider.startPlayback()
56+
}
57+
}
58+
}
59+
60+
private fun initClickListeners() {
61+
mapView.gestures.addOnMapClickListener(this)
62+
}
63+
64+
private fun initLocationComponent() {
65+
val locationComponentPlugin2 = mapView.location2
66+
locationComponentPlugin2.setLocationProvider(customLocationProvider)
67+
locationComponentPlugin2.let {
68+
it.locationPuck = LocationPuck3D(
69+
modelUri = "asset://sportcar.glb",
70+
modelScale = listOf(0.1f, 0.1f, 0.1f),
71+
modelTranslation = listOf(0.1f, 0.1f, 0.1f),
72+
modelRotation = listOf(0.0f, 0.0f, 180.0f)
73+
)
74+
}
75+
locationComponentPlugin2.enabled = true
76+
locationComponentPlugin2.puckBearingEnabled = true
77+
}
78+
79+
override fun onMapClick(point: Point): Boolean {
80+
customLocationProvider.queueLocationUpdate(point)
81+
return true
82+
}
83+
84+
companion object {
85+
private val HELSINKI = Point.fromLngLat(24.9384, 60.1699)
86+
}
87+
}

app/src/main/res/values/example_descriptions.xml

+1
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,5 @@
9393
<string name="description_info_window">Legacy InfoWindow implementation using view annotations</string>
9494
<string name="description_viewport">Viewport camera showcase</string>
9595
<string name="description_advanced_viewport">Advanced viewport with gestures showcase</string>
96+
<string name="description_custom_location_provider">Showcase usage of custom location provider.</string>
9697
</resources>

app/src/main/res/values/example_titles.xml

+1
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,5 @@
9393
<string name="activity_info_window">View annotation as InfoWindow</string>
9494
<string name="activity_viewport">Viewport camera</string>
9595
<string name="activity_advanced_viewport">Advanced Viewport with gestures</string>
96+
<string name="activity_custom_location_provider">Custom location provider</string>
9697
</resources>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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

Comments
 (0)