diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8dcc41844b..90f6274174 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1195,6 +1195,33 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".ExampleOverviewActivity" />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/location/CustomJourneyLocationProviderActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/location/CustomJourneyLocationProviderActivity.kt
new file mode 100644
index 0000000000..dc6e27892b
--- /dev/null
+++ b/app/src/main/java/com/mapbox/maps/testapp/examples/location/CustomJourneyLocationProviderActivity.kt
@@ -0,0 +1,117 @@
+package com.mapbox.maps.testapp.examples.location
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.ViewGroup
+import android.widget.Button
+import androidx.appcompat.app.AppCompatActivity
+import com.mapbox.geojson.Point
+import com.mapbox.maps.CameraOptions
+import com.mapbox.maps.MapView
+import com.mapbox.maps.Style
+import com.mapbox.maps.plugin.LocationPuck3D
+import com.mapbox.maps.plugin.annotation.annotations
+import com.mapbox.maps.plugin.annotation.generated.PointAnnotation
+import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager
+import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions
+import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager
+import com.mapbox.maps.plugin.gestures.OnMapClickListener
+import com.mapbox.maps.plugin.gestures.gestures
+import com.mapbox.maps.plugin.locationcomponent.CustomJourneyLocationProvider
+import com.mapbox.maps.plugin.locationcomponent.Journey
+import com.mapbox.maps.plugin.locationcomponent.location2
+import com.mapbox.maps.testapp.R
+import com.mapbox.maps.testapp.utils.BitmapUtils
+import java.util.*
+
+/**
+ * Example of using custom location provider.
+ */
+class CustomJourneyLocationProviderActivity : AppCompatActivity(), OnMapClickListener {
+ private val journey = Journey()
+ private val customJourneyLocationProvider = CustomJourneyLocationProvider().apply { loadJourney(journey) }
+ private lateinit var mapView: MapView
+ private val annotationList = LinkedList()
+ private lateinit var pointAnnotationManager: PointAnnotationManager
+
+ @SuppressLint("SetTextI18n")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ mapView = MapView(this)
+ setContentView(mapView)
+ mapView.addView(
+ Button(this).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ text = "Cancel"
+ setOnClickListener {
+ journey.pause()
+ }
+ }
+ )
+ pointAnnotationManager = mapView.annotations.createPointAnnotationManager()
+ mapView.getMapboxMap()
+ .apply {
+ setCamera(
+ CameraOptions.Builder()
+ .center(HELSINKI)
+ .pitch(40.0)
+ .zoom(14.0)
+ .build()
+ )
+ loadStyleUri(Style.MAPBOX_STREETS) {
+ initLocationComponent()
+ initClickListeners()
+ journey.start()
+ }
+ }
+
+ journey.observeJourneyUpdates { location, bearing, locationAnimationDurationMs, bearingAnimateDurationMs ->
+ annotationList.poll()?.let {
+ pointAnnotationManager.delete(it)
+ }
+ true
+ }
+ }
+
+ private fun initClickListeners() {
+ mapView.gestures.addOnMapClickListener(this)
+ }
+
+ private fun initLocationComponent() {
+ val locationComponentPlugin2 = mapView.location2
+ locationComponentPlugin2.setLocationProvider(customJourneyLocationProvider)
+ locationComponentPlugin2.let {
+ it.locationPuck = LocationPuck3D(
+ modelUri = "asset://sportcar.glb",
+ modelScale = listOf(0.2f, 0.2f, 0.2f),
+ modelTranslation = listOf(0.1f, 0.1f, 0.1f),
+ modelRotation = listOf(0.0f, 0.0f, 180.0f)
+ )
+ }
+ locationComponentPlugin2.enabled = true
+ locationComponentPlugin2.puckBearingEnabled = true
+ }
+
+ override fun onMapClick(point: Point): Boolean {
+ journey.queueLocationUpdate(point)
+ BitmapUtils.bitmapFromDrawableRes(
+ this,
+ R.drawable.red_marker
+ )?.let {
+ pointAnnotationManager.create(
+ PointAnnotationOptions()
+ .withPoint(point)
+ .withIconImage(it)
+ .withDraggable(true)
+ ).also { annotation -> annotationList.add(annotation) }
+ }
+ return true
+ }
+
+ companion object {
+ private val HELSINKI = Point.fromLngLat(24.9384, 60.1699)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/location/MultipleLocationComponentActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/location/MultipleLocationComponentActivity.kt
new file mode 100644
index 0000000000..1c338c65de
--- /dev/null
+++ b/app/src/main/java/com/mapbox/maps/testapp/examples/location/MultipleLocationComponentActivity.kt
@@ -0,0 +1,184 @@
+package com.mapbox.maps.testapp.examples.location
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.mapbox.geojson.Point
+import com.mapbox.maps.*
+import com.mapbox.maps.plugin.LocationPuck2D
+import com.mapbox.maps.plugin.LocationPuck3D
+import com.mapbox.maps.plugin.annotation.annotations
+import com.mapbox.maps.plugin.annotation.generated.*
+import com.mapbox.maps.plugin.gestures.OnMapClickListener
+import com.mapbox.maps.plugin.gestures.gestures
+import com.mapbox.maps.plugin.locationcomponent.CustomJourneyLocationProvider
+import com.mapbox.maps.plugin.locationcomponent.Journey
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin2
+import com.mapbox.maps.plugin.viewport.data.MultiPuckViewportStateBearing
+import com.mapbox.maps.plugin.viewport.data.MultiPuckViewportStateOptions
+import com.mapbox.maps.plugin.viewport.state.MultiPuckViewportState
+import com.mapbox.maps.plugin.viewport.viewport
+import com.mapbox.maps.testapp.R
+import com.mapbox.maps.testapp.databinding.ActivityMultiLocationcomponentBinding
+import com.mapbox.maps.testapp.utils.BitmapUtils
+import com.mapbox.maps.testapp.utils.createLocationComponent
+import java.util.*
+
+/**
+ * Example of using multiple location component.
+ */
+@OptIn(MapboxExperimental::class)
+class MultipleLocationComponentActivity : AppCompatActivity(), OnMapClickListener {
+ private lateinit var mapView: MapView
+ private val journeys = mutableListOf(Journey(speed = 150.0, angularSpeed = 150.0), Journey(speed = 100.0, angularSpeed = 150.0))
+ private val customJourneyLocationProviders = mutableListOf(
+ CustomJourneyLocationProvider().apply { loadJourney(journeys.first()) },
+ CustomJourneyLocationProvider().apply { loadJourney(journeys.last()) }
+ )
+ private val annotationLists =
+ mutableListOf(LinkedList(), LinkedList())
+ private val locationComponents = mutableListOf()
+ private lateinit var pointAnnotationManager: PointAnnotationManager
+ private lateinit var multiPuckViewportState: MultiPuckViewportState
+
+ var selectedPuck = 0
+
+ @SuppressLint("SetTextI18n")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val binding = ActivityMultiLocationcomponentBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ mapView = binding.mapView
+ binding.toggleControlBtn.apply {
+ text = "Controlling ${if (selectedPuck == 0) "Car" else "Duck"}"
+ setOnClickListener {
+ toggleControl()
+ text = "Controlling ${if (selectedPuck == 0) "Car" else "Duck"}"
+ }
+ }
+ binding.followPucksButton.setOnClickListener {
+ mapView.viewport.transitionTo(multiPuckViewportState)
+ }
+ pointAnnotationManager = mapView.annotations.createPointAnnotationManager()
+ mapView.getMapboxMap()
+ .apply {
+ setCamera(
+ CameraOptions.Builder()
+ .center(HELSINKI)
+ .pitch(40.0)
+ .zoom(14.0)
+ .build()
+ )
+ loadStyleUri(Style.MAPBOX_STREETS) {
+ initLocationComponents()
+ initClickListeners()
+ journeys.forEach { it.start() }
+ }
+ }
+
+ journeys.forEachIndexed { index, journey ->
+ journey.observeJourneyUpdates { _, _, _, _ ->
+ annotationLists[index].poll()?.let {
+ pointAnnotationManager.delete(it)
+ }
+ true
+ }
+ }
+ }
+
+ private fun toggleControl() {
+ selectedPuck = (selectedPuck + 1) % 2
+ }
+
+ private fun initClickListeners() {
+ mapView.gestures.addOnMapClickListener(this)
+ }
+
+ private fun initLocationComponents() {
+ // Puck with pulsing car
+ locationComponents.add(
+ mapView.createLocationComponent(LocationComponentInitOptions.getNextUniqueLocationComponentOptions())
+ .apply {
+ setLocationProvider(customJourneyLocationProviders.first())
+ enabled = true
+ locationPuck = LocationPuck2D(
+ topImage = null,
+ bearingImage = null,
+ )
+ puckBearingEnabled = true
+ pulsingEnabled = true
+ }
+ )
+ locationComponents.add(
+ mapView.createLocationComponent(LocationComponentInitOptions.getNextUniqueLocationComponentOptions())
+ .apply {
+ setLocationProvider(customJourneyLocationProviders.first())
+ enabled = true
+ locationPuck = LocationPuck3D(
+ modelUri = "asset://sportcar.glb",
+ modelScale = listOf(0.3f, 0.3f, 0.3f),
+ modelTranslation = listOf(0.1f, 0.1f, 0.1f),
+ modelRotation = listOf(0.0f, 0.0f, 180.0f)
+ )
+ puckBearingEnabled = true
+ }
+ )
+ // Puck with pulsing duck
+ locationComponents.add(
+ mapView.createLocationComponent(LocationComponentInitOptions.getNextUniqueLocationComponentOptions())
+ .apply {
+ setLocationProvider(customJourneyLocationProviders.last())
+ enabled = true
+ locationPuck = LocationPuck2D(
+ topImage = null,
+ bearingImage = null,
+ )
+ puckBearingEnabled = true
+ pulsingEnabled = true
+ }
+ )
+ locationComponents.add(
+ mapView.createLocationComponent(LocationComponentInitOptions.getNextUniqueLocationComponentOptions())
+ .apply {
+ setLocationProvider(customJourneyLocationProviders.last())
+ enabled = true
+ locationPuck = LocationPuck3D(
+ modelUri = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Embedded/Duck.gltf",
+ modelScale = listOf(0.5f, 0.5f, 0.5f),
+ modelRotation = listOf(0f, 0f, -90f),
+ modelTranslation = listOf(0f, 0.0f, 0.0f)
+ )
+ puckBearingEnabled = true
+ }
+ )
+ multiPuckViewportState = mapView.viewport.makeMultiPuckViewportState(
+ MultiPuckViewportStateOptions
+ .Builder()
+ .bearing(MultiPuckViewportStateBearing.SyncWithLocationPuck(locationComponents.first()))
+ .padding(EdgeInsets(100.0, 100.0, 100.0, 100.0))
+ .build(),
+ locationComponents = locationComponents
+ )
+ }
+
+ override fun onMapClick(point: Point): Boolean {
+ journeys[selectedPuck].queueLocationUpdate(point)
+ BitmapUtils.bitmapFromDrawableRes(
+ this,
+ if (selectedPuck == 0) R.drawable.red_marker else R.drawable.blue_marker_view
+ )?.let {
+ pointAnnotationManager.create(
+ PointAnnotationOptions()
+ .withPoint(point)
+ .withIconImage(it)
+ .withDraggable(true)
+ ).also { annotation -> annotationLists[selectedPuck].add(annotation) }
+ }
+ return true
+ }
+
+ companion object {
+ private val HELSINKI = Point.fromLngLat(24.9384, 60.1699)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mapbox/maps/testapp/utils/LocationComponentUtils.kt b/app/src/main/java/com/mapbox/maps/testapp/utils/LocationComponentUtils.kt
new file mode 100644
index 0000000000..2a138b682c
--- /dev/null
+++ b/app/src/main/java/com/mapbox/maps/testapp/utils/LocationComponentUtils.kt
@@ -0,0 +1,17 @@
+package com.mapbox.maps.testapp.utils
+
+import com.mapbox.maps.MapView
+import com.mapbox.maps.plugin.Plugin
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin2
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentPluginImpl
+
+public fun MapView.createLocationComponent(locationComponentInitOptions: LocationComponentInitOptions): LocationComponentPlugin2 {
+ val locationComponent = LocationComponentPluginImpl(locationComponentInitOptions)
+ val locationComponentPlugin = Plugin.Custom(
+ locationComponentInitOptions.hashCode().toString(),
+ locationComponent
+ )
+ this.createPlugin(locationComponentPlugin)
+ return locationComponent
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_multi_locationcomponent.xml b/app/src/main/res/layout/activity_multi_locationcomponent.xml
new file mode 100644
index 0000000000..6cfa8cbf62
--- /dev/null
+++ b/app/src/main/res/layout/activity_multi_locationcomponent.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/example_descriptions.xml b/app/src/main/res/values/example_descriptions.xml
index fd4e7e657c..a7af38f6ea 100644
--- a/app/src/main/res/values/example_descriptions.xml
+++ b/app/src/main/res/values/example_descriptions.xml
@@ -94,4 +94,6 @@
Viewport camera showcase
Advanced viewport with gestures showcase
Add a third party vector tile source.
+ Showcase usage of custom location provider.
+ Showcase usage of multiple location component.
diff --git a/app/src/main/res/values/example_titles.xml b/app/src/main/res/values/example_titles.xml
index 67229d2cba..1dc4ae9036 100644
--- a/app/src/main/res/values/example_titles.xml
+++ b/app/src/main/res/values/example_titles.xml
@@ -94,4 +94,6 @@
Viewport camera
Advanced Viewport with gestures
External Vector Source
+ Custom location provider
+ Multiple location components
diff --git a/plugin-locationcomponent/api/metalava.txt b/plugin-locationcomponent/api/metalava.txt
index 1e4547e0c8..dbfa4cc7ad 100644
--- a/plugin-locationcomponent/api/metalava.txt
+++ b/plugin-locationcomponent/api/metalava.txt
@@ -1,6 +1,13 @@
// Signature format: 3.0
package com.mapbox.maps.plugin.locationcomponent {
+ @com.mapbox.maps.MapboxExperimental public final class CustomJourneyLocationProvider implements com.mapbox.maps.plugin.locationcomponent.LocationProvider {
+ ctor public CustomJourneyLocationProvider();
+ method public void loadJourney(com.mapbox.maps.plugin.locationcomponent.Journey journey);
+ method public void registerLocationConsumer(com.mapbox.maps.plugin.locationcomponent.LocationConsumer locationConsumer);
+ method public void unRegisterLocationConsumer(com.mapbox.maps.plugin.locationcomponent.LocationConsumer locationConsumer);
+ }
+
public final class DefaultLocationProvider implements com.mapbox.maps.plugin.locationcomponent.LocationProvider {
ctor public DefaultLocationProvider(android.content.Context context);
method public void addOnCompassCalibrationListener(com.mapbox.maps.plugin.locationcomponent.LocationCompassCalibrationListener listener);
@@ -10,6 +17,27 @@ package com.mapbox.maps.plugin.locationcomponent {
method public void updatePuckBearingSource(com.mapbox.maps.plugin.PuckBearingSource source);
}
+ @com.mapbox.maps.MapboxExperimental public final class Journey {
+ ctor public Journey(double speed = 100.0, double angularSpeed = 100.0);
+ method public double getAngularSpeed();
+ method public java.util.List getRemainingLocationsInQueue();
+ method public double getSpeed();
+ method public void observeJourneyUpdates(com.mapbox.maps.plugin.locationcomponent.JourneyDataObserver observer);
+ method public void pause();
+ method public void queueLocationUpdate(com.mapbox.geojson.Point location);
+ method public void queueLocationUpdates(java.util.List locations);
+ method public void restart();
+ method public void resume();
+ method public void start();
+ property public final double angularSpeed;
+ property public final java.util.List remainingLocationsInQueue;
+ property public final double speed;
+ }
+
+ public fun interface JourneyDataObserver {
+ method public boolean onNewData(com.mapbox.geojson.Point location, double bearing, long locationAnimationDurationMs, long bearingAnimateDurationMs);
+ }
+
public fun interface LocationCompassCalibrationListener {
method public void onCompassCalibrationNeeded();
}
@@ -35,8 +63,49 @@ package com.mapbox.maps.plugin.locationcomponent {
field public static final long TRANSITION_ANIMATION_DURATION_MS = 750L; // 0x2eeL
}
+ public final class LocationComponentInitOptions {
+ method public String! getBearingIconImageId();
+ method public String! getPuck2DLayerId();
+ method public String! getPuck3DLayerId();
+ method public String! getPuck3DSourceId();
+ method public String! getShadowIconImageId();
+ method public String! getTopIconImageId();
+ property public final String! bearingIconImageId;
+ property public final String! puck2DLayerId;
+ property public final String! puck3DLayerId;
+ property public final String! puck3DSourceId;
+ property public final String! shadowIconImageId;
+ property public final String! topIconImageId;
+ }
+
+ public static final class LocationComponentInitOptions.Builder {
+ ctor public LocationComponentInitOptions.Builder();
+ method public com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions build();
+ method public String getBearingIconImageId();
+ method public String getPuck2DLayerId();
+ method public String getPuck3DLayerId();
+ method public String getPuck3DSourceId();
+ method public String getShadowIconImageId();
+ method public String getTopIconImageId();
+ method public com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions.Builder setBearingIconImageId(String bearingIconImageId);
+ method public com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions.Builder setPuck2DLayerId(String puck2DLayerId);
+ method public com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions.Builder setPuck3DLayerId(String puck3DLayerId);
+ method public com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions.Builder setPuck3DSourceId(String puck3DSourceId);
+ method public com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions.Builder setShadowIconImageId(String shadowIconImageId);
+ method public com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions.Builder setTopIconImageId(String topIconImageId);
+ property public final String bearingIconImageId;
+ property public final String puck2DLayerId;
+ property public final String puck3DLayerId;
+ property public final String puck3DSourceId;
+ property public final String shadowIconImageId;
+ property public final String topIconImageId;
+ }
+
+ public final class LocationComponentInitOptionsKt {
+ }
+
public final class LocationComponentPluginImpl extends com.mapbox.maps.plugin.locationcomponent.generated.LocationComponentSettingsBase2 implements com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin2 com.mapbox.maps.plugin.locationcomponent.LocationConsumer2 {
- ctor public LocationComponentPluginImpl();
+ ctor public LocationComponentPluginImpl(com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions locationComponentInitOptions = LocationComponentInitOptions.().build());
method public void addOnIndicatorAccuracyRadiusChangedListener(com.mapbox.maps.plugin.locationcomponent.OnIndicatorAccuracyRadiusChangedListener listener);
method public void addOnIndicatorBearingChangedListener(com.mapbox.maps.plugin.locationcomponent.OnIndicatorBearingChangedListener listener);
method public void addOnIndicatorPositionChangedListener(com.mapbox.maps.plugin.locationcomponent.OnIndicatorPositionChangedListener listener);
@@ -45,6 +114,7 @@ package com.mapbox.maps.plugin.locationcomponent {
method public void bind(android.content.Context context, android.util.AttributeSet? attrs, float pixelRatio);
method protected com.mapbox.maps.plugin.locationcomponent.generated.LocationComponentSettings getInternalSettings();
method protected com.mapbox.maps.plugin.locationcomponent.generated.LocationComponentSettings2 getInternalSettings2();
+ method public com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions getLocationComponentInitOptions();
method public com.mapbox.maps.plugin.locationcomponent.LocationProvider? getLocationProvider();
method public void isLocatedAt(com.mapbox.geojson.Point point, com.mapbox.maps.plugin.locationcomponent.PuckLocatedAtPointListener listener);
method public void onAccuracyRadiusUpdated(double[] radius, kotlin.jvm.functions.Function1 super android.animation.ValueAnimator,kotlin.Unit>? options);
@@ -65,6 +135,7 @@ package com.mapbox.maps.plugin.locationcomponent {
method public void setLocationProvider(com.mapbox.maps.plugin.locationcomponent.LocationProvider locationProvider);
property protected com.mapbox.maps.plugin.locationcomponent.generated.LocationComponentSettings internalSettings;
property protected com.mapbox.maps.plugin.locationcomponent.generated.LocationComponentSettings2 internalSettings2;
+ property public final com.mapbox.maps.plugin.locationcomponent.LocationComponentInitOptions locationComponentInitOptions;
field public com.mapbox.maps.plugin.locationcomponent.generated.LocationComponentSettings internalSettings;
field public com.mapbox.maps.plugin.locationcomponent.generated.LocationComponentSettings2 internalSettings2;
}
diff --git a/plugin-locationcomponent/api/plugin-locationcomponent.api b/plugin-locationcomponent/api/plugin-locationcomponent.api
index ee61fab6a9..f422b8c41a 100644
--- a/plugin-locationcomponent/api/plugin-locationcomponent.api
+++ b/plugin-locationcomponent/api/plugin-locationcomponent.api
@@ -1,3 +1,10 @@
+public final class com/mapbox/maps/plugin/locationcomponent/CustomJourneyLocationProvider : com/mapbox/maps/plugin/locationcomponent/LocationProvider {
+ public fun ()V
+ public final fun loadJourney (Lcom/mapbox/maps/plugin/locationcomponent/Journey;)V
+ public fun registerLocationConsumer (Lcom/mapbox/maps/plugin/locationcomponent/LocationConsumer;)V
+ public fun unRegisterLocationConsumer (Lcom/mapbox/maps/plugin/locationcomponent/LocationConsumer;)V
+}
+
public final class com/mapbox/maps/plugin/locationcomponent/DefaultLocationProvider : com/mapbox/maps/plugin/locationcomponent/LocationProvider {
public fun (Landroid/content/Context;)V
public final fun addOnCompassCalibrationListener (Lcom/mapbox/maps/plugin/locationcomponent/LocationCompassCalibrationListener;)V
@@ -7,6 +14,26 @@ public final class com/mapbox/maps/plugin/locationcomponent/DefaultLocationProvi
public final fun updatePuckBearingSource (Lcom/mapbox/maps/plugin/PuckBearingSource;)V
}
+public final class com/mapbox/maps/plugin/locationcomponent/Journey {
+ public fun ()V
+ public fun (DD)V
+ public synthetic fun (DDILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun getAngularSpeed ()D
+ public final fun getRemainingLocationsInQueue ()Ljava/util/List;
+ public final fun getSpeed ()D
+ public final fun observeJourneyUpdates (Lcom/mapbox/maps/plugin/locationcomponent/JourneyDataObserver;)V
+ public final fun pause ()V
+ public final fun queueLocationUpdate (Lcom/mapbox/geojson/Point;)V
+ public final fun queueLocationUpdates (Ljava/util/List;)V
+ public final fun restart ()V
+ public final fun resume ()V
+ public final fun start ()V
+}
+
+public abstract interface class com/mapbox/maps/plugin/locationcomponent/JourneyDataObserver {
+ public abstract fun onNewData (Lcom/mapbox/geojson/Point;DJJ)Z
+}
+
public abstract interface class com/mapbox/maps/plugin/locationcomponent/LocationCompassCalibrationListener {
public abstract fun onCompassCalibrationNeeded ()V
}
@@ -32,13 +59,56 @@ public final class com/mapbox/maps/plugin/locationcomponent/LocationComponentCon
public static final field TRANSITION_ANIMATION_DURATION_MS J
}
+public final class com/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions {
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getBearingIconImageId ()Ljava/lang/String;
+ public final fun getPuck2DLayerId ()Ljava/lang/String;
+ public final fun getPuck3DLayerId ()Ljava/lang/String;
+ public final fun getPuck3DSourceId ()Ljava/lang/String;
+ public final fun getShadowIconImageId ()Ljava/lang/String;
+ public final fun getTopIconImageId ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final class com/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions$Builder {
+ public fun ()V
+ public final fun build ()Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions;
+ public final fun getBearingIconImageId ()Ljava/lang/String;
+ public final fun getPuck2DLayerId ()Ljava/lang/String;
+ public final fun getPuck3DLayerId ()Ljava/lang/String;
+ public final fun getPuck3DSourceId ()Ljava/lang/String;
+ public final fun getShadowIconImageId ()Ljava/lang/String;
+ public final fun getTopIconImageId ()Ljava/lang/String;
+ public final fun setBearingIconImageId (Ljava/lang/String;)Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions$Builder;
+ public final synthetic fun setBearingIconImageId (Ljava/lang/String;)V
+ public final fun setPuck2DLayerId (Ljava/lang/String;)Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions$Builder;
+ public final synthetic fun setPuck2DLayerId (Ljava/lang/String;)V
+ public final fun setPuck3DLayerId (Ljava/lang/String;)Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions$Builder;
+ public final synthetic fun setPuck3DLayerId (Ljava/lang/String;)V
+ public final fun setPuck3DSourceId (Ljava/lang/String;)Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions$Builder;
+ public final synthetic fun setPuck3DSourceId (Ljava/lang/String;)V
+ public final fun setShadowIconImageId (Ljava/lang/String;)Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions$Builder;
+ public final synthetic fun setShadowIconImageId (Ljava/lang/String;)V
+ public final fun setTopIconImageId (Ljava/lang/String;)Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions$Builder;
+ public final synthetic fun setTopIconImageId (Ljava/lang/String;)V
+}
+
+public final class com/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptionsKt {
+ public static final synthetic fun LocationComponentInitOptions (Lkotlin/jvm/functions/Function1;)Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions;
+}
+
public final class com/mapbox/maps/plugin/locationcomponent/LocationComponentPluginImpl : com/mapbox/maps/plugin/locationcomponent/generated/LocationComponentSettingsBase2, com/mapbox/maps/plugin/locationcomponent/LocationComponentPlugin2, com/mapbox/maps/plugin/locationcomponent/LocationConsumer2 {
public fun ()V
+ public fun (Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions;)V
+ public synthetic fun (Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun addOnIndicatorAccuracyRadiusChangedListener (Lcom/mapbox/maps/plugin/locationcomponent/OnIndicatorAccuracyRadiusChangedListener;)V
public fun addOnIndicatorBearingChangedListener (Lcom/mapbox/maps/plugin/locationcomponent/OnIndicatorBearingChangedListener;)V
public fun addOnIndicatorPositionChangedListener (Lcom/mapbox/maps/plugin/locationcomponent/OnIndicatorPositionChangedListener;)V
public fun bind (Landroid/content/Context;Landroid/util/AttributeSet;F)V
public fun cleanup ()V
+ public final fun getLocationComponentInitOptions ()Lcom/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions;
public fun getLocationProvider ()Lcom/mapbox/maps/plugin/locationcomponent/LocationProvider;
public fun initialize ()V
public fun isLocatedAt (Lcom/mapbox/geojson/Point;Lcom/mapbox/maps/plugin/locationcomponent/PuckLocatedAtPointListener;)V
diff --git a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/CustomJourneyLocationProvider.kt b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/CustomJourneyLocationProvider.kt
new file mode 100644
index 0000000000..52810cfe60
--- /dev/null
+++ b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/CustomJourneyLocationProvider.kt
@@ -0,0 +1,287 @@
+package com.mapbox.maps.plugin.locationcomponent
+
+import android.os.Handler
+import android.os.Looper
+import android.view.animation.LinearInterpolator
+import com.mapbox.geojson.Point
+import com.mapbox.maps.MapboxExperimental
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.CopyOnWriteArrayList
+import java.util.concurrent.CopyOnWriteArraySet
+import kotlin.math.*
+
+/**
+ * A custom location provider implementation that allows to play location updates at constant speed.
+ */
+@MapboxExperimental
+class CustomJourneyLocationProvider : LocationProvider {
+ private var locationConsumers = CopyOnWriteArraySet()
+
+ /**
+ * Load a journey to the CustomJourneyLocationProvider.
+ */
+ fun loadJourney(journey: Journey) {
+ journey.observeJourneyUpdates { point, bearing, locationAnimationDurationMs, bearingAnimationDurationMs ->
+ emitLocationUpdated(point, bearing, locationAnimationDurationMs, bearingAnimationDurationMs)
+ true
+ }
+ }
+
+ private fun emitLocationUpdated(
+ location: Point,
+ bearing: Double,
+ locationAnimationDuration: Long,
+ bearingAnimateDuration: Long,
+ ) {
+ locationConsumers.forEach {
+ it.onBearingUpdated(bearing) {
+ duration = bearingAnimateDuration
+ }
+ it.onLocationUpdated(location) {
+ duration = locationAnimationDuration
+ interpolator = LinearInterpolator()
+ }
+ }
+ }
+
+ /**
+ * Handling of registered location consumers.
+ */
+ override fun registerLocationConsumer(locationConsumer: LocationConsumer) {
+ this.locationConsumers.add(locationConsumer)
+ }
+
+ /**
+ * Handling of unregistered location consumer.
+ */
+ override fun unRegisterLocationConsumer(locationConsumer: LocationConsumer) {
+ this.locationConsumers.remove(locationConsumer)
+ }
+}
+
+/**
+ * Abstraction of a Journey.
+ */
+@MapboxExperimental
+class Journey(
+ /**
+ * The speed to playback the journey.
+ */
+ val speed: Double = 100.0,
+ /**
+ * The angular speed to playback the journey.
+ */
+ val angularSpeed: Double = 500.0
+) {
+ private val locationList = CopyOnWriteArrayList()
+ private val initialTimeStamp: Long = 0
+ private val remainingPoints = ConcurrentLinkedQueue()
+ private var isPlaying = false
+ private val handler = Handler(Looper.getMainLooper())
+
+ private val observers = CopyOnWriteArraySet()
+
+ /**
+ * Return the remaining locations in the queue.
+ */
+ val remainingLocationsInQueue: List
+ get() {
+ with(remainingPoints) {
+ return this.map { it.location }
+ }
+ }
+
+ /**
+ * Observe the journey updates.
+ */
+ fun observeJourneyUpdates(observer: JourneyDataObserver) {
+ observers.add(observer)
+ }
+
+ /**
+ * Start the playback, any incoming location updates will be queued and played sequentially.
+ */
+ fun start() {
+ isPlaying = true
+ drainQueue()
+ }
+
+ /**
+ * Cancel any ongoing playback, new incoming location updates will be queued but not played.
+ */
+ fun pause() {
+ isPlaying = false
+ handler.removeCallbacksAndMessages(null)
+ }
+
+ /**
+ * Resume the remaining journey.
+ */
+ fun resume() {
+ isPlaying = true
+ drainQueue()
+ }
+
+ /**
+ * Restart the journey.
+ */
+ fun restart() {
+ remainingPoints.clear()
+ remainingPoints.addAll(locationList)
+ isPlaying = true
+ }
+
+ /**
+ * Queue a new location update event to be played at constant speed.
+ */
+ fun queueLocationUpdate(
+ location: Point
+ ) {
+ val bearing = locationList.lastOrNull()?.location?.let {
+ bearing(it, location)
+ } ?: 0.0
+ val animationDurationMs = locationList.lastOrNull()?.location?.let {
+ (distanceInMeter(it, location) / speed) * 1000.0
+ } ?: 1000L
+ val bearingAnimateDurationMs =
+ abs(
+ shortestRotation(
+ bearing,
+ locationList.lastOrNull()?.bearing ?: 0.0
+ ) / angularSpeed
+ ) * 1000.0
+
+ val nextData =
+ QueueData(location, bearing, animationDurationMs.toLong(), bearingAnimateDurationMs.toLong())
+ locationList.add(nextData)
+ remainingPoints.add(nextData)
+ if (remainingPoints.size == 1 && isPlaying) {
+ drainQueue()
+ }
+ }
+
+ /**
+ * Queue a list of geo locations to be played at constant speed.
+ */
+ fun queueLocationUpdates(locations: List) {
+ locations.forEach {
+ queueLocationUpdate(it)
+ }
+ }
+
+ private fun drainQueue() {
+ remainingPoints.peek()?.let { data ->
+ observers.forEach {
+ if (!it.onNewData(
+ data.location,
+ data.bearing,
+ data.locationAnimationDurationMs,
+ data.bearingAnimateDurationMs
+ )
+ ) {
+ observers.remove(it)
+ }
+ }
+ if (isPlaying) {
+ handler.postDelayed(
+ {
+ remainingPoints.poll()
+ drainQueue()
+ },
+ max(data.locationAnimationDurationMs, data.bearingAnimateDurationMs)
+ )
+ }
+ }
+ }
+
+ private data class QueueData(
+ val location: Point,
+ val bearing: Double,
+ val locationAnimationDurationMs: Long,
+ val bearingAnimateDurationMs: Long
+ )
+
+ private companion object {
+ /**
+ * Takes two [Point] and finds the geographic bearing between them.
+ *
+ * @param point1 first point used for calculating the bearing
+ * @param point2 second point used for calculating the bearing
+ * @return bearing in decimal degrees
+ */
+ fun bearing(point1: Point, point2: Point): Double {
+ val lon1: Double = degreesToRadians(point1.longitude())
+ val lon2: Double = degreesToRadians(point2.longitude())
+ val lat1: Double = degreesToRadians(point1.latitude())
+ val lat2: Double = degreesToRadians(point2.latitude())
+ val value1 = sin(lon2 - lon1) * cos(lat2)
+ val value2 = cos(lat1) * sin(lat2) - (sin(lat1) * cos(lat2) * cos(lon2 - lon1))
+ return radiansToDegrees(atan2(value1, value2))
+ }
+
+ fun radiansToDegrees(radians: Double): Double {
+ val degrees = radians % (2 * Math.PI)
+ return degrees * 180 / Math.PI
+ }
+
+ fun degreesToRadians(degrees: Double): Double {
+ val radians = degrees % 360
+ return radians * Math.PI / 180
+ }
+
+ fun distanceInMeter(point1: Point, point2: Point): Double {
+ val radius = 6370000.0
+ val lat = degreesToRadians(point2.latitude() - point1.latitude())
+ val lon = degreesToRadians(point2.longitude() - point1.longitude())
+ val a = sin(lat / 2) * sin(lat / 2) + cos(degreesToRadians(point1.latitude())) * cos(
+ degreesToRadians(point2.latitude())
+ ) * sin(lon / 2) * sin(lon / 2)
+ val c = 2 * atan2(sqrt(a), sqrt(1 - a))
+ return abs(radius * c)
+ }
+
+ /**
+ * Util for finding the shortest path from the current rotated degree to the new degree.
+ *
+ * @param targetHeading the new position of the rotation
+ * @param currentHeading the current position of the rotation
+ * @return the shortest degree of rotation possible
+ */
+ fun shortestRotation(targetHeading: Double, currentHeading: Double): Double {
+ val diff = currentHeading - targetHeading
+ return when {
+ diff > 180.0f -> {
+ targetHeading + 360.0f
+ }
+ diff < -180.0f -> {
+ targetHeading - 360.0f
+ }
+ else -> {
+ targetHeading
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Defines the interface to observe the journey data uppdates.
+ */
+fun interface JourneyDataObserver {
+ /**
+ * Notifies that new data is available.
+ *
+ * @param location the next location update.
+ * @param bearing the bearing towards the next location update.
+ * @param locationAnimationDurationMs maximum duration of the animation in ms.
+ * @param bearingAnimateDurationMs
+ *
+ * @return true if new data is needed and stay subscribed. returning false will unsubscribe from further data updates.
+ */
+ fun onNewData(
+ location: Point,
+ bearing: Double,
+ locationAnimationDurationMs: Long,
+ bearingAnimateDurationMs: Long
+ ): Boolean
+}
\ No newline at end of file
diff --git a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LayerSourceProvider.kt b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LayerSourceProvider.kt
index 9ac4ba077b..757333dbf0 100644
--- a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LayerSourceProvider.kt
+++ b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LayerSourceProvider.kt
@@ -2,18 +2,15 @@ package com.mapbox.maps.plugin.locationcomponent
import com.mapbox.maps.plugin.LocationPuck2D
import com.mapbox.maps.plugin.LocationPuck3D
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.LOCATION_INDICATOR_LAYER
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.MODEL_LAYER
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.MODEL_SOURCE
-internal class LayerSourceProvider {
+internal class LayerSourceProvider(private val locationComponentInitOptions: LocationComponentInitOptions) {
fun getModelSource(locationModelLayerOptions: LocationPuck3D): ModelSourceWrapper {
if (locationModelLayerOptions.modelUri.isEmpty()) {
throw IllegalArgumentException("Model Url must not be empty!")
}
return ModelSourceWrapper(
- MODEL_SOURCE,
+ locationComponentInitOptions.puck3DSourceId,
locationModelLayerOptions.modelUri,
locationModelLayerOptions.position.map { it.toDouble() }
)
@@ -21,19 +18,19 @@ internal class LayerSourceProvider {
fun getModelLayer(locationModelLayerOptions: LocationPuck3D) =
ModelLayerWrapper(
- MODEL_LAYER,
- MODEL_SOURCE,
+ locationComponentInitOptions.puck3DLayerId,
+ locationComponentInitOptions.puck3DSourceId,
locationModelLayerOptions.modelScale.map { it.toDouble() },
locationModelLayerOptions.modelRotation.map { it.toDouble() },
locationModelLayerOptions.modelTranslation.map { it.toDouble() },
locationModelLayerOptions.modelOpacity.toDouble()
)
- fun getLocationIndicatorLayer() = LocationIndicatorLayerWrapper(LOCATION_INDICATOR_LAYER)
+ fun getLocationIndicatorLayer() = LocationIndicatorLayerWrapper(locationComponentInitOptions.puck2DLayerId)
fun getLocationIndicatorLayerRenderer(puckOptions: LocationPuck2D) =
- LocationIndicatorLayerRenderer(puckOptions, this)
+ LocationIndicatorLayerRenderer(locationComponentInitOptions, puckOptions, this)
fun getModelLayerRenderer(locationModelLayerOptions: LocationPuck3D) =
- ModelLayerRenderer(this, locationModelLayerOptions)
+ ModelLayerRenderer(locationComponentInitOptions, this, locationModelLayerOptions)
}
\ No newline at end of file
diff --git a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions.kt b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions.kt
new file mode 100644
index 0000000000..e7fcd0d91f
--- /dev/null
+++ b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationComponentInitOptions.kt
@@ -0,0 +1,235 @@
+package com.mapbox.maps.plugin.locationcomponent
+
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.BEARING_ICON
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.LOCATION_INDICATOR_LAYER
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.MODEL_LAYER
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.MODEL_SOURCE
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.SHADOW_ICON
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.TOP_ICON
+import java.util.*
+
+/**
+ * Initialisation options for location component to allow multiple instances
+ * of LocationComponent.
+ */
+public class LocationComponentInitOptions private constructor(
+ /**
+ * The layer id of the location indicator layer used to draw the 2d puck.
+ */
+ public val puck2DLayerId: String,
+ /**
+ * The layer id of the model layer used to draw the 3d puck.
+ */
+ public val puck3DLayerId: String,
+ /**
+ * The source id of the model layer used to draw the 3d puck.
+ */
+ public val puck3DSourceId: String,
+ /**
+ * The top icon image id for the 2d puck.
+ */
+ public val topIconImageId: String,
+ /**
+ * The shadow icon image id for the 2d puck.
+ */
+ public val shadowIconImageId: String,
+ /**
+ * The bearing icon image id for the 2d puck.
+ */
+ public val bearingIconImageId: String
+) {
+ /**
+ * Convert the LocationComponentInitOptions to a String.
+ */
+ public override fun toString() =
+ "LocationComponentInitOptions(puck2DLayerId=$puck2DLayerId,puck3DLayerId=$puck3DLayerId, puck3DSourceId=$puck3DSourceId, topIconImageId=$topIconImageId,shadowIconImageId=$shadowIconImageId, bearingIconImageId=$bearingIconImageId)"
+
+ /**
+ * Compares two LocationComponentOptions.
+ */
+ public override fun equals(other: Any?): Boolean = other is LocationComponentInitOptions &&
+ puck2DLayerId == other.puck2DLayerId &&
+ puck3DLayerId == other.puck3DLayerId &&
+ puck3DSourceId == other.puck3DSourceId &&
+ topIconImageId == other.topIconImageId &&
+ shadowIconImageId == other.shadowIconImageId &&
+ bearingIconImageId == other.bearingIconImageId
+
+ /**
+ * The hashcode of the LocationComponentOptions.
+ */
+ public override fun hashCode(): Int = Objects.hash(
+ puck2DLayerId, puck3DLayerId, puck3DSourceId,
+ topIconImageId, shadowIconImageId, bearingIconImageId
+ )
+
+ /**
+ * Convert LocationComponentOptions to a Builder.
+ */
+ public fun toBuilder(): Builder = Builder()
+ .setPuck2DLayerId(this.puck2DLayerId)
+ .setPuck3DLayerId(this.puck3DLayerId)
+ .setPuck3DSourceId(this.puck3DSourceId)
+ .setTopIconImageId(this.topIconImageId)
+ .setShadowIconImageId(this.shadowIconImageId)
+ .setBearingIconImageId(this.bearingIconImageId)
+
+ /**
+ * Composes and builds a [LocationComponentInitOptions] object.
+ *
+ * This is a concrete implementation of the builder design pattern.
+ *
+ * @property
+ */
+ public class Builder {
+ /**
+ * The layer id of the location indicator layer used to draw the 2d puck.
+ */
+ @set:JvmSynthetic
+ public var puck2DLayerId: String = LOCATION_INDICATOR_LAYER
+
+ /**
+ * The layer id of the model layer used to draw the 3d puck.
+ */
+ @set:JvmSynthetic
+ public var puck3DLayerId: String = MODEL_LAYER
+
+ /**
+ * The source id of the model layer used to draw the 3d puck.
+ */
+ @set:JvmSynthetic
+ public var puck3DSourceId: String = MODEL_SOURCE
+
+ /**
+ * The top icon image id for the 2d puck.
+ */
+ @set:JvmSynthetic
+ public var topIconImageId: String = TOP_ICON
+
+ /**
+ * The shadow icon image id for the 2d puck.
+ */
+ @set:JvmSynthetic
+ public var shadowIconImageId: String = SHADOW_ICON
+
+ /**
+ * The bearing icon image id for the 2d puck.
+ */
+ @set:JvmSynthetic
+ public var bearingIconImageId: String = BEARING_ICON
+
+ /**
+ * Set puck2DLayerId
+ *
+ * @param puck2DLayerId puck2DLayerId
+ * @return Builder
+ */
+ public fun setPuck2DLayerId(puck2DLayerId: String): Builder {
+ this.puck2DLayerId = puck2DLayerId
+ return this
+ }
+
+ /**
+ * Set puck3DLayerId
+ *
+ * @param puck3DLayerId puck3DLayerId
+ * @return Builder
+ */
+ public fun setPuck3DLayerId(puck3DLayerId: String): Builder {
+ this.puck3DLayerId = puck3DLayerId
+ return this
+ }
+
+ /**
+ * Set puck3DSourceId
+ *
+ * @param puck3DSourceId puck3DSourceId
+ * @return Builder
+ */
+ public fun setPuck3DSourceId(puck3DSourceId: String): Builder {
+ this.puck3DSourceId = puck3DSourceId
+ return this
+ }
+
+ /**
+ * Set topIconImageId
+ *
+ * @param topIconImageId topIconImageId
+ * @return Builder
+ */
+ public fun setTopIconImageId(topIconImageId: String): Builder {
+ this.topIconImageId = topIconImageId
+ return this
+ }
+
+ /**
+ * Set shadowIconImageId
+ *
+ * @param shadowIconImageId shadowIconImageId
+ * @return Builder
+ */
+ public fun setShadowIconImageId(shadowIconImageId: String): Builder {
+ this.shadowIconImageId = shadowIconImageId
+ return this
+ }
+
+ /**
+ * Set bearingIconImageId
+ *
+ * @param bearingIconImageId bearingIconImageId
+ * @return Builder
+ */
+ public fun setBearingIconImageId(bearingIconImageId: String): Builder {
+ this.bearingIconImageId = bearingIconImageId
+ return this
+ }
+
+ /**
+ * Returns a [LocationComponentInitOptions] reference to the object being constructed by the
+ * builder.
+ *
+ * Throws an [IllegalArgumentException] when a non-null property wasn't initialised.
+ *
+ * @return LocationComponentInitOptions
+ */
+ public fun build(): LocationComponentInitOptions {
+ return LocationComponentInitOptions(
+ puck2DLayerId, puck3DLayerId, puck3DSourceId,
+ topIconImageId, shadowIconImageId, bearingIconImageId
+ )
+ }
+ }
+
+ /**
+ * Companion object of [LocationComponentInitOptions].
+ */
+ companion object {
+ private var customLocationComponentCount = 0
+
+ /**
+ * Create a unique LocationComponentInitOptions with incremental layer/source/image ids.
+ */
+ @JvmStatic
+ fun getNextUniqueLocationComponentOptions() = LocationComponentInitOptions {
+ puck2DLayerId = "custom_location_component_2d_layer_$customLocationComponentCount"
+ puck3DLayerId = "custom_location_component_3d_layer_$customLocationComponentCount"
+ puck3DSourceId = "custom_location_component_3d_source_$customLocationComponentCount"
+ puck3DSourceId = "custom_location_component_top_icon_image_id_$customLocationComponentCount"
+ puck3DSourceId =
+ "custom_location_component_shadow_icon_image_id_$customLocationComponentCount"
+ puck3DSourceId =
+ "custom_location_component_bearing_icon_image_id_$customLocationComponentCount"
+ customLocationComponentCount++
+ }
+ }
+}
+
+/**
+ * Creates a [LocationComponentInitOptions] through a DSL-style builder.
+ *
+ * @param initializer the initialisation block
+ * @return LocationComponentInitOptions
+ */
+@JvmSynthetic
+public fun LocationComponentInitOptions(initializer: LocationComponentInitOptions.Builder.() -> Unit):
+ LocationComponentInitOptions = LocationComponentInitOptions.Builder().apply(initializer).build()
\ No newline at end of file
diff --git a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationComponentPluginImpl.kt b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationComponentPluginImpl.kt
index f7a928b72e..b863ce42fb 100644
--- a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationComponentPluginImpl.kt
+++ b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationComponentPluginImpl.kt
@@ -11,11 +11,8 @@ import com.mapbox.maps.RenderedQueryGeometry
import com.mapbox.maps.RenderedQueryOptions
import com.mapbox.maps.extension.style.StyleInterface
import com.mapbox.maps.plugin.delegates.MapDelegateProvider
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.LOCATION_INDICATOR_LAYER
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.MODEL_LAYER
import com.mapbox.maps.plugin.locationcomponent.animators.PuckAnimatorManager
import com.mapbox.maps.plugin.locationcomponent.generated.*
-import com.mapbox.maps.plugin.locationcomponent.generated.LocationComponentAttributeParser
import java.lang.ref.WeakReference
import java.util.concurrent.CopyOnWriteArraySet
@@ -23,7 +20,13 @@ import java.util.concurrent.CopyOnWriteArraySet
* Default implementation of the LocationComponentPlugin, it renders the configured location puck
* to the user's current location.
*/
-class LocationComponentPluginImpl : LocationComponentPlugin2, LocationConsumer2,
+class LocationComponentPluginImpl(
+ /**
+ * The initialisation options for the location component, defaults to the default LocationComponent configurations.
+ */
+ val locationComponentInitOptions: LocationComponentInitOptions = LocationComponentInitOptions.Builder()
+ .build()
+) : LocationComponentPlugin2, LocationConsumer2,
LocationComponentSettingsBase2() {
private lateinit var delegateProvider: MapDelegateProvider
@@ -135,8 +138,8 @@ class LocationComponentPluginImpl : LocationComponentPlugin2, LocationConsumer2,
RenderedQueryGeometry(delegateProvider.mapCameraManagerDelegate.pixelForCoordinate(point)),
RenderedQueryOptions(
listOf(
- LOCATION_INDICATOR_LAYER,
- MODEL_LAYER
+ locationComponentInitOptions.puck2DLayerId,
+ locationComponentInitOptions.puck3DLayerId
),
null
)
@@ -213,7 +216,7 @@ class LocationComponentPluginImpl : LocationComponentPlugin2, LocationConsumer2,
internalSettings.layerAbove,
internalSettings.layerBelow
),
- layerSourceProvider = LayerSourceProvider(),
+ layerSourceProvider = LayerSourceProvider(locationComponentInitOptions),
animationManager = PuckAnimatorManager(
indicatorPositionChangedListener,
indicatorBearingChangedListener,
diff --git a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationIndicatorLayerRenderer.kt b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationIndicatorLayerRenderer.kt
index 27322a2efc..a83d126be4 100644
--- a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationIndicatorLayerRenderer.kt
+++ b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/LocationIndicatorLayerRenderer.kt
@@ -5,16 +5,13 @@ import com.mapbox.bindgen.Value
import com.mapbox.geojson.Point
import com.mapbox.maps.extension.style.StyleInterface
import com.mapbox.maps.plugin.LocationPuck2D
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.BEARING_ICON
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.LOCATION_INDICATOR_LAYER
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.SHADOW_ICON
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.TOP_ICON
import com.mapbox.maps.plugin.locationcomponent.utils.BitmapUtils
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Locale
internal class LocationIndicatorLayerRenderer(
+ private val locationComponentInitOptions: LocationComponentInitOptions,
private val puckOptions: LocationPuck2D,
layerSourceProvider: LayerSourceProvider
) : LocationLayerRenderer {
@@ -28,7 +25,7 @@ internal class LocationIndicatorLayerRenderer(
}
override fun isRendererInitialised(): Boolean {
- return style?.styleLayerExists(LOCATION_INDICATOR_LAYER) ?: false
+ return style?.styleLayerExists(locationComponentInitOptions.puck2DLayerId) ?: false
}
override fun addLayers(positionManager: LocationComponentPositionManager) {
@@ -76,22 +73,22 @@ internal class LocationIndicatorLayerRenderer(
private fun setupBitmaps() {
puckOptions.topImage?.let { BitmapUtils.getBitmapFromDrawable(it) }
- ?.let { style?.addImage(TOP_ICON, it) }
+ ?.let { style?.addImage(locationComponentInitOptions.topIconImageId, it) }
puckOptions.bearingImage?.let { BitmapUtils.getBitmapFromDrawable(it) }
- ?.let { style?.addImage(BEARING_ICON, it) }
+ ?.let { style?.addImage(locationComponentInitOptions.bearingIconImageId, it) }
puckOptions.shadowImage?.let { BitmapUtils.getBitmapFromDrawable(it) }
- ?.let { style?.addImage(SHADOW_ICON, it) }
- layer.topImage(TOP_ICON)
- layer.bearingImage(BEARING_ICON)
- layer.shadowImage(SHADOW_ICON)
+ ?.let { style?.addImage(locationComponentInitOptions.shadowIconImageId, it) }
+ layer.topImage(locationComponentInitOptions.topIconImageId)
+ layer.bearingImage(locationComponentInitOptions.bearingIconImageId)
+ layer.shadowImage(locationComponentInitOptions.shadowIconImageId)
layer.opacity(puckOptions.opacity.toDouble())
}
override fun clearBitmaps() {
- style?.removeStyleImage(TOP_ICON)
- style?.removeStyleImage(BEARING_ICON)
- style?.removeStyleImage(SHADOW_ICON)
+ style?.removeStyleImage(locationComponentInitOptions.topIconImageId)
+ style?.removeStyleImage(locationComponentInitOptions.bearingIconImageId)
+ style?.removeStyleImage(locationComponentInitOptions.shadowIconImageId)
}
override fun updateStyle(style: StyleInterface) {
diff --git a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/ModelLayerRenderer.kt b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/ModelLayerRenderer.kt
index 5c2618abe7..2cfe3358d9 100644
--- a/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/ModelLayerRenderer.kt
+++ b/plugin-locationcomponent/src/main/java/com/mapbox/maps/plugin/locationcomponent/ModelLayerRenderer.kt
@@ -6,10 +6,9 @@ import com.mapbox.bindgen.Value
import com.mapbox.geojson.Point
import com.mapbox.maps.extension.style.StyleInterface
import com.mapbox.maps.plugin.LocationPuck3D
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.MODEL_LAYER
-import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants.MODEL_SOURCE
internal class ModelLayerRenderer(
+ private val locationComponentInitOptions: LocationComponentInitOptions,
layerSourceProvider: LayerSourceProvider,
private val locationModelLayerOptions: LocationPuck3D
) : LocationLayerRenderer {
@@ -32,11 +31,11 @@ internal class ModelLayerRenderer(
}
private fun isLayerInitialised(): Boolean {
- return style?.styleLayerExists(MODEL_LAYER) ?: false
+ return style?.styleLayerExists(locationComponentInitOptions.puck3DLayerId) ?: false
}
private fun isSourceInitialised(): Boolean {
- return style?.styleSourceExists(MODEL_SOURCE) ?: false
+ return style?.styleSourceExists(locationComponentInitOptions.puck3DSourceId) ?: false
}
override fun addLayers(positionManager: LocationComponentPositionManager) {
diff --git a/plugin-viewport/src/main/kotlin/com/mapbox/maps/plugin/viewport/ViewportPluginImpl.kt b/plugin-viewport/src/main/kotlin/com/mapbox/maps/plugin/viewport/ViewportPluginImpl.kt
index b2a4fe055a..315cff893f 100644
--- a/plugin-viewport/src/main/kotlin/com/mapbox/maps/plugin/viewport/ViewportPluginImpl.kt
+++ b/plugin-viewport/src/main/kotlin/com/mapbox/maps/plugin/viewport/ViewportPluginImpl.kt
@@ -10,16 +10,11 @@ import com.mapbox.maps.plugin.animation.Cancelable
import com.mapbox.maps.plugin.animation.MapAnimationOwnerRegistry
import com.mapbox.maps.plugin.animation.camera
import com.mapbox.maps.plugin.delegates.MapDelegateProvider
-import com.mapbox.maps.plugin.viewport.data.DefaultViewportTransitionOptions
-import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions
-import com.mapbox.maps.plugin.viewport.data.OverviewViewportStateOptions
-import com.mapbox.maps.plugin.viewport.data.ViewportOptions
-import com.mapbox.maps.plugin.viewport.data.ViewportStatusChangeReason
-import com.mapbox.maps.plugin.viewport.state.FollowPuckViewportState
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin2
+import com.mapbox.maps.plugin.viewport.data.*
+import com.mapbox.maps.plugin.viewport.state.*
import com.mapbox.maps.plugin.viewport.state.FollowPuckViewportStateImpl
-import com.mapbox.maps.plugin.viewport.state.OverviewViewportState
import com.mapbox.maps.plugin.viewport.state.OverviewViewportStateImpl
-import com.mapbox.maps.plugin.viewport.state.ViewportState
import com.mapbox.maps.plugin.viewport.transition.DefaultViewportTransition
import com.mapbox.maps.plugin.viewport.transition.DefaultViewportTransitionImpl
import com.mapbox.maps.plugin.viewport.transition.ImmediateViewportTransition
@@ -239,6 +234,19 @@ class ViewportPluginImpl(private val handler: Handler = Handler(Looper.getMainLo
return FollowPuckViewportStateImpl(delegateProvider, options)
}
+ /**
+ * Create a new [MultiPuckViewportState] instance with provided [MultiPuckViewportStateOptions].
+ *
+ * @param options The desired [MultiPuckViewportStateOptions], defaults to [MultiPuckViewportStateOptions] that's initialised with default parameters.
+ * @return The newly-created [MultiPuckViewportState] instance.
+ */
+ override fun makeMultiPuckViewportState(
+ options: MultiPuckViewportStateOptions,
+ locationComponents: List
+ ): MultiPuckViewportState {
+ return MultiPuckViewportStateImpl(delegateProvider, options, locationComponents)
+ }
+
/**
* Create an [OverviewViewportState] instance with provided [OverviewViewportStateOptions].
*
diff --git a/plugin-viewport/src/main/kotlin/com/mapbox/maps/plugin/viewport/state/MultiPuckViewportStateImpl.kt b/plugin-viewport/src/main/kotlin/com/mapbox/maps/plugin/viewport/state/MultiPuckViewportStateImpl.kt
new file mode 100644
index 0000000000..4f6ff31c6c
--- /dev/null
+++ b/plugin-viewport/src/main/kotlin/com/mapbox/maps/plugin/viewport/state/MultiPuckViewportStateImpl.kt
@@ -0,0 +1,283 @@
+package com.mapbox.maps.plugin.viewport.state
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import androidx.annotation.VisibleForTesting
+import com.mapbox.geojson.MultiPoint
+import com.mapbox.geojson.Point
+import com.mapbox.maps.CameraOptions
+import com.mapbox.maps.EdgeInsets
+import com.mapbox.maps.logW
+import com.mapbox.maps.plugin.animation.Cancelable
+import com.mapbox.maps.plugin.animation.camera
+import com.mapbox.maps.plugin.delegates.MapDelegateProvider
+import com.mapbox.maps.plugin.locationcomponent.*
+import com.mapbox.maps.plugin.viewport.DEFAULT_STATE_ANIMATION_DURATION_MS
+import com.mapbox.maps.plugin.viewport.data.MultiPuckViewportStateBearing
+import com.mapbox.maps.plugin.viewport.data.MultiPuckViewportStateOptions
+import com.mapbox.maps.plugin.viewport.transition.MapboxViewportTransitionFactory
+import com.mapbox.maps.threading.AnimationThreadController
+import java.util.concurrent.*
+import kotlin.math.abs
+
+/**
+ * The actual implementation of [FollowPuckViewportState] that follows the location component's puck position.
+ *
+ * Note: [LocationComponentPlugin] should be enabled to use this viewport state.
+ */
+internal class MultiPuckViewportStateImpl(
+ mapDelegateProvider: MapDelegateProvider,
+ initialOptions: MultiPuckViewportStateOptions,
+ private val locationComponents: List,
+ private val transitionFactory: MapboxViewportTransitionFactory = MapboxViewportTransitionFactory(
+ mapDelegateProvider
+ )
+) : MultiPuckViewportState {
+ private val cameraPlugin = mapDelegateProvider.mapPluginProviderDelegate.camera
+ private val cameraDelegate = mapDelegateProvider.mapCameraManagerDelegate
+ private val dataSourceUpdateObservers = CopyOnWriteArraySet()
+
+ private var runningAnimation: AnimatorSet? = null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var isFollowingStateRunning = false
+ private var isObservingLocationUpdates = false
+ private var lastZoom: Double? = null
+
+ private val lastLocations: MutableMap =
+ locationComponents.associateBy(
+ { it },
+ { null }
+ ).toMutableMap()
+
+ private val lastBearings: MutableMap =
+ locationComponents.associateBy(
+ { it },
+ { null }
+ ).toMutableMap()
+ private val locationComponentIndicatorPositionChangedListeners: Map =
+ locationComponents.associateBy(
+ { it },
+ {
+ OnIndicatorPositionChangedListener { point ->
+ lastLocations[it] = point
+ notifyLatestViewportData()
+ }
+ }
+ )
+ private val locationComponentIndicatorBearingChangedListeners: Map =
+ locationComponents.associateBy(
+ { it },
+ {
+ OnIndicatorBearingChangedListener { bearing ->
+ lastBearings[it] = bearing
+ notifyLatestViewportData()
+ }
+ }
+ )
+
+ private fun notifyLatestViewportData() {
+ if (lastLocations.values.filterNotNull().isNotEmpty() &&
+ (
+ options.bearing is MultiPuckViewportStateBearing.Constant ||
+ lastBearings.values.filterNotNull().isNotEmpty()
+ )
+ ) {
+ val viewportData = evaluateViewportData()
+ if (isFollowingStateRunning) {
+ // Use instant update here since the location updates are already interpolated by the location component plugin
+ updateFrame(viewportData, true)
+ }
+ dataSourceUpdateObservers.forEach {
+ if (!it.onNewData(viewportData)) {
+ dataSourceUpdateObservers.remove(it)
+ }
+ }
+ }
+ }
+
+ private fun evaluateViewportData(): CameraOptions {
+ val cameraOptions = cameraDelegate.cameraForGeometry(
+ MultiPoint.fromLngLats(lastLocations.values.filterNotNull()),
+ options.padding ?: EdgeInsets(0.0, 0.0, 0.0, 0.0),
+ with(options.bearing) {
+ when (this) {
+ is MultiPuckViewportStateBearing.Constant -> bearing
+ is MultiPuckViewportStateBearing.SyncWithLocationPuck -> lastBearings[locationComponent] ?: 0.0
+ else -> 0.0
+ }
+ },
+ options.pitch
+ )
+ val targetZoom = cameraOptions.zoom!!
+ val nextZoom = lastZoom?.let {
+ if (abs(targetZoom - it) > ZOOM_UPDATE_STEP) {
+ if (targetZoom > it) {
+ it + ZOOM_UPDATE_STEP
+ } else {
+ it - ZOOM_UPDATE_STEP
+ }
+ } else {
+ targetZoom
+ }
+ } ?: targetZoom
+ lastZoom = nextZoom
+
+ return cameraOptions.toBuilder().zoom(nextZoom).build()
+ }
+
+ private fun addIndicatorListenerIfNeeded() {
+ if (!isObservingLocationUpdates) {
+ locationComponentIndicatorPositionChangedListeners.entries.forEach {
+ it.key.addOnIndicatorPositionChangedListener(it.value)
+ }
+ locationComponentIndicatorBearingChangedListeners.entries.forEach {
+ it.key.addOnIndicatorBearingChangedListener(it.value)
+ }
+ isObservingLocationUpdates = true
+ }
+ }
+
+ private fun removeIndicatorListenerIfNeeded() {
+ if (isObservingLocationUpdates && dataSourceUpdateObservers.isEmpty() && !isFollowingStateRunning) {
+ locationComponentIndicatorPositionChangedListeners.entries.forEach {
+ it.key.removeOnIndicatorPositionChangedListener(it.value)
+ }
+ locationComponentIndicatorBearingChangedListeners.entries.forEach {
+ it.key.removeOnIndicatorBearingChangedListener(it.value)
+ }
+ isObservingLocationUpdates = false
+ }
+ }
+
+ /**
+ * Describes the configuration options of the state.
+ */
+ override var options: MultiPuckViewportStateOptions = initialOptions
+ set(value) {
+ field = value
+ notifyLatestViewportData()
+ }
+
+ /**
+ * Observer the new camera options produced by the [ViewportState], it can be used to get the
+ * latest state [CameraOptions] for [ViewportTransition].
+ *
+ * @param viewportStateDataObserver observer that observe new viewport data.
+ * @return a handle that cancels current observation.
+ */
+ override fun observeDataSource(viewportStateDataObserver: ViewportStateDataObserver): Cancelable {
+ checkLocationComponentEnablement()
+ addIndicatorListenerIfNeeded()
+ dataSourceUpdateObservers.add(viewportStateDataObserver)
+ return Cancelable {
+ dataSourceUpdateObservers.remove(viewportStateDataObserver)
+ removeIndicatorListenerIfNeeded()
+ }
+ }
+
+ private fun checkLocationComponentEnablement() {
+ if (locationComponents.none { it.enabled }) {
+ logW(
+ TAG,
+ "At least one Location component is required to be enabled to use MultiPuckViewportState, otherwise there would be no MultiPuckViewportState updates or ViewportTransition updates towards the MultiPuckViewportState."
+ )
+ }
+ }
+
+ /**
+ * Start updating the camera for the current [ViewportState].
+ */
+ override fun startUpdatingCamera() {
+ checkLocationComponentEnablement()
+ addIndicatorListenerIfNeeded()
+ isFollowingStateRunning = true
+ }
+
+ /**
+ * Stop updating the camera for the current [ViewportState].
+ */
+ override fun stopUpdatingCamera() {
+ isFollowingStateRunning = false
+ cancelAnimation()
+ removeIndicatorListenerIfNeeded()
+ }
+
+ private fun cancelAnimation() {
+ AnimationThreadController.postOnAnimatorThread {
+ runningAnimation?.apply {
+ cancel()
+ childAnimations.forEach {
+ cameraPlugin.unregisterAnimators(it as ValueAnimator)
+ }
+ }
+ runningAnimation = null
+ }
+ }
+
+ private fun startAnimation(
+ animatorSet: AnimatorSet,
+ instant: Boolean,
+ ) {
+ cancelAnimation()
+ animatorSet.childAnimations.forEach {
+ cameraPlugin.registerAnimators(it as ValueAnimator)
+ }
+ if (instant) {
+ animatorSet.duration = 0
+ }
+ AnimationThreadController.postOnAnimatorThread {
+ animatorSet.start()
+ runningAnimation = animatorSet
+ }
+ }
+
+ private fun finishAnimation(animatorSet: AnimatorSet?) {
+ animatorSet?.childAnimations?.forEach {
+ cameraPlugin.unregisterAnimators(it as ValueAnimator)
+ }
+ if (runningAnimation == animatorSet) {
+ runningAnimation = null
+ }
+ }
+
+ private fun updateFrame(
+ cameraOptions: CameraOptions,
+ instant: Boolean = false,
+ onComplete: ((isFinished: Boolean) -> Unit)? = null
+ ) {
+ startAnimation(
+ transitionFactory.transitionLinear(cameraOptions, DEFAULT_STATE_ANIMATION_DURATION_MS)
+ .apply {
+ addListener(
+ object : Animator.AnimatorListener {
+ private var isCanceled = false
+ override fun onAnimationStart(animation: Animator) {
+ // no-ops
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ onComplete?.invoke(!isCanceled)
+ finishAnimation(this@apply)
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ isCanceled = true
+ }
+
+ override fun onAnimationRepeat(animation: Animator) {
+ // no-ops
+ }
+ }
+ )
+ },
+ instant
+ )
+ }
+
+ private companion object {
+ const val TAG = "MultiPuckViewportStateImpl"
+ const val ZOOM_UPDATE_STEP = 0.001
+ }
+}
\ No newline at end of file
diff --git a/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/ViewportPlugin.kt b/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/ViewportPlugin.kt
index 8125dc845a..967561690a 100644
--- a/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/ViewportPlugin.kt
+++ b/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/ViewportPlugin.kt
@@ -1,11 +1,10 @@
package com.mapbox.maps.plugin.viewport
import com.mapbox.maps.plugin.MapPlugin
-import com.mapbox.maps.plugin.viewport.data.DefaultViewportTransitionOptions
-import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions
-import com.mapbox.maps.plugin.viewport.data.OverviewViewportStateOptions
-import com.mapbox.maps.plugin.viewport.data.ViewportOptions
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin2
+import com.mapbox.maps.plugin.viewport.data.*
import com.mapbox.maps.plugin.viewport.state.FollowPuckViewportState
+import com.mapbox.maps.plugin.viewport.state.MultiPuckViewportState
import com.mapbox.maps.plugin.viewport.state.OverviewViewportState
import com.mapbox.maps.plugin.viewport.state.ViewportState
import com.mapbox.maps.plugin.viewport.transition.DefaultViewportTransition
@@ -121,6 +120,17 @@ interface ViewportPlugin : MapPlugin {
options: FollowPuckViewportStateOptions = FollowPuckViewportStateOptions.Builder().build()
): FollowPuckViewportState
+ /**
+ * Create a new [MultiPuckViewportState] instance with provided [MultiPuckViewportStateOptions].
+ *
+ * @param options The desired [MultiPuckViewportStateOptions], defaults to [MultiPuckViewportStateOptions] that's initialised with default parameters.
+ * @return The newly-created [MultiPuckViewportState] instance.
+ */
+ fun makeMultiPuckViewportState(
+ options: MultiPuckViewportStateOptions = MultiPuckViewportStateOptions.Builder().build(),
+ locationComponents: List
+ ): MultiPuckViewportState
+
/**
* Create a new [OverviewViewportState] instance with provided [OverviewViewportStateOptions].
*
diff --git a/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/data/MultiPuckViewportStateBearing.kt b/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/data/MultiPuckViewportStateBearing.kt
new file mode 100644
index 0000000000..260046dde7
--- /dev/null
+++ b/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/data/MultiPuckViewportStateBearing.kt
@@ -0,0 +1,63 @@
+package com.mapbox.maps.plugin.viewport.data
+
+import com.mapbox.maps.CameraOptions
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin2
+import com.mapbox.maps.plugin.viewport.state.MultiPuckViewportState
+import java.util.*
+
+/**
+ * Describes different ways that [MultiPuckViewportState] can obtain values to use when setting
+ * [CameraOptions.bearing].
+ */
+sealed class MultiPuckViewportStateBearing {
+ /**
+ * The [MultiPuckViewportState] sets the camera bearing to the constant value on every frame.
+ *
+ * @param bearing The bearing that the [MultiPuckViewportState] uses to generate camera updates.
+ */
+ class Constant(val bearing: Double) : MultiPuckViewportStateBearing() {
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ */
+ override fun equals(other: Any?) = other is Constant && bearing == other.bearing
+
+ /**
+ * Returns a hash code value for the object.
+ */
+ override fun hashCode() = Objects.hash(bearing)
+
+ /**
+ * Returns a String for the object.
+ */
+ override fun toString() = "MultiPuckViewportStateBearing#Constant(bearing=$bearing)"
+ }
+
+ /**
+ * The [MultiPuckViewportState] sets the camera bearing to the same as the location puck's bearing.
+ *
+ * When set to this mode, the viewport's bearing is driven by the location, thus guarantees
+ * the synchronization of the location puck and camera position.
+ */
+ class SyncWithLocationPuck(
+ /**
+ * The location component instance to be synced with.
+ */
+ val locationComponent: LocationComponentPlugin2
+ ) : MultiPuckViewportStateBearing() {
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ */
+ override fun equals(other: Any?) =
+ other is SyncWithLocationPuck && locationComponent == other.locationComponent
+
+ /**
+ * Returns a hash code value for the object.
+ */
+ override fun hashCode() = Objects.hash(locationComponent)
+
+ /**
+ * Returns a String for the object.
+ */
+ override fun toString() = "MultiPuckViewportStateBearing#SyncWithLocationPuck"
+ }
+}
\ No newline at end of file
diff --git a/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/data/MultiPuckViewportStateOptions.kt b/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/data/MultiPuckViewportStateOptions.kt
new file mode 100644
index 0000000000..74ab942e7d
--- /dev/null
+++ b/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/data/MultiPuckViewportStateOptions.kt
@@ -0,0 +1,135 @@
+package com.mapbox.maps.plugin.viewport.data
+
+import com.mapbox.maps.CameraOptions
+import com.mapbox.maps.EdgeInsets
+import com.mapbox.maps.plugin.viewport.DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_PITCH
+import com.mapbox.maps.plugin.viewport.DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_ZOOM
+import com.mapbox.maps.plugin.viewport.state.FollowPuckViewportState
+import java.util.Objects
+
+/**
+ * Configuration options that impact the [FollowPuckViewportState].
+ *
+ * Each of the [CameraOptions] related properties is optional, so that the state can be configured
+ * to only modify certain aspects of the camera if desired. This can be used, to achieve effects like
+ * allowing zoom gestures to work simultaneously with [FollowPuckViewportState].
+ *
+ * @see [ViewportOptions.transitionsToIdleUponUserInteraction]
+ */
+class MultiPuckViewportStateOptions private constructor(
+ /**
+ * The value to use for setting [CameraOptions.padding]. If null, padding will not be modified by
+ * the [FollowPuckViewportState].
+ *
+ * Defaults to 0 padding.
+ */
+ val padding: EdgeInsets?,
+ /**
+ * The value to use for setting [CameraOptions.zoom]. If null, zoom will not be modified by
+ * the [FollowPuckViewportState].
+ *
+ * Defaults to [DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_ZOOM].
+ */
+ val zoom: Double?,
+ /**
+ * Indicates how to obtain the value to use for [CameraOptions.bearing] when setting the camera.
+ * If set to null, bearing will not be modified by the [FollowPuckViewportState].
+ *
+ * Defaults to [MultiPuckViewportStateBearing.Constant(0)]
+ */
+ val bearing: MultiPuckViewportStateBearing?,
+ /**
+ * The value to use for setting [CameraOptions.pitch]. If null, pitch will not be modified by
+ * the [FollowPuckViewportState].
+ *
+ * Defaults to [DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_PITCH] degrees.
+ */
+ val pitch: Double?,
+) {
+ /**
+ * Returns a builder that created the [MultiPuckViewportStateOptions]
+ */
+ fun toBuilder() = Builder().padding(padding).zoom(zoom).bearing(bearing).pitch(pitch)
+
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ */
+ override fun equals(other: Any?) = other is MultiPuckViewportStateOptions &&
+ padding == other.padding &&
+ Objects.equals(zoom, other.zoom) &&
+ bearing == other.bearing &&
+ Objects.equals(pitch, other.pitch)
+
+ /**
+ * Returns a hash code value for the object.
+ */
+ override fun hashCode() = Objects.hash(padding, zoom, bearing, pitch)
+
+ /**
+ * Returns a String for the object.
+ */
+ override fun toString() =
+ "FollowPuckViewportStateOptions(padding=$padding, zoom=$zoom, bearing=$bearing, pitch=$pitch)"
+
+ /**
+ * Builder for [MultiPuckViewportStateOptions]
+ */
+ class Builder {
+ private var padding: EdgeInsets? = EdgeInsets(0.0, 0.0, 0.0, 0.0)
+ private var zoom: Double? = DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_ZOOM
+ private var bearing: MultiPuckViewportStateBearing? =
+ MultiPuckViewportStateBearing.Constant(0.0)
+ private var pitch: Double? = DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_PITCH
+
+ /**
+ * The value to use for setting [CameraOptions.padding]. If null, padding will not be modified by
+ * the [FollowPuckViewportState].
+ *
+ * Defaults to 0 padding.
+ */
+ fun padding(padding: EdgeInsets?) = apply {
+ this.padding = padding
+ }
+
+ /**
+ * The value to use for setting [CameraOptions.zoom]. If null, zoom will not be modified by
+ * the [FollowPuckViewportState].
+ *
+ * Defaults to [DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_ZOOM].
+ */
+ fun zoom(zoom: Double?) = apply {
+ this.zoom = zoom
+ }
+
+ /**
+ * Indicates how to obtain the value to use for [CameraOptions.bearing] when setting the camera.
+ * If set to null, bearing will not be modified by the [MultiPuckViewportState].
+ *
+ * Defaults to [MultiPuckViewportStateBearing.Constant(0)]
+ */
+ fun bearing(options: MultiPuckViewportStateBearing?) = apply {
+ this.bearing = options
+ }
+
+ /**
+ * The value to use for setting [CameraOptions.pitch]. If null, pitch will not be modified by
+ * the [FollowPuckViewportState].
+ *
+ * Defaults to [DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_PITCH] degrees.
+ */
+ fun pitch(pitch: Double?) = apply {
+ this.pitch = pitch
+ }
+
+ /**
+ * Builds [MultiPuckViewportStateOptions]
+ */
+ fun build() =
+ MultiPuckViewportStateOptions(
+ padding,
+ zoom,
+ bearing,
+ pitch,
+ )
+ }
+}
\ No newline at end of file
diff --git a/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/state/MultiPuckViewportState.kt b/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/state/MultiPuckViewportState.kt
new file mode 100644
index 0000000000..88b9326c4a
--- /dev/null
+++ b/sdk-base/src/main/java/com/mapbox/maps/plugin/viewport/state/MultiPuckViewportState.kt
@@ -0,0 +1,21 @@
+package com.mapbox.maps.plugin.viewport.state
+
+import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin
+import com.mapbox.maps.plugin.viewport.ViewportPlugin
+import com.mapbox.maps.plugin.viewport.data.MultiPuckViewportStateOptions
+
+/**
+ * The [ViewportState] that tracks the location puck's position.
+ *
+ * Use [ViewportPlugin.makeMultiPuckViewportState] to create instances of [MultiPuckViewportState].
+ *
+ * Note: [LocationComponentPlugin] should be enabled to use this viewport state, and Users are
+ * responsible to create the viewport states and keep a reference to these states for
+ * future operations.
+ */
+interface MultiPuckViewportState : ViewportState {
+ /**
+ * Describes the configuration options of the state.
+ */
+ var options: MultiPuckViewportStateOptions
+}
\ No newline at end of file