summaryrefslogtreecommitdiff
path: root/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location')
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraCompassBearingAnimator.java24
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraGpsBearingAnimator.java23
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraLatLngAnimator.java25
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassEngine.java65
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassListener.java33
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LatLngEvaluator.java19
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerAccuracyAnimator.java24
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerBitmapProvider.java33
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerCompassBearingAnimator.java24
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerFeatureProvider.java26
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerGpsBearingAnimator.java23
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerLatLngAnimator.java25
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java107
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java375
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java263
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java1019
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentCompassEngine.java267
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java66
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java1561
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java394
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java92
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxCameraAnimatorAdapter.java38
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxFloatAnimator.java17
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxLatLngAnimator.java19
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraMoveInvalidateListener.java7
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraTrackingChangedListener.java21
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationComponentClickListener.java11
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationComponentLongClickListener.java11
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationStaleListener.java13
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/StaleStateManager.java80
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/TiltAnimator.java27
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java102
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/ZoomAnimator.java29
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/CameraMode.java66
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/RenderMode.java45
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/package-info.java4
36 files changed, 4978 insertions, 0 deletions
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraCompassBearingAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraCompassBearingAnimator.java
new file mode 100644
index 0000000000..ea1817ab5e
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraCompassBearingAnimator.java
@@ -0,0 +1,24 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.ValueAnimator;
+
+import java.util.List;
+
+class CameraCompassBearingAnimator extends MapboxFloatAnimator<MapboxAnimator.OnCameraAnimationsValuesChangeListener> {
+ CameraCompassBearingAnimator(Float previous, Float target,
+ List<OnCameraAnimationsValuesChangeListener> updateListeners) {
+ super(previous, target, updateListeners);
+ }
+
+ @Override
+ int provideAnimatorType() {
+ return ANIMATOR_CAMERA_COMPASS_BEARING;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (OnCameraAnimationsValuesChangeListener listener : updateListeners) {
+ listener.onNewCompassBearingValue((Float) animation.getAnimatedValue());
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraGpsBearingAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraGpsBearingAnimator.java
new file mode 100644
index 0000000000..f46cf805ff
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraGpsBearingAnimator.java
@@ -0,0 +1,23 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.ValueAnimator;
+
+import java.util.List;
+
+class CameraGpsBearingAnimator extends MapboxFloatAnimator<MapboxAnimator.OnCameraAnimationsValuesChangeListener> {
+ CameraGpsBearingAnimator(Float previous, Float target, List<OnCameraAnimationsValuesChangeListener> updateListeners) {
+ super(previous, target, updateListeners);
+ }
+
+ @Override
+ int provideAnimatorType() {
+ return ANIMATOR_CAMERA_GPS_BEARING;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (OnCameraAnimationsValuesChangeListener listener : updateListeners) {
+ listener.onNewGpsBearingValue((Float) animation.getAnimatedValue());
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraLatLngAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraLatLngAnimator.java
new file mode 100644
index 0000000000..533abfc335
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CameraLatLngAnimator.java
@@ -0,0 +1,25 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.ValueAnimator;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import java.util.List;
+
+class CameraLatLngAnimator extends MapboxLatLngAnimator<MapboxAnimator.OnCameraAnimationsValuesChangeListener> {
+ CameraLatLngAnimator(LatLng previous, LatLng target, List<OnCameraAnimationsValuesChangeListener> updateListeners) {
+ super(previous, target, updateListeners);
+ }
+
+ @Override
+ int provideAnimatorType() {
+ return ANIMATOR_CAMERA_LATLNG;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (OnCameraAnimationsValuesChangeListener listener : updateListeners) {
+ listener.onNewLatLngValue((LatLng) animation.getAnimatedValue());
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassEngine.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassEngine.java
new file mode 100644
index 0000000000..3691bdc0ea
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassEngine.java
@@ -0,0 +1,65 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.support.annotation.NonNull;
+
+import com.mapbox.mapboxsdk.location.modes.CameraMode;
+import com.mapbox.mapboxsdk.location.modes.RenderMode;
+
+/**
+ * Interface defining the source of compass heading data that is
+ * consumed by the {@link LocationComponent} when in compass related
+ * {@link RenderMode} or
+ * {@link CameraMode}s.
+ */
+public interface CompassEngine {
+
+ /**
+ * Adds a {@link CompassListener} that can be used to
+ * receive heading and state changes.
+ *
+ * @param compassListener to be added
+ */
+ void addCompassListener(@NonNull CompassListener compassListener);
+
+ /**
+ * Removes a {@link CompassListener} that can be used to
+ * receive heading and state changes.
+ *
+ * @param compassListener to be removed
+ */
+ void removeCompassListener(@NonNull CompassListener compassListener);
+
+ /**
+ * Returns the last heading value produced and pushed via
+ * a compass listener.
+ *
+ * @return last heading value
+ */
+ float getLastHeading();
+
+ /**
+ * Provides the last know accuracy status from the sensor manager.
+ * <p>
+ * An integer value which is identical to the {@code SensorManager} class constants:
+ * <ul>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_NO_CONTACT}</li>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_UNRELIABLE}</li>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_LOW}</li>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_MEDIUM}</li>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_HIGH}</li>
+ * </ul>
+ *
+ * @return last accuracy status
+ */
+ int getLastAccuracySensorStatus();
+
+ /**
+ * Lifecycle method that can be used for adding or releasing resources.
+ */
+ void onStart();
+
+ /**
+ * Lifecycle method that can be used for adding or releasing resources.
+ */
+ void onStop();
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassListener.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassListener.java
new file mode 100644
index 0000000000..3e5eb7f258
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassListener.java
@@ -0,0 +1,33 @@
+package com.mapbox.mapboxsdk.location;
+
+/**
+ * Callbacks related to the compass
+ */
+public interface CompassListener {
+
+ /**
+ * Callback's invoked when a new compass update occurs. You can listen into the compass updates
+ * using {@link LocationComponent#addCompassListener(CompassListener)} and implementing these
+ * callbacks. Note that this interface is also used internally to to update the UI chevron/arrow.
+ *
+ * @param userHeading the new compass heading
+ */
+ void onCompassChanged(float userHeading);
+
+ /**
+ * This gets invoked when the compass accuracy status changes from one value to another. It
+ * provides an integer value which is identical to the {@code SensorManager} class constants:
+ * <ul>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_NO_CONTACT}</li>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_UNRELIABLE}</li>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_LOW}</li>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_MEDIUM}</li>
+ * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_HIGH}</li>
+ * </ul>
+ *
+ * @param compassStatus the new accuracy of this sensor, one of
+ * {@code SensorManager.SENSOR_STATUS_*}
+ */
+ void onCompassAccuracyChange(int compassStatus);
+}
+
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LatLngEvaluator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LatLngEvaluator.java
new file mode 100644
index 0000000000..fa966641fd
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LatLngEvaluator.java
@@ -0,0 +1,19 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.TypeEvaluator;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+class LatLngEvaluator implements TypeEvaluator<LatLng> {
+
+ private final LatLng latLng = new LatLng();
+
+ @Override
+ public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
+ latLng.setLatitude(startValue.getLatitude()
+ + ((endValue.getLatitude() - startValue.getLatitude()) * fraction));
+ latLng.setLongitude(startValue.getLongitude()
+ + ((endValue.getLongitude() - startValue.getLongitude()) * fraction));
+ return latLng;
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerAccuracyAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerAccuracyAnimator.java
new file mode 100644
index 0000000000..e893f0fee9
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerAccuracyAnimator.java
@@ -0,0 +1,24 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.ValueAnimator;
+
+import java.util.List;
+
+class LayerAccuracyAnimator extends MapboxFloatAnimator<MapboxAnimator.OnLayerAnimationsValuesChangeListener> {
+
+ LayerAccuracyAnimator(Float previous, Float target, List<OnLayerAnimationsValuesChangeListener> updateListeners) {
+ super(previous, target, updateListeners);
+ }
+
+ @Override
+ int provideAnimatorType() {
+ return ANIMATOR_LAYER_ACCURACY;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (OnLayerAnimationsValuesChangeListener listener : updateListeners) {
+ listener.onNewAccuracyRadiusValue((Float) animation.getAnimatedValue());
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerBitmapProvider.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerBitmapProvider.java
new file mode 100644
index 0000000000..6c9f063c18
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerBitmapProvider.java
@@ -0,0 +1,33 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.DrawableRes;
+import android.support.v4.content.ContextCompat;
+
+import com.mapbox.mapboxsdk.R;
+
+import static com.mapbox.mapboxsdk.location.Utils.generateShadow;
+import static com.mapbox.mapboxsdk.location.Utils.getBitmapFromDrawable;
+import static com.mapbox.mapboxsdk.location.Utils.getDrawable;
+
+class LayerBitmapProvider {
+
+ private final Context context;
+
+ LayerBitmapProvider(Context context) {
+ this.context = context;
+ }
+
+ Bitmap generateBitmap(@DrawableRes int drawableRes, @ColorInt Integer tintColor) {
+ Drawable drawable = getDrawable(context, drawableRes, tintColor);
+ return getBitmapFromDrawable(drawable);
+ }
+
+ Bitmap generateShadowBitmap(LocationComponentOptions options) {
+ Drawable shadowDrawable = ContextCompat.getDrawable(context, R.drawable.mapbox_user_icon_shadow);
+ return generateShadow(shadowDrawable, options.elevation());
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerCompassBearingAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerCompassBearingAnimator.java
new file mode 100644
index 0000000000..e75eaca2b5
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerCompassBearingAnimator.java
@@ -0,0 +1,24 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.ValueAnimator;
+
+import java.util.List;
+
+class LayerCompassBearingAnimator extends MapboxFloatAnimator<MapboxAnimator.OnLayerAnimationsValuesChangeListener> {
+ LayerCompassBearingAnimator(Float previous,
+ Float target, List<OnLayerAnimationsValuesChangeListener> updateListeners) {
+ super(previous, target, updateListeners);
+ }
+
+ @Override
+ int provideAnimatorType() {
+ return ANIMATOR_LAYER_COMPASS_BEARING;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (OnLayerAnimationsValuesChangeListener listener : updateListeners) {
+ listener.onNewCompassBearingValue((Float) animation.getAnimatedValue());
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerFeatureProvider.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerFeatureProvider.java
new file mode 100644
index 0000000000..0be38dc4db
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerFeatureProvider.java
@@ -0,0 +1,26 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.Point;
+
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_COMPASS_BEARING;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_LOCATION_STALE;
+
+class LayerFeatureProvider {
+
+ @NonNull
+ Feature generateLocationFeature(@Nullable Feature locationFeature, LocationComponentOptions options) {
+ if (locationFeature != null) {
+ return locationFeature;
+ }
+ locationFeature = Feature.fromGeometry(Point.fromLngLat(0.0, 0.0));
+ locationFeature.addNumberProperty(PROPERTY_GPS_BEARING, 0f);
+ locationFeature.addNumberProperty(PROPERTY_COMPASS_BEARING, 0f);
+ locationFeature.addBooleanProperty(PROPERTY_LOCATION_STALE, options.enableStaleState());
+ return locationFeature;
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerGpsBearingAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerGpsBearingAnimator.java
new file mode 100644
index 0000000000..75cea13750
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerGpsBearingAnimator.java
@@ -0,0 +1,23 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.ValueAnimator;
+
+import java.util.List;
+
+class LayerGpsBearingAnimator extends MapboxFloatAnimator<MapboxAnimator.OnLayerAnimationsValuesChangeListener> {
+ LayerGpsBearingAnimator(Float previous, Float target, List<OnLayerAnimationsValuesChangeListener> updateListeners) {
+ super(previous, target, updateListeners);
+ }
+
+ @Override
+ int provideAnimatorType() {
+ return ANIMATOR_LAYER_GPS_BEARING;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (OnLayerAnimationsValuesChangeListener listener : updateListeners) {
+ listener.onNewGpsBearingValue((Float) animation.getAnimatedValue());
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerLatLngAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerLatLngAnimator.java
new file mode 100644
index 0000000000..f4dc2861cf
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerLatLngAnimator.java
@@ -0,0 +1,25 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.ValueAnimator;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import java.util.List;
+
+class LayerLatLngAnimator extends MapboxLatLngAnimator<MapboxAnimator.OnLayerAnimationsValuesChangeListener> {
+ LayerLatLngAnimator(LatLng previous, LatLng target, List<OnLayerAnimationsValuesChangeListener> updateListeners) {
+ super(previous, target, updateListeners);
+ }
+
+ @Override
+ int provideAnimatorType() {
+ return ANIMATOR_LAYER_LATLNG;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (OnLayerAnimationsValuesChangeListener listener : updateListeners) {
+ listener.onNewLatLngValue((LatLng) animation.getAnimatedValue());
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java
new file mode 100644
index 0000000000..d6198a7da8
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java
@@ -0,0 +1,107 @@
+package com.mapbox.mapboxsdk.location;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.mapboxsdk.style.layers.CircleLayer;
+import com.mapbox.mapboxsdk.style.layers.Layer;
+import com.mapbox.mapboxsdk.style.layers.Property;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.ACCURACY_LAYER;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_LAYER;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_LAYER;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_LAYER;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.LOCATION_SOURCE;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_ALPHA;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_COLOR;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_RADIUS;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_STALE_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BEARING_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_COMPASS_BEARING;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON_OFFSET;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_STALE_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_LOCATION_STALE;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.match;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.switchCase;
+import static com.mapbox.mapboxsdk.style.layers.Property.ICON_ROTATION_ALIGNMENT_MAP;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleOpacity;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circlePitchAlignment;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleRadius;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleStrokeColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconOffset;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconRotate;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconRotationAlignment;
+
+class LayerSourceProvider {
+
+ private static final String EMPTY_STRING = "";
+
+ GeoJsonSource generateSource(Feature locationFeature) {
+ return new GeoJsonSource(
+ LOCATION_SOURCE,
+ locationFeature,
+ new GeoJsonOptions().withMaxZoom(16)
+ );
+ }
+
+ Layer generateLayer(String layerId) {
+ SymbolLayer layer = new SymbolLayer(layerId, LOCATION_SOURCE);
+ layer.setProperties(
+ iconAllowOverlap(true),
+ iconIgnorePlacement(true),
+ iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP),
+ iconRotate(
+ match(literal(layerId), literal(0f),
+ stop(FOREGROUND_LAYER, get(PROPERTY_GPS_BEARING)),
+ stop(BACKGROUND_LAYER, get(PROPERTY_GPS_BEARING)),
+ stop(SHADOW_LAYER, get(PROPERTY_GPS_BEARING)),
+ stop(BEARING_LAYER, get(PROPERTY_COMPASS_BEARING))
+ )
+ ),
+ iconImage(
+ match(literal(layerId), literal(EMPTY_STRING),
+ stop(FOREGROUND_LAYER, switchCase(
+ get(PROPERTY_LOCATION_STALE), get(PROPERTY_FOREGROUND_STALE_ICON),
+ get(PROPERTY_FOREGROUND_ICON))),
+ stop(BACKGROUND_LAYER, switchCase(
+ get(PROPERTY_LOCATION_STALE), get(PROPERTY_BACKGROUND_STALE_ICON),
+ get(PROPERTY_BACKGROUND_ICON))),
+ stop(SHADOW_LAYER, literal(SHADOW_ICON)),
+ stop(BEARING_LAYER, get(PROPERTY_BEARING_ICON))
+ )
+ ),
+ iconOffset(
+ match(literal(layerId), literal(new Float[] {0f, 0f}),
+ stop(literal(FOREGROUND_LAYER), get(PROPERTY_FOREGROUND_ICON_OFFSET)),
+ stop(literal(SHADOW_LAYER), get(PROPERTY_SHADOW_ICON_OFFSET))
+ )
+ )
+ );
+ return layer;
+ }
+
+ Layer generateAccuracyLayer() {
+ return new CircleLayer(ACCURACY_LAYER, LOCATION_SOURCE)
+ .withProperties(
+ circleRadius(get(PROPERTY_ACCURACY_RADIUS)),
+ circleColor(get(PROPERTY_ACCURACY_COLOR)),
+ circleOpacity(get(PROPERTY_ACCURACY_ALPHA)),
+ circleStrokeColor(get(PROPERTY_ACCURACY_COLOR)),
+ circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP)
+ );
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java
new file mode 100644
index 0000000000..67a9727f5c
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java
@@ -0,0 +1,375 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.annotation.SuppressLint;
+import android.location.Location;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.animation.LinearInterpolator;
+
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.ACCURACY_RADIUS_ANIMATION_DURATION;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.COMPASS_UPDATE_RATE_MS;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.MAX_ANIMATION_DURATION_MS;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_CAMERA_COMPASS_BEARING;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_CAMERA_GPS_BEARING;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_CAMERA_LATLNG;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_ACCURACY;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_COMPASS_BEARING;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_GPS_BEARING;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_LATLNG;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_TILT;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_ZOOM;
+
+final class LocationAnimatorCoordinator {
+
+ @SuppressLint("UseSparseArrays")
+ final Map<Integer, MapboxAnimator> animatorMap = new HashMap<>();
+
+ final List<MapboxAnimator.OnLayerAnimationsValuesChangeListener> layerListeners = new ArrayList<>();
+ final List<MapboxAnimator.OnCameraAnimationsValuesChangeListener> cameraListeners = new ArrayList<>();
+
+ private Location previousLocation;
+ private float previousAccuracyRadius = -1;
+ private float previousCompassBearing = -1;
+ private long locationUpdateTimestamp = -1;
+
+ void addLayerListener(MapboxAnimator.OnLayerAnimationsValuesChangeListener listener) {
+ layerListeners.add(listener);
+ }
+
+ void removeLayerListener(MapboxAnimator.OnLayerAnimationsValuesChangeListener listener) {
+ layerListeners.remove(listener);
+ }
+
+ void addCameraListener(MapboxAnimator.OnCameraAnimationsValuesChangeListener listener) {
+ cameraListeners.add(listener);
+ }
+
+ void removeCameraListener(MapboxAnimator.OnCameraAnimationsValuesChangeListener listener) {
+ cameraListeners.remove(listener);
+ }
+
+ void feedNewLocation(@NonNull Location newLocation, @NonNull CameraPosition currentCameraPosition,
+ boolean isGpsNorth) {
+ if (previousLocation == null) {
+ previousLocation = newLocation;
+ locationUpdateTimestamp = SystemClock.elapsedRealtime() - TRANSITION_ANIMATION_DURATION_MS;
+ }
+
+ LatLng previousLayerLatLng = getPreviousLayerLatLng();
+ float previousLayerBearing = getPreviousLayerGpsBearing();
+ LatLng previousCameraLatLng = currentCameraPosition.target;
+ float previousCameraBearing = (float) currentCameraPosition.bearing;
+
+ LatLng targetLatLng = new LatLng(newLocation);
+ float targetLayerBearing = newLocation.getBearing();
+ float targetCameraBearing = newLocation.getBearing();
+ targetCameraBearing = checkGpsNorth(isGpsNorth, targetCameraBearing);
+
+ updateLayerAnimators(previousLayerLatLng, targetLatLng, previousLayerBearing, targetLayerBearing);
+ updateCameraAnimators(previousCameraLatLng, previousCameraBearing, targetLatLng, targetCameraBearing);
+
+ playLocationAnimators(getAnimationDuration());
+
+ previousLocation = newLocation;
+ }
+
+ void feedNewCompassBearing(float targetCompassBearing, @NonNull CameraPosition currentCameraPosition) {
+ if (previousCompassBearing < 0) {
+ previousCompassBearing = targetCompassBearing;
+ }
+
+ float previousLayerBearing = getPreviousLayerCompassBearing();
+ float previousCameraBearing = (float) currentCameraPosition.bearing;
+
+ updateCompassAnimators(targetCompassBearing, previousLayerBearing, previousCameraBearing);
+ playCompassAnimators(COMPASS_UPDATE_RATE_MS);
+
+ previousCompassBearing = targetCompassBearing;
+ }
+
+ void feedNewAccuracyRadius(float targetAccuracyRadius, boolean noAnimation) {
+ if (previousAccuracyRadius < 0) {
+ previousAccuracyRadius = targetAccuracyRadius;
+ }
+
+ float previousAccuracyRadius = getPreviousAccuracyRadius();
+ updateAccuracyAnimators(targetAccuracyRadius, previousAccuracyRadius);
+ playAccuracyAnimator(noAnimation ? 0 : ACCURACY_RADIUS_ANIMATION_DURATION);
+
+ this.previousAccuracyRadius = targetAccuracyRadius;
+ }
+
+ void feedNewZoomLevel(double targetZoomLevel, @NonNull CameraPosition currentCameraPosition, long animationDuration,
+ @Nullable MapboxMap.CancelableCallback callback) {
+ updateZoomAnimator((float) targetZoomLevel, (float) currentCameraPosition.zoom, callback);
+ playZoomAnimator(animationDuration);
+ }
+
+ void feedNewTilt(double targetTilt, @NonNull CameraPosition currentCameraPosition, long animationDuration,
+ @Nullable MapboxMap.CancelableCallback callback) {
+ updateTiltAnimator((float) targetTilt, (float) currentCameraPosition.tilt, callback);
+ playTiltAnimator(animationDuration);
+ }
+
+ private LatLng getPreviousLayerLatLng() {
+ LatLng previousLatLng;
+ MapboxAnimator latLngAnimator = animatorMap.get(ANIMATOR_LAYER_LATLNG);
+ if (latLngAnimator != null) {
+ previousLatLng = (LatLng) latLngAnimator.getAnimatedValue();
+ } else {
+ previousLatLng = new LatLng(previousLocation);
+ }
+ return previousLatLng;
+ }
+
+ private float getPreviousLayerGpsBearing() {
+ LayerGpsBearingAnimator animator = (LayerGpsBearingAnimator) animatorMap.get(ANIMATOR_LAYER_GPS_BEARING);
+ float previousBearing;
+ if (animator != null) {
+ previousBearing = (float) animator.getAnimatedValue();
+ } else {
+ previousBearing = previousLocation.getBearing();
+ }
+ return previousBearing;
+ }
+
+ private float getPreviousLayerCompassBearing() {
+ LayerCompassBearingAnimator animator =
+ (LayerCompassBearingAnimator) animatorMap.get(ANIMATOR_LAYER_COMPASS_BEARING);
+
+ float previousBearing;
+ if (animator != null) {
+ previousBearing = (float) animator.getAnimatedValue();
+ } else {
+ previousBearing = previousCompassBearing;
+ }
+ return previousBearing;
+ }
+
+ private float getPreviousAccuracyRadius() {
+ LayerAccuracyAnimator animator = (LayerAccuracyAnimator) animatorMap.get(ANIMATOR_LAYER_ACCURACY);
+ float previousRadius;
+ if (animator != null) {
+ previousRadius = (float) animator.getAnimatedValue();
+ } else {
+ previousRadius = previousAccuracyRadius;
+ }
+ return previousRadius;
+ }
+
+ private void updateLayerAnimators(LatLng previousLatLng, LatLng targetLatLng,
+ float previousBearing, float targetBearing) {
+ createNewAnimator(ANIMATOR_LAYER_LATLNG, new LayerLatLngAnimator(previousLatLng, targetLatLng, layerListeners));
+
+ float normalizedLayerBearing = Utils.shortestRotation(targetBearing, previousBearing);
+ createNewAnimator(ANIMATOR_LAYER_GPS_BEARING,
+ new LayerGpsBearingAnimator(previousBearing, normalizedLayerBearing, layerListeners));
+ }
+
+ private void updateCameraAnimators(LatLng previousCameraLatLng, float previousCameraBearing,
+ LatLng targetLatLng, float targetBearing) {
+ createNewAnimator(ANIMATOR_CAMERA_LATLNG,
+ new CameraLatLngAnimator(previousCameraLatLng, targetLatLng, cameraListeners));
+
+ float normalizedCameraBearing = Utils.shortestRotation(targetBearing, previousCameraBearing);
+ createNewAnimator(ANIMATOR_CAMERA_GPS_BEARING,
+ new CameraGpsBearingAnimator(previousCameraBearing, normalizedCameraBearing, cameraListeners));
+ }
+
+ private void updateCompassAnimators(float targetCompassBearing, float previousLayerBearing,
+ float previousCameraBearing) {
+ float normalizedLayerBearing = Utils.shortestRotation(targetCompassBearing, previousLayerBearing);
+ createNewAnimator(ANIMATOR_LAYER_COMPASS_BEARING,
+ new LayerCompassBearingAnimator(previousLayerBearing, normalizedLayerBearing, layerListeners));
+
+ float normalizedCameraBearing = Utils.shortestRotation(targetCompassBearing, previousCameraBearing);
+ createNewAnimator(ANIMATOR_CAMERA_COMPASS_BEARING,
+ new CameraCompassBearingAnimator(previousCameraBearing, normalizedCameraBearing, cameraListeners));
+ }
+
+ private void updateAccuracyAnimators(float targetAccuracyRadius, float previousAccuracyRadius) {
+ createNewAnimator(ANIMATOR_LAYER_ACCURACY,
+ new LayerAccuracyAnimator(previousAccuracyRadius, targetAccuracyRadius, layerListeners));
+ }
+
+ private void updateZoomAnimator(float targetZoomLevel, float previousZoomLevel,
+ @Nullable MapboxMap.CancelableCallback cancelableCallback) {
+ createNewAnimator(ANIMATOR_ZOOM,
+ new ZoomAnimator(previousZoomLevel, targetZoomLevel, cameraListeners, cancelableCallback));
+ }
+
+ private void updateTiltAnimator(float targetTilt, float previousTiltLevel,
+ @Nullable MapboxMap.CancelableCallback cancelableCallback) {
+ createNewAnimator(ANIMATOR_TILT,
+ new TiltAnimator(previousTiltLevel, targetTilt, cameraListeners, cancelableCallback));
+ }
+
+ private long getAnimationDuration() {
+ long previousUpdateTimeStamp = locationUpdateTimestamp;
+ locationUpdateTimestamp = SystemClock.elapsedRealtime();
+
+ long animationDuration;
+ if (previousUpdateTimeStamp == 0) {
+ animationDuration = 0;
+ } else {
+ animationDuration = (long) ((locationUpdateTimestamp - previousUpdateTimeStamp) * 1.1f)
+ /*make animation slightly longer*/;
+ }
+
+ animationDuration = Math.min(animationDuration, MAX_ANIMATION_DURATION_MS);
+
+ return animationDuration;
+ }
+
+ private float checkGpsNorth(boolean isGpsNorth, float targetCameraBearing) {
+ if (isGpsNorth) {
+ targetCameraBearing = 0;
+ }
+ return targetCameraBearing;
+ }
+
+ private void playLocationAnimators(long duration) {
+ List<Animator> locationAnimators = new ArrayList<>();
+ locationAnimators.add(animatorMap.get(ANIMATOR_LAYER_LATLNG));
+ locationAnimators.add(animatorMap.get(ANIMATOR_LAYER_GPS_BEARING));
+ locationAnimators.add(animatorMap.get(ANIMATOR_CAMERA_LATLNG));
+ locationAnimators.add(animatorMap.get(ANIMATOR_CAMERA_GPS_BEARING));
+ AnimatorSet locationAnimatorSet = new AnimatorSet();
+ locationAnimatorSet.playTogether(locationAnimators);
+ locationAnimatorSet.setInterpolator(new LinearInterpolator());
+ locationAnimatorSet.setDuration(duration);
+ locationAnimatorSet.start();
+ }
+
+ private void playCompassAnimators(long duration) {
+ List<Animator> compassAnimators = new ArrayList<>();
+ compassAnimators.add(animatorMap.get(ANIMATOR_LAYER_COMPASS_BEARING));
+ compassAnimators.add(animatorMap.get(ANIMATOR_CAMERA_COMPASS_BEARING));
+ AnimatorSet compassAnimatorSet = new AnimatorSet();
+ compassAnimatorSet.playTogether(compassAnimators);
+ compassAnimatorSet.setDuration(duration);
+ compassAnimatorSet.start();
+ }
+
+ private void playAccuracyAnimator(long duration) {
+ MapboxAnimator animator = animatorMap.get(ANIMATOR_LAYER_ACCURACY);
+ animator.setDuration(duration);
+ animator.start();
+ }
+
+ private void playZoomAnimator(long duration) {
+ MapboxAnimator animator = animatorMap.get(ANIMATOR_ZOOM);
+ animator.setDuration(duration);
+ animator.start();
+ }
+
+ private void playTiltAnimator(long duration) {
+ MapboxAnimator animator = animatorMap.get(ANIMATOR_TILT);
+ animator.setDuration(duration);
+ animator.start();
+ }
+
+ private void playCameraLocationAnimators(long duration) {
+ List<Animator> locationAnimators = new ArrayList<>();
+ locationAnimators.add(animatorMap.get(ANIMATOR_CAMERA_LATLNG));
+ locationAnimators.add(animatorMap.get(ANIMATOR_CAMERA_GPS_BEARING));
+ AnimatorSet locationAnimatorSet = new AnimatorSet();
+ locationAnimatorSet.playTogether(locationAnimators);
+ locationAnimatorSet.setInterpolator(new LinearInterpolator());
+ locationAnimatorSet.setDuration(duration);
+ locationAnimatorSet.start();
+ }
+
+ void resetAllCameraAnimations(CameraPosition currentCameraPosition, boolean isGpsNorth) {
+ resetCameraCompassAnimation(currentCameraPosition);
+ resetCameraLocationAnimations(currentCameraPosition, isGpsNorth);
+ playCameraLocationAnimators(TRANSITION_ANIMATION_DURATION_MS);
+ }
+
+ private void resetCameraLocationAnimations(CameraPosition currentCameraPosition, boolean isGpsNorth) {
+ resetCameraLatLngAnimation(currentCameraPosition);
+ resetCameraGpsBearingAnimation(currentCameraPosition, isGpsNorth);
+ }
+
+ private void resetCameraLatLngAnimation(CameraPosition currentCameraPosition) {
+ CameraLatLngAnimator animator = (CameraLatLngAnimator) animatorMap.get(ANIMATOR_CAMERA_LATLNG);
+ if (animator == null) {
+ return;
+ }
+
+ LatLng currentTarget = animator.getTarget();
+ LatLng previousCameraTarget = currentCameraPosition.target;
+ createNewAnimator(ANIMATOR_CAMERA_LATLNG,
+ new CameraLatLngAnimator(previousCameraTarget, currentTarget, cameraListeners));
+ }
+
+ private void resetCameraGpsBearingAnimation(CameraPosition currentCameraPosition, boolean isGpsNorth) {
+ CameraGpsBearingAnimator animator = (CameraGpsBearingAnimator) animatorMap.get(ANIMATOR_CAMERA_GPS_BEARING);
+ if (animator == null) {
+ return;
+ }
+
+ float currentTargetBearing = animator.getTarget();
+ currentTargetBearing = checkGpsNorth(isGpsNorth, currentTargetBearing);
+ float previousCameraBearing = (float) currentCameraPosition.bearing;
+ float normalizedCameraBearing = Utils.shortestRotation(currentTargetBearing, previousCameraBearing);
+ createNewAnimator(ANIMATOR_CAMERA_GPS_BEARING,
+ new CameraGpsBearingAnimator(previousCameraBearing, normalizedCameraBearing, cameraListeners));
+ }
+
+ private void resetCameraCompassAnimation(CameraPosition currentCameraPosition) {
+ CameraCompassBearingAnimator animator =
+ (CameraCompassBearingAnimator) animatorMap.get(ANIMATOR_CAMERA_COMPASS_BEARING);
+ if (animator == null) {
+ return;
+ }
+
+ float currentTargetBearing = animator.getTarget();
+ float previousCameraBearing = (float) currentCameraPosition.bearing;
+ float normalizedCameraBearing = Utils.shortestRotation(currentTargetBearing, previousCameraBearing);
+ createNewAnimator(ANIMATOR_CAMERA_COMPASS_BEARING,
+ new CameraCompassBearingAnimator(previousCameraBearing, normalizedCameraBearing, cameraListeners));
+ }
+
+ private void createNewAnimator(@MapboxAnimator.Type int animatorType, MapboxAnimator animator) {
+ cancelAnimator(animatorType);
+ animatorMap.put(animatorType, animator);
+ }
+
+ void cancelZoomAnimation() {
+ cancelAnimator(ANIMATOR_ZOOM);
+ }
+
+ void cancelTiltAnimation() {
+ cancelAnimator(ANIMATOR_TILT);
+ }
+
+ void cancelAllAnimations() {
+ for (@MapboxAnimator.Type int animatorType : animatorMap.keySet()) {
+ cancelAnimator(animatorType);
+ }
+ }
+
+ private void cancelAnimator(@MapboxAnimator.Type int animatorType) {
+ MapboxAnimator animator = animatorMap.get(animatorType);
+ if (animator != null) {
+ animator.cancel();
+ animator.removeAllUpdateListeners();
+ animator.removeAllListeners();
+ animatorMap.put(animatorType, null);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java
new file mode 100644
index 0000000000..5b2209179e
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java
@@ -0,0 +1,263 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import com.mapbox.android.gestures.AndroidGesturesManager;
+import com.mapbox.android.gestures.MoveGestureDetector;
+import com.mapbox.android.gestures.RotateGestureDetector;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.location.modes.CameraMode;
+
+import java.util.List;
+import java.util.Set;
+
+final class LocationCameraController implements MapboxAnimator.OnCameraAnimationsValuesChangeListener {
+
+ @CameraMode.Mode
+ private int cameraMode;
+
+ private final MapboxMap mapboxMap;
+ private final OnCameraTrackingChangedListener internalCameraTrackingChangedListener;
+ private LocationComponentOptions options;
+ private boolean adjustFocalPoint;
+
+ private final MoveGestureDetector moveGestureDetector;
+ private final OnCameraMoveInvalidateListener onCameraMoveInvalidateListener;
+
+ LocationCameraController(
+ Context context,
+ MapboxMap mapboxMap,
+ OnCameraTrackingChangedListener internalCameraTrackingChangedListener,
+ LocationComponentOptions options,
+ OnCameraMoveInvalidateListener onCameraMoveInvalidateListener) {
+ this.mapboxMap = mapboxMap;
+ mapboxMap.setGesturesManager(
+ new LocationGesturesManager(context), true, true);
+ moveGestureDetector = mapboxMap.getGesturesManager().getMoveGestureDetector();
+ mapboxMap.addOnMoveListener(onMoveListener);
+ mapboxMap.addOnRotateListener(onRotateListener);
+ mapboxMap.addOnFlingListener(onFlingListener);
+
+ this.internalCameraTrackingChangedListener = internalCameraTrackingChangedListener;
+ this.onCameraMoveInvalidateListener = onCameraMoveInvalidateListener;
+ initializeOptions(options);
+ }
+
+ // Package private for testing purposes
+ LocationCameraController(MapboxMap mapboxMap,
+ MoveGestureDetector moveGestureDetector,
+ OnCameraTrackingChangedListener internalCameraTrackingChangedListener,
+ OnCameraMoveInvalidateListener onCameraMoveInvalidateListener) {
+ this.mapboxMap = mapboxMap;
+ this.moveGestureDetector = moveGestureDetector;
+ this.internalCameraTrackingChangedListener = internalCameraTrackingChangedListener;
+ this.onCameraMoveInvalidateListener = onCameraMoveInvalidateListener;
+ }
+
+ void initializeOptions(LocationComponentOptions options) {
+ this.options = options;
+ }
+
+ void setCameraMode(@CameraMode.Mode int cameraMode) {
+ final boolean wasTracking = isLocationTracking();
+ this.cameraMode = cameraMode;
+ mapboxMap.cancelTransitions();
+ adjustGesturesThresholds();
+ notifyCameraTrackingChangeListener(wasTracking);
+ }
+
+ int getCameraMode() {
+ return cameraMode;
+ }
+
+ private void setBearing(float bearing) {
+ mapboxMap.moveCamera(CameraUpdateFactory.bearingTo(bearing));
+ onCameraMoveInvalidateListener.onInvalidateCameraMove();
+ }
+
+ private void setLatLng(LatLng latLng) {
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLng(latLng));
+ onCameraMoveInvalidateListener.onInvalidateCameraMove();
+ }
+
+ private void setZoom(float zoom) {
+ mapboxMap.moveCamera(CameraUpdateFactory.zoomTo(zoom));
+ onCameraMoveInvalidateListener.onInvalidateCameraMove();
+ }
+
+ private void setTilt(float tilt) {
+ mapboxMap.moveCamera(CameraUpdateFactory.tiltTo(tilt));
+ onCameraMoveInvalidateListener.onInvalidateCameraMove();
+ }
+
+ @Override
+ public void onNewLatLngValue(LatLng latLng) {
+ if (cameraMode == CameraMode.TRACKING
+ || cameraMode == CameraMode.TRACKING_COMPASS
+ || cameraMode == CameraMode.TRACKING_GPS
+ || cameraMode == CameraMode.TRACKING_GPS_NORTH) {
+ setLatLng(latLng);
+
+ if (adjustFocalPoint) {
+ PointF focalPoint = mapboxMap.getProjection().toScreenLocation(latLng);
+ mapboxMap.getUiSettings().setFocalPoint(focalPoint);
+ adjustFocalPoint = false;
+ }
+ }
+ }
+
+ @Override
+ public void onNewGpsBearingValue(float gpsBearing) {
+ boolean trackingNorth = cameraMode == CameraMode.TRACKING_GPS_NORTH
+ && mapboxMap.getCameraPosition().bearing != 0;
+
+ if (cameraMode == CameraMode.TRACKING_GPS
+ || cameraMode == CameraMode.NONE_GPS
+ || trackingNorth) {
+ setBearing(gpsBearing);
+ }
+ }
+
+ @Override
+ public void onNewCompassBearingValue(float compassBearing) {
+ if (cameraMode == CameraMode.TRACKING_COMPASS
+ || cameraMode == CameraMode.NONE_COMPASS) {
+ setBearing(compassBearing);
+ }
+ }
+
+ @Override
+ public void onNewZoomValue(float zoom) {
+ setZoom(zoom);
+ }
+
+ @Override
+ public void onNewTiltValue(float tilt) {
+ setTilt(tilt);
+ }
+
+ private void adjustGesturesThresholds() {
+ if (isLocationTracking()) {
+ adjustFocalPoint = true;
+ moveGestureDetector.setMoveThreshold(options.trackingInitialMoveThreshold());
+ } else {
+ moveGestureDetector.setMoveThreshold(0f);
+ }
+ }
+
+ private boolean isLocationTracking() {
+ return cameraMode == CameraMode.TRACKING
+ || cameraMode == CameraMode.TRACKING_COMPASS
+ || cameraMode == CameraMode.TRACKING_GPS
+ || cameraMode == CameraMode.TRACKING_GPS_NORTH;
+ }
+
+ private boolean isBearingTracking() {
+ return cameraMode == CameraMode.NONE_COMPASS
+ || cameraMode == CameraMode.TRACKING_COMPASS
+ || cameraMode == CameraMode.NONE_GPS
+ || cameraMode == CameraMode.TRACKING_GPS
+ || cameraMode == CameraMode.TRACKING_GPS_NORTH;
+ }
+
+ private void notifyCameraTrackingChangeListener(boolean wasTracking) {
+ internalCameraTrackingChangedListener.onCameraTrackingChanged(cameraMode);
+ if (wasTracking && !isLocationTracking()) {
+ mapboxMap.getUiSettings().setFocalPoint(null);
+ internalCameraTrackingChangedListener.onCameraTrackingDismissed();
+ }
+ }
+
+ private MapboxMap.OnMoveListener onMoveListener = new MapboxMap.OnMoveListener() {
+ private boolean interrupt;
+
+ @Override
+ public void onMoveBegin(MoveGestureDetector detector) {
+ if (detector.getPointersCount() > 1
+ && detector.getMoveThreshold() != options.trackingMultiFingerMoveThreshold()
+ && isLocationTracking()) {
+ detector.setMoveThreshold(options.trackingMultiFingerMoveThreshold());
+ interrupt = true;
+ }
+ }
+
+ @Override
+ public void onMove(MoveGestureDetector detector) {
+ if (interrupt) {
+ detector.interrupt();
+ return;
+ }
+
+ setCameraMode(CameraMode.NONE);
+ }
+
+ @Override
+ public void onMoveEnd(MoveGestureDetector detector) {
+ if (!interrupt && isLocationTracking()) {
+ moveGestureDetector.setMoveThreshold(options.trackingInitialMoveThreshold());
+ }
+ interrupt = false;
+ }
+ };
+
+ private MapboxMap.OnRotateListener onRotateListener = new MapboxMap.OnRotateListener() {
+ @Override
+ public void onRotateBegin(RotateGestureDetector detector) {
+ if (isBearingTracking()) {
+ setCameraMode(CameraMode.NONE);
+ }
+ }
+
+ @Override
+ public void onRotate(RotateGestureDetector detector) {
+ // no implementation
+ }
+
+ @Override
+ public void onRotateEnd(RotateGestureDetector detector) {
+ // no implementation
+ }
+ };
+
+ private MapboxMap.OnFlingListener onFlingListener = new MapboxMap.OnFlingListener() {
+ @Override
+ public void onFling() {
+ setCameraMode(CameraMode.NONE);
+ }
+ };
+
+ private class LocationGesturesManager extends AndroidGesturesManager {
+
+ public LocationGesturesManager(Context context) {
+ super(context);
+ }
+
+ public LocationGesturesManager(Context context, boolean applyDefaultThresholds) {
+ super(context, applyDefaultThresholds);
+ }
+
+ public LocationGesturesManager(Context context, Set<Integer>[] exclusiveGestures) {
+ super(context, exclusiveGestures);
+ }
+
+ public LocationGesturesManager(Context context, List<Set<Integer>> exclusiveGestures,
+ boolean applyDefaultThresholds) {
+ super(context, exclusiveGestures, applyDefaultThresholds);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent motionEvent) {
+ if (motionEvent != null) {
+ int action = motionEvent.getActionMasked();
+ if (action == MotionEvent.ACTION_UP) {
+ adjustGesturesThresholds();
+ }
+ }
+ return super.onTouchEvent(motionEvent);
+ }
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java
new file mode 100644
index 0000000000..4b0860998f
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java
@@ -0,0 +1,1019 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.location.Location;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresPermission;
+import android.support.annotation.StyleRes;
+import android.support.v7.app.AppCompatDelegate;
+import android.view.WindowManager;
+
+import com.mapbox.android.core.location.LocationEngine;
+import com.mapbox.android.core.location.LocationEngineListener;
+import com.mapbox.android.core.location.LocationEnginePriority;
+import com.mapbox.android.core.location.LocationEngineProvider;
+import com.mapbox.mapboxsdk.R;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdate;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.location.modes.CameraMode;
+import com.mapbox.mapboxsdk.location.modes.RenderMode;
+import com.mapbox.mapboxsdk.log.Logger;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener;
+import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveListener;
+import com.mapbox.mapboxsdk.maps.MapboxMap.OnMapClickListener;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_TILT_ANIM_DURATION;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_ZOOM_ANIM_DURATION;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.STATE_LOCATION_CAMERA_MODE;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.STATE_LOCATION_ENABLED;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.STATE_LOCATION_LAST_LOCATION;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.STATE_LOCATION_OPTIONS;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.STATE_LOCATION_RENDER_MODE;
+
+/**
+ * The Location layer plugin provides location awareness to your mobile application. Enabling this
+ * plugin provides a contextual experience to your users by showing an icon representing the users
+ * current location. A few different modes are offered to provide the right context to your users at
+ * the correct time. {@link RenderMode#NORMAL} simply shows the users location on the map
+ * represented as a dot. {@link RenderMode#COMPASS} mode allows you to display an arrow icon
+ * (by default) that points in the direction the device is pointing in.
+ * {@link RenderMode#GPS} can be used in conjunction with our Navigation SDK to
+ * display a larger icon (customized with {@link LocationComponentOptions#gpsDrawable()}) we call the user puck.
+ * <p>
+ * This plugin also offers the ability to set a map camera behavior for tracking the user
+ * location. These different {@link CameraMode}s will track, stop tracking the location based on the
+ * mode set with {@link LocationComponent#setCameraMode(int)}.
+ * <p>
+ * Lastly, {@link LocationComponent#setLocationComponentEnabled(boolean)} can be used
+ * to disable the Location Layer but keep the instance around till the activity is destroyed.
+ * <p>
+ * Using this plugin requires you to request permission beforehand manually or using
+ * {@link com.mapbox.android.core.permissions.PermissionsManager}. Either
+ * {@code ACCESS_COARSE_LOCATION} or {@code ACCESS_FINE_LOCATION} permissions can be requested and
+ * this plugin work as expected.
+ * <p>
+ * When instantiating the plugin for the first time, the map's max/min zoom levels will be set to
+ * {@link LocationComponentOptions#MAX_ZOOM_DEFAULT} and {@link LocationComponentOptions#MIN_ZOOM_DEFAULT} respectively.
+ * You can adjust the zoom range with {@link LocationComponentOptions#maxZoom()} and
+ * {@link LocationComponentOptions#minZoom()}.
+ * <p>
+ * When an activity, or a fragment, that contains the plugin is destroyed and recreated,
+ * the plugin will restore its state, which is:
+ * <br/>
+ * - If the plugin was enabled, last location will be displayed.
+ * You still need to activate the plugin, or just provide the {@link LocationEngine}.
+ * <br/>
+ * - {@link CameraMode} and {@link RenderMode} will be restored.
+ * <br/>
+ * - {@link LocationComponentOptions} will be restored.
+ */
+public final class LocationComponent {
+ private static final String TAG = "Mbgl-LocationComponent";
+
+ private final MapboxMap mapboxMap;
+ private LocationComponentOptions options;
+ private LocationEngine locationEngine;
+ private CompassEngine compassEngine;
+ private boolean usingInternalLocationEngine;
+
+ private LocationLayerController locationLayerController;
+ private LocationCameraController locationCameraController;
+
+ private LocationAnimatorCoordinator locationAnimatorCoordinator;
+
+ /**
+ * Holds last location which is being returned in the {@link #getLastKnownLocation()}
+ * when there is no {@link #locationEngine} set or when the last location returned by the engine is null.
+ */
+ private Location lastLocation;
+ private CameraPosition lastCameraPosition;
+
+ /**
+ * Indicates that the plugin is enabled and should be displaying location if Mapbox components are available and
+ * the lifecycle is in a resumed state.
+ */
+ private boolean isEnabled;
+
+ /**
+ * Indicated that plugin's lifecycle {@link #onStart()} method has been called or the plugin is initialized..
+ * This allows Mapbox components enter started state and display data, and adds state safety for methods like
+ * {@link #setLocationComponentEnabled(boolean)}
+ * <p>
+ * Initialized in a started state because the plugin can be instantiated after lifecycle's onStart() and
+ * the developer might not register the lifecycle observer but call lifecycle methods manually instead.
+ */
+ private boolean isComponentStarted;
+
+ /**
+ * Indicates if Mapbox components are ready to be interacted with. This can differ from {@link #isComponentStarted}
+ * if the Mapbox style is being reloaded.
+ */
+ private boolean isLayerReady;
+
+ private StaleStateManager staleStateManager;
+ private final CopyOnWriteArrayList<OnLocationStaleListener> onLocationStaleListeners
+ = new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<OnLocationComponentClickListener> onLocationComponentClickListeners
+ = new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<OnLocationComponentLongClickListener> onLocationComponentLongClickListeners
+ = new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<OnCameraTrackingChangedListener> onCameraTrackingChangedListeners
+ = new CopyOnWriteArrayList<>();
+
+ /**
+ * Construct a LocationComponent. In order to display location,
+ * the location layer has to be activated with {@link LocationComponent#activateLocationComponent(Context)},
+ * or one of the overloads.
+ *
+ * @param mapboxMap the MapboxMap to apply the LocationComponent with
+ */
+ public LocationComponent(@NonNull Context context, @NonNull MapboxMap mapboxMap) {
+ this.mapboxMap = mapboxMap;
+ options = LocationComponentOptions.createFromAttributes(context, R.style.mapbox_LocationComponent);
+ initialize(context);
+ }
+
+ /**
+ * This method will show or hide the location icon and enable or disable the camera
+ * tracking the location.
+ *
+ * @param isEnabled true to show layers and enable camera, false otherwise
+ */
+ private void setLocationComponentEnabled(boolean isEnabled) {
+ if (isEnabled) {
+ enableLocationComponent();
+ } else {
+ disableLocationComponent();
+ }
+ }
+
+ /**
+ * This method will show the location icon and enable the camera tracking the location.
+ * <p>
+ * <strong>Note</strong>: This method will initialize and use an internal {@link LocationEngine}.
+ *
+ * @param context the context
+ */
+ @RequiresPermission(anyOf = {ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION})
+ public void activateLocationComponent(@NonNull Context context) {
+ activateLocationComponent(context, LocationComponentOptions.createFromAttributes(context, R.style
+ .mapbox_LocationComponent));
+ }
+
+ /**
+ * This method will show the location icon and enable the camera tracking the location.
+ *
+ * @param context the context
+ * @param useDefaultLocationEngine true if you want to initialize and use the built-in location engine or false if
+ * there should be no location engine initialized
+ */
+ @RequiresPermission(anyOf = {ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION})
+ public void activateLocationComponent(@NonNull Context context, boolean useDefaultLocationEngine) {
+ if (useDefaultLocationEngine) {
+ activateLocationComponent(context, R.style.mapbox_LocationComponent);
+ } else {
+ activateLocationComponent(context, null, R.style.mapbox_LocationComponent);
+ }
+ }
+
+ /**
+ * This method will show the location icon and enable the camera tracking the location.
+ * <p>
+ * <strong>Note</strong>: This method will initialize and use an internal {@link LocationEngine}.
+ *
+ * @param context the context
+ * @param styleRes the LocationComponent style res
+ */
+ @RequiresPermission(anyOf = {ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION})
+ public void activateLocationComponent(@NonNull Context context, @StyleRes int styleRes) {
+ activateLocationComponent(context, LocationComponentOptions.createFromAttributes(context, styleRes));
+ }
+
+ /**
+ * This method will show the location icon and enable the camera tracking the location.
+ * <p>
+ * <strong>Note</strong>: This method will initialize and use an internal {@link LocationEngine}.
+ * </p>
+ *
+ * @param context the context
+ * @param options the options
+ */
+ @RequiresPermission(anyOf = {ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION})
+ public void activateLocationComponent(@NonNull Context context, @NonNull LocationComponentOptions options) {
+ applyStyle(options);
+ initializeLocationEngine(context);
+ setLocationComponentEnabled(true);
+ }
+
+ /**
+ * This method will show the location icon and enable the camera tracking the location.
+ *
+ * @param context the context
+ * @param locationEngine the engine, or null if you'd like to only force location updates
+ * @param styleRes the LocationComponent style res
+ */
+ public void activateLocationComponent(@NonNull Context context, @Nullable LocationEngine locationEngine,
+ @StyleRes int styleRes) {
+ activateLocationComponent(locationEngine, LocationComponentOptions.createFromAttributes(context, styleRes));
+ }
+
+ /**
+ * This method will show the location icon and enable the camera tracking the location.
+ *
+ * @param context the context
+ * @param locationEngine the engine
+ */
+ public void activateLocationComponent(@NonNull Context context, @NonNull LocationEngine locationEngine) {
+ activateLocationComponent(context, locationEngine, R.style.mapbox_LocationComponent);
+ }
+
+ /**
+ * This method will show the location icon and enable the camera tracking the location.
+ *
+ * @param locationEngine the engine, or null if you'd like to only force location updates
+ * @param options the options
+ */
+ public void activateLocationComponent(@Nullable LocationEngine locationEngine,
+ @NonNull LocationComponentOptions options) {
+ setLocationEngine(locationEngine);
+ applyStyle(options);
+ setLocationComponentEnabled(true);
+ }
+
+ /**
+ * This method will hide the location icon and disable the camera tracking the location.
+ */
+ public void deactivateLocationComponent() {
+ setLocationComponentEnabled(false);
+ }
+
+ /**
+ * Returns whether the plugin is enabled, meaning that location can be displayed and camera modes can be used.
+ *
+ * @return true if the plugin is enabled, false otherwise
+ */
+ public boolean isLocationLayerEnabled() {
+ return isEnabled;
+ }
+
+ /**
+ * Sets the camera mode, which determines how the map camera will track the rendered location.
+ * <p>
+ * <ul>
+ * <li>{@link CameraMode#NONE}: No camera tracking</li>
+ * <li>{@link CameraMode#NONE_COMPASS}: Camera does not track location, but does track compass bearing</li>
+ * <li>{@link CameraMode#NONE_GPS}: Camera does not track location, but does track GPS bearing</li>
+ * <li>{@link CameraMode#TRACKING}: Camera tracks the user location</li>
+ * <li>{@link CameraMode#TRACKING_COMPASS}: Camera tracks the user location, with bearing provided by a compass</li>
+ * <li>{@link CameraMode#TRACKING_GPS}: Camera tracks the user location, with normalized bearing</li>
+ * <li>{@link CameraMode#TRACKING_GPS_NORTH}: Camera tracks the user location, with bearing always set to north</li>
+ * </ul>
+ *
+ * @param cameraMode one of the modes found in {@link CameraMode}
+ */
+ public void setCameraMode(@CameraMode.Mode int cameraMode) {
+ locationCameraController.setCameraMode(cameraMode);
+ boolean isGpsNorth = cameraMode == CameraMode.TRACKING_GPS_NORTH;
+ locationAnimatorCoordinator.resetAllCameraAnimations(mapboxMap.getCameraPosition(), isGpsNorth);
+ }
+
+ /**
+ * Provides the current camera mode being used to track
+ * the location or compass updates.
+ *
+ * @return the current camera mode
+ */
+ @CameraMode.Mode
+ public int getCameraMode() {
+ return locationCameraController.getCameraMode();
+ }
+
+ /**
+ * Sets the render mode, which determines how the location updates will be rendered on the map.
+ * <p>
+ * <ul>
+ * <li>{@link RenderMode#NORMAL}: Shows user location, bearing ignored</li>
+ * <li>{@link RenderMode#COMPASS}: Shows user location with bearing considered from compass</li>
+ * <li>{@link RenderMode#GPS}: Shows user location with bearing considered from location</li>
+ * </ul>
+ *
+ * @param renderMode one of the modes found in {@link RenderMode}
+ */
+ public void setRenderMode(@RenderMode.Mode int renderMode) {
+ locationLayerController.setRenderMode(renderMode);
+ updateLayerOffsets(true);
+ }
+
+ /**
+ * Provides the current render mode being used to show
+ * the location and/or compass updates on the map.
+ *
+ * @return the current render mode
+ */
+ @RenderMode.Mode
+ public int getRenderMode() {
+ return locationLayerController.getRenderMode();
+ }
+
+ /**
+ * Returns the current location options being used.
+ *
+ * @return the current {@link LocationComponentOptions}
+ */
+ public LocationComponentOptions getLocationComponentOptions() {
+ return options;
+ }
+
+ /**
+ * Apply a new LocationLayerController style with a style resource.
+ *
+ * @param styleRes a XML style overriding some or all the options
+ */
+ public void applyStyle(@NonNull Context context, @StyleRes int styleRes) {
+ applyStyle(LocationComponentOptions.createFromAttributes(context, styleRes));
+ }
+
+ /**
+ * Apply a new LocationLayerController style with location layer options.
+ *
+ * @param options to update the current style
+ */
+ public void applyStyle(LocationComponentOptions options) {
+ this.options = options;
+ locationLayerController.applyStyle(options);
+ staleStateManager.setEnabled(options.enableStaleState());
+ staleStateManager.setDelayTime(options.staleStateTimeout());
+ updateMapWithOptions(options);
+ }
+
+ /**
+ * Zooms to the desired zoom level.
+ * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}.
+ * If you are not using any of {@link CameraMode} modes,
+ * use one of {@link MapboxMap#moveCamera(CameraUpdate)},
+ * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead.
+ *
+ * @param zoomLevel The desired zoom level.
+ * @param animationDuration The zoom animation duration.
+ * @param callback The callback with finish/cancel information
+ */
+ public void zoomWhileTracking(double zoomLevel, long animationDuration,
+ @Nullable MapboxMap.CancelableCallback callback) {
+ if (!isLayerReady) {
+ return;
+ } else if (getCameraMode() == CameraMode.NONE) {
+ Logger.e(TAG, String.format("%s%s",
+ "LocationComponent#zoomWhileTracking method can only be used",
+ " when a camera mode other than CameraMode#NONE is engaged."));
+ return;
+ }
+ locationAnimatorCoordinator.feedNewZoomLevel(zoomLevel, mapboxMap.getCameraPosition(), animationDuration, callback);
+ }
+
+ /**
+ * Zooms to the desired zoom level.
+ * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}.
+ * If you are not using any of {@link CameraMode} modes,
+ * use one of {@link MapboxMap#moveCamera(CameraUpdate)},
+ * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead.
+ *
+ * @param zoomLevel The desired zoom level.
+ * @param animationDuration The zoom animation duration.
+ */
+ public void zoomWhileTracking(double zoomLevel, long animationDuration) {
+ zoomWhileTracking(zoomLevel, animationDuration, null);
+ }
+
+ /**
+ * Zooms to the desired zoom level.
+ * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}.
+ * If you are not using any of {@link CameraMode} modes,
+ * use one of {@link MapboxMap#moveCamera(CameraUpdate)},
+ * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead.
+ *
+ * @param zoomLevel The desired zoom level.
+ */
+ public void zoomWhileTracking(double zoomLevel) {
+ zoomWhileTracking(zoomLevel, DEFAULT_TRACKING_ZOOM_ANIM_DURATION, null);
+ }
+
+ /**
+ * Cancels animation started by {@link #zoomWhileTracking(double, long, MapboxMap.CancelableCallback)}.
+ */
+ public void cancelZoomWhileTrackingAnimation() {
+ locationAnimatorCoordinator.cancelZoomAnimation();
+ }
+
+ /**
+ * Tilts the camera.
+ * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}.
+ * If you are not using any of {@link CameraMode} modes,
+ * use one of {@link MapboxMap#moveCamera(CameraUpdate)},
+ * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead.
+ *
+ * @param tilt The desired camera tilt.
+ * @param animationDuration The tilt animation duration.
+ * @param callback The callback with finish/cancel information
+ */
+ public void tiltWhileTracking(double tilt, long animationDuration,
+ @Nullable MapboxMap.CancelableCallback callback) {
+ if (!isLayerReady) {
+ return;
+ } else if (getCameraMode() == CameraMode.NONE) {
+ Logger.e(TAG, String.format("%s%s",
+ "LocationComponent#tiltWhileTracking method can only be used",
+ " when a camera mode other than CameraMode#NONE is engaged."));
+ return;
+ }
+ locationAnimatorCoordinator.feedNewTilt(tilt, mapboxMap.getCameraPosition(), animationDuration, callback);
+ }
+
+ /**
+ * Tilts the camera.
+ * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}.
+ * If you are not using any of {@link CameraMode} modes,
+ * use one of {@link MapboxMap#moveCamera(CameraUpdate)},
+ * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead.
+ *
+ * @param tilt The desired camera tilt.
+ * @param animationDuration The tilt animation duration.
+ */
+ public void tiltWhileTracking(double tilt, long animationDuration) {
+ tiltWhileTracking(tilt, animationDuration, null);
+ }
+
+ /**
+ * Tilts the camera.
+ * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}.
+ * If you are not using any of {@link CameraMode} modes,
+ * use one of {@link MapboxMap#moveCamera(CameraUpdate)},
+ * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead.
+ *
+ * @param tilt The desired camera tilt.
+ */
+ public void tiltWhileTracking(double tilt) {
+ tiltWhileTracking(tilt, DEFAULT_TRACKING_TILT_ANIM_DURATION, null);
+ }
+
+ /**
+ * Cancels animation started by {@link #tiltWhileTracking(double, long, MapboxMap.CancelableCallback)}.
+ */
+ public void cancelTiltWhileTrackingAnimation() {
+ locationAnimatorCoordinator.cancelTiltAnimation();
+ }
+
+ /**
+ * Use to either force a location update or to manually control when the user location gets
+ * updated.
+ *
+ * @param location where the location icon is placed on the map
+ */
+ public void forceLocationUpdate(@Nullable Location location) {
+ updateLocation(location, false);
+ }
+
+ /**
+ * Set the location engine to update the current user location.
+ * <p>
+ * If {@code null} is passed in, all updates will occur through the
+ * {@link LocationComponent#forceLocationUpdate(Location)} method.
+ *
+ * @param locationEngine a {@link LocationEngine} this plugin should use to handle updates
+ */
+ public void setLocationEngine(@Nullable LocationEngine locationEngine) {
+ if (this.locationEngine != null) {
+ // If internal location engines being used, extra steps need to be taken to deconstruct the
+ // instance.
+ if (usingInternalLocationEngine) {
+ this.locationEngine.removeLocationUpdates();
+ this.locationEngine.deactivate();
+ usingInternalLocationEngine = false;
+ }
+ this.locationEngine.removeLocationEngineListener(locationEngineListener);
+ this.locationEngine = null;
+ }
+
+ if (locationEngine != null) {
+ this.locationEngine = locationEngine;
+ if (isEnabled) {
+ this.locationEngine.addLocationEngineListener(locationEngineListener);
+ }
+ }
+ }
+
+ /**
+ * Returns the current {@link LocationEngine} being used for updating the user location layer.
+ *
+ * @return the {@link LocationEngine} being used to update the user location layer
+ */
+ @Nullable
+ public LocationEngine getLocationEngine() {
+ return locationEngine;
+ }
+
+ /**
+ * Sets the compass engine used to provide compass heading values.
+ *
+ * @param compassEngine to be used
+ */
+ public void setCompassEngine(@NonNull CompassEngine compassEngine) {
+ this.compassEngine.removeCompassListener(compassListener);
+ this.compassEngine = compassEngine;
+ compassEngine.addCompassListener(compassListener);
+ }
+
+ /**
+ * Returns the compass engine used to provide compass heading values.
+ *
+ * @return compass engine currently being used
+ */
+ @NonNull
+ public CompassEngine getCompassEngine() {
+ return compassEngine;
+ }
+
+ /**
+ * Get the last know location of the location layer plugin.
+ *
+ * @return the last known location
+ */
+ @Nullable
+ @RequiresPermission(anyOf = {ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION})
+ public Location getLastKnownLocation() {
+ Location location = locationEngine != null ? locationEngine.getLastLocation() : null;
+ if (location == null) {
+ location = lastLocation;
+ }
+ return location;
+ }
+
+ /**
+ * Return the last known {@link CompassEngine} accuracy status of the location layer plugin.
+ * <p>
+ * The last known accuracy of the compass sensor, one of SensorManager.SENSOR_STATUS_*
+ *
+ * @return the last know compass accuracy bearing
+ */
+ public float getLastKnownCompassAccuracyStatus() {
+ return compassEngine.getLastAccuracySensorStatus();
+ }
+
+ /**
+ * Add a compass listener to get heading updates every second. Once the first listener gets added,
+ * the sensor gets initiated and starts returning values.
+ *
+ * @param compassListener a {@link CompassListener} for listening into compass heading and
+ * accuracy changes
+ */
+ public void addCompassListener(@NonNull CompassListener compassListener) {
+ compassEngine.addCompassListener(compassListener);
+ }
+
+ /**
+ * Remove a compass listener.
+ *
+ * @param compassListener the {@link CompassListener} which you'd like to remove from the listener
+ * list.
+ */
+ public void removeCompassListener(@NonNull CompassListener compassListener) {
+ compassEngine.removeCompassListener(compassListener);
+ }
+
+ /**
+ * Adds a listener that gets invoked when the user clicks the location layer.
+ *
+ * @param listener The location layer click listener that is invoked when the
+ * location layer is clicked
+ */
+ public void addOnLocationClickListener(@NonNull OnLocationComponentClickListener listener) {
+ onLocationComponentClickListeners.add(listener);
+ }
+
+ /**
+ * Removes the passed listener from the current list of location click listeners.
+ *
+ * @param listener to be removed
+ */
+ public void removeOnLocationClickListener(@NonNull OnLocationComponentClickListener listener) {
+ onLocationComponentClickListeners.remove(listener);
+ }
+
+ /**
+ * Adds a listener that gets invoked when the user long clicks the location layer.
+ *
+ * @param listener The location layer click listener that is invoked when the
+ * location layer is clicked
+ */
+ public void addOnLocationLongClickListener(@NonNull OnLocationComponentLongClickListener listener) {
+ onLocationComponentLongClickListeners.add(listener);
+ }
+
+ /**
+ * Removes the passed listener from the current list of location long click listeners.
+ *
+ * @param listener to be removed
+ */
+ public void removeOnLocationLongClickListener(@NonNull OnLocationComponentLongClickListener listener) {
+ onLocationComponentLongClickListeners.remove(listener);
+ }
+
+ /**
+ * Adds a listener that gets invoked when camera tracking state changes.
+ *
+ * @param listener Listener that gets invoked when camera tracking state changes.
+ */
+ public void addOnCameraTrackingChangedListener(@NonNull OnCameraTrackingChangedListener listener) {
+ onCameraTrackingChangedListeners.add(listener);
+ }
+
+ /**
+ * Removes a listener that gets invoked when camera tracking state changes.
+ *
+ * @param listener Listener that gets invoked when camera tracking state changes.
+ */
+ public void removeOnCameraTrackingChangedListener(@NonNull OnCameraTrackingChangedListener listener) {
+ onCameraTrackingChangedListeners.remove(listener);
+ }
+
+ /**
+ * Adds the passed listener that gets invoked when user updates have stopped long enough for the last update
+ * to be considered stale.
+ * <p>
+ * This timeout is set by {@link LocationComponentOptions#staleStateTimeout()}.
+ *
+ * @param listener invoked when last update is considered stale
+ */
+ public void addOnLocationStaleListener(@NonNull OnLocationStaleListener listener) {
+ onLocationStaleListeners.add(listener);
+ }
+
+ /**
+ * Removes the passed listener from the current list of stale listeners.
+ *
+ * @param listener to be removed from the list
+ */
+ public void removeOnLocationStaleListener(@NonNull OnLocationStaleListener listener) {
+ onLocationStaleListeners.remove(listener);
+ }
+
+ /**
+ * Internal use.
+ */
+ public void onStart() {
+ isComponentStarted = true;
+ onLocationLayerStart();
+ }
+
+ /**
+ * Internal use.
+ */
+ public void onStop() {
+ onLocationLayerStop();
+ isComponentStarted = false;
+ }
+
+ /**
+ * Internal use.
+ */
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ outState.putBoolean(STATE_LOCATION_ENABLED, isEnabled);
+ outState.putParcelable(STATE_LOCATION_OPTIONS, options);
+ outState.putInt(STATE_LOCATION_RENDER_MODE, locationLayerController.getRenderMode());
+ outState.putInt(STATE_LOCATION_CAMERA_MODE, locationCameraController.getCameraMode());
+ outState.putParcelable(STATE_LOCATION_LAST_LOCATION, lastLocation);
+ }
+
+ /**
+ * Internal use.
+ */
+ public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ updateLocation(savedInstanceState.getParcelable(STATE_LOCATION_LAST_LOCATION), true);
+ setCameraMode(savedInstanceState.getInt(STATE_LOCATION_CAMERA_MODE));
+ setRenderMode(savedInstanceState.getInt(STATE_LOCATION_RENDER_MODE));
+ applyStyle(savedInstanceState.getParcelable(STATE_LOCATION_OPTIONS));
+ setLocationComponentEnabled(savedInstanceState.getBoolean(STATE_LOCATION_ENABLED));
+ }
+
+ /**
+ * Internal use.
+ */
+ public void onDestroy() {
+ if (locationEngine != null && usingInternalLocationEngine) {
+ locationEngine.deactivate();
+ }
+ }
+
+ /**
+ * Internal use.
+ */
+ public void onStartLoadingMap() {
+ onLocationLayerStop();
+ }
+
+ /**
+ * Internal use.
+ */
+ public void onFinishLoadingStyle() {
+ locationLayerController.initializeComponents(options);
+ locationCameraController.initializeOptions(options);
+ onLocationLayerStart();
+ }
+
+ @SuppressLint("MissingPermission")
+ private void onLocationLayerStart() {
+ if (!isComponentStarted) {
+ return;
+ }
+
+ if (!isLayerReady) {
+ isLayerReady = true;
+ if (mapboxMap != null) {
+ mapboxMap.addOnCameraMoveListener(onCameraMoveListener);
+ mapboxMap.addOnCameraIdleListener(onCameraIdleListener);
+ }
+ if (options.enableStaleState()) {
+ staleStateManager.onStart();
+ }
+ compassEngine.onStart();
+ }
+
+ if (isEnabled) {
+ if (locationEngine != null) {
+ locationEngine.addLocationEngineListener(locationEngineListener);
+ if (locationEngine.isConnected() && usingInternalLocationEngine) {
+ locationEngine.requestLocationUpdates();
+ }
+ }
+ setCameraMode(locationCameraController.getCameraMode());
+ setLastLocation();
+ setLastCompassHeading();
+ }
+ }
+
+ private void onLocationLayerStop() {
+ if (!isLayerReady || !isComponentStarted) {
+ return;
+ }
+
+ isLayerReady = false;
+ locationLayerController.hide();
+ staleStateManager.onStop();
+ compassEngine.onStop();
+ locationAnimatorCoordinator.cancelAllAnimations();
+ if (locationEngine != null) {
+ if (usingInternalLocationEngine) {
+ locationEngine.removeLocationUpdates();
+ }
+ locationEngine.removeLocationEngineListener(locationEngineListener);
+ }
+ if (mapboxMap != null) {
+ mapboxMap.removeOnCameraMoveListener(onCameraMoveListener);
+ mapboxMap.removeOnCameraIdleListener(onCameraIdleListener);
+ }
+ }
+
+ private void initialize(@NonNull Context context) {
+ AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
+
+ mapboxMap.addOnMapClickListener(onMapClickListener);
+ mapboxMap.addOnMapLongClickListener(onMapLongClickListener);
+
+ LayerSourceProvider sourceProvider = new LayerSourceProvider();
+ LayerFeatureProvider featureProvider = new LayerFeatureProvider();
+ LayerBitmapProvider bitmapProvider = new LayerBitmapProvider(context);
+ locationLayerController = new LocationLayerController(mapboxMap, sourceProvider, featureProvider, bitmapProvider,
+ options);
+ locationCameraController = new LocationCameraController(
+ context, mapboxMap, cameraTrackingChangedListener, options, onCameraMoveInvalidateListener);
+ locationAnimatorCoordinator = new LocationAnimatorCoordinator();
+ locationAnimatorCoordinator.addLayerListener(locationLayerController);
+ locationAnimatorCoordinator.addCameraListener(locationCameraController);
+
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ compassEngine = new LocationComponentCompassEngine(windowManager, sensorManager);
+ compassEngine.addCompassListener(compassListener);
+ staleStateManager = new StaleStateManager(onLocationStaleListener, options);
+
+ updateMapWithOptions(options);
+
+ setRenderMode(RenderMode.NORMAL);
+ setCameraMode(CameraMode.NONE);
+ }
+
+ private void initializeLocationEngine(@NonNull Context context) {
+ if (this.locationEngine != null) {
+ if (usingInternalLocationEngine) {
+ this.locationEngine.removeLocationUpdates();
+ this.locationEngine.deactivate();
+ }
+ this.locationEngine.removeLocationEngineListener(locationEngineListener);
+ }
+
+ usingInternalLocationEngine = true;
+ locationEngine = new LocationEngineProvider(context).obtainBestLocationEngineAvailable();
+ locationEngine.setPriority(LocationEnginePriority.HIGH_ACCURACY);
+ locationEngine.setFastestInterval(1000);
+ locationEngine.addLocationEngineListener(locationEngineListener);
+ locationEngine.activate();
+ }
+
+ private void enableLocationComponent() {
+ isEnabled = true;
+ onLocationLayerStart();
+ }
+
+ private void disableLocationComponent() {
+ isEnabled = false;
+ onLocationLayerStop();
+ }
+
+ private void updateMapWithOptions(final LocationComponentOptions options) {
+ mapboxMap.setPadding(
+ options.padding()[0], options.padding()[1], options.padding()[2], options.padding()[3]
+ );
+
+ mapboxMap.setMaxZoomPreference(options.maxZoom());
+ mapboxMap.setMinZoomPreference(options.minZoom());
+ }
+
+ /**
+ * Updates the user location icon.
+ *
+ * @param location the latest user location
+ */
+ private void updateLocation(final Location location, boolean fromLastLocation) {
+ if (location == null) {
+ return;
+ } else if (!isLayerReady) {
+ lastLocation = location;
+ return;
+ }
+
+ if (isEnabled && isComponentStarted) {
+ locationLayerController.show();
+ }
+
+ if (!fromLastLocation) {
+ staleStateManager.updateLatestLocationTime();
+ }
+ CameraPosition currentCameraPosition = mapboxMap.getCameraPosition();
+ boolean isGpsNorth = getCameraMode() == CameraMode.TRACKING_GPS_NORTH;
+ locationAnimatorCoordinator.feedNewLocation(location, currentCameraPosition, isGpsNorth);
+ updateAccuracyRadius(location, false);
+ lastLocation = location;
+ }
+
+ private void updateCompassHeading(float heading) {
+ locationAnimatorCoordinator.feedNewCompassBearing(heading, mapboxMap.getCameraPosition());
+ }
+
+ /**
+ * If the locationEngine contains a last location value, we use it for the initial location layer
+ * position.
+ */
+ @SuppressLint("MissingPermission")
+ private void setLastLocation() {
+ updateLocation(getLastKnownLocation(), true);
+ }
+
+ private void setLastCompassHeading() {
+ updateCompassHeading(compassEngine.getLastHeading());
+ }
+
+ @SuppressLint("MissingPermission")
+ private void updateLayerOffsets(boolean forceUpdate) {
+ CameraPosition position = mapboxMap.getCameraPosition();
+ if (lastCameraPosition == null || forceUpdate) {
+ lastCameraPosition = position;
+ locationLayerController.updateForegroundBearing((float) position.bearing);
+ locationLayerController.updateForegroundOffset(position.tilt);
+ updateAccuracyRadius(getLastKnownLocation(), true);
+ return;
+ }
+
+ if (position.bearing != lastCameraPosition.bearing) {
+ locationLayerController.updateForegroundBearing((float) position.bearing);
+ }
+ if (position.tilt != lastCameraPosition.tilt) {
+ locationLayerController.updateForegroundOffset(position.tilt);
+ }
+ if (position.zoom != lastCameraPosition.zoom) {
+ updateAccuracyRadius(getLastKnownLocation(), true);
+ }
+ lastCameraPosition = position;
+ }
+
+ private void updateAccuracyRadius(Location location, boolean noAnimation) {
+ locationAnimatorCoordinator.feedNewAccuracyRadius(Utils.calculateZoomLevelRadius(mapboxMap, location), noAnimation);
+ }
+
+ private OnCameraMoveListener onCameraMoveListener = new OnCameraMoveListener() {
+ @Override
+ public void onCameraMove() {
+ updateLayerOffsets(false);
+ }
+ };
+
+ private OnCameraIdleListener onCameraIdleListener = new OnCameraIdleListener() {
+ @Override
+ public void onCameraIdle() {
+ updateLayerOffsets(false);
+ }
+ };
+
+ private OnMapClickListener onMapClickListener = new OnMapClickListener() {
+ @Override
+ public void onMapClick(@NonNull LatLng point) {
+ if (!onLocationComponentClickListeners.isEmpty() && locationLayerController.onMapClick(point)) {
+ for (OnLocationComponentClickListener listener : onLocationComponentClickListeners) {
+ listener.onLocationComponentClick();
+ }
+ }
+ }
+ };
+
+ private MapboxMap.OnMapLongClickListener onMapLongClickListener = new MapboxMap.OnMapLongClickListener() {
+ @Override
+ public void onMapLongClick(@NonNull LatLng point) {
+ if (!onLocationComponentLongClickListeners.isEmpty() && locationLayerController.onMapClick(point)) {
+ for (OnLocationComponentLongClickListener listener : onLocationComponentLongClickListeners) {
+ listener.onLocationComponentLongClick();
+ }
+ }
+ }
+ };
+
+ private OnLocationStaleListener onLocationStaleListener = new OnLocationStaleListener() {
+ @Override
+ public void onStaleStateChange(boolean isStale) {
+ locationLayerController.setLocationsStale(isStale);
+
+ for (OnLocationStaleListener listener : onLocationStaleListeners) {
+ listener.onStaleStateChange(isStale);
+ }
+ }
+ };
+
+ private OnCameraMoveInvalidateListener onCameraMoveInvalidateListener = new OnCameraMoveInvalidateListener() {
+ @Override
+ public void onInvalidateCameraMove() {
+ onCameraMoveListener.onCameraMove();
+ }
+ };
+
+ private CompassListener compassListener = new CompassListener() {
+ @Override
+ public void onCompassChanged(float userHeading) {
+ updateCompassHeading(userHeading);
+ }
+
+ @Override
+ public void onCompassAccuracyChange(int compassStatus) {
+ // Currently don't handle this inside SDK
+ }
+ };
+
+ private LocationEngineListener locationEngineListener = new LocationEngineListener() {
+ @Override
+ @SuppressWarnings( {"MissingPermission"})
+ public void onConnected() {
+ if (usingInternalLocationEngine && isLayerReady && isEnabled) {
+ locationEngine.requestLocationUpdates();
+ }
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ updateLocation(location, false);
+ }
+ };
+
+ private OnCameraTrackingChangedListener cameraTrackingChangedListener = new OnCameraTrackingChangedListener() {
+ @Override
+ public void onCameraTrackingDismissed() {
+ for (OnCameraTrackingChangedListener listener : onCameraTrackingChangedListeners) {
+ listener.onCameraTrackingDismissed();
+ }
+ }
+
+ @Override
+ public void onCameraTrackingChanged(int currentMode) {
+ locationAnimatorCoordinator.cancelZoomAnimation();
+ locationAnimatorCoordinator.cancelTiltAnimation();
+ for (OnCameraTrackingChangedListener listener : onCameraTrackingChangedListeners) {
+ listener.onCameraTrackingChanged(currentMode);
+ }
+ }
+ };
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentCompassEngine.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentCompassEngine.java
new file mode 100644
index 0000000000..b53d909de3
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentCompassEngine.java
@@ -0,0 +1,267 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import timber.log.Timber;
+
+/**
+ * This manager class handles compass events such as starting the tracking of device bearing, or
+ * when a new compass update occurs.
+ */
+class LocationComponentCompassEngine implements CompassEngine, SensorEventListener {
+
+ // The rate sensor events will be delivered at. As the Android documentation states, this is only
+ // a hint to the system and the events might actually be received faster or slower then this
+ // specified rate. Since the minimum Android API levels about 9, we are able to set this value
+ // ourselves rather than using one of the provided constants which deliver updates too quickly for
+ // our use case. The default is set to 100ms
+ private static final int SENSOR_DELAY_MICROS = 100 * 1000;
+ // Filtering coefficient 0 < ALPHA < 1
+ private static final float ALPHA = 0.45f;
+
+ private final WindowManager windowManager;
+ private final SensorManager sensorManager;
+ private final List<CompassListener> compassListeners = new ArrayList<>();
+
+ // Not all devices have a compassSensor
+ @Nullable
+ private Sensor compassSensor;
+ @Nullable
+ private Sensor gravitySensor;
+ @Nullable
+ private Sensor magneticFieldSensor;
+
+ private float[] truncatedRotationVectorValue = new float[4];
+ private float[] rotationMatrix = new float[9];
+ private float[] rotationVectorValue;
+ private float lastHeading;
+ private int lastAccuracySensorStatus;
+
+ private long compassUpdateNextTimestamp;
+ private float[] gravityValues = new float[3];
+ private float[] magneticValues = new float[3];
+
+ /**
+ * Construct a new instance of the this class. A internal compass listeners needed to separate it
+ * from the cleared list of public listeners.
+ */
+ LocationComponentCompassEngine(WindowManager windowManager, SensorManager sensorManager) {
+ this.windowManager = windowManager;
+ this.sensorManager = sensorManager;
+ compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
+ if (compassSensor == null) {
+ if (isGyroscopeAvailable()) {
+ Timber.d("Rotation vector sensor not supported on device, falling back to orientation.");
+ compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
+ } else {
+ Timber.d("Rotation vector sensor not supported on device, falling back to accelerometer and magnetic field.");
+ gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ magneticFieldSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+ }
+ }
+ }
+
+ @Override
+ public void addCompassListener(@NonNull CompassListener compassListener) {
+ if (compassListeners.isEmpty()) {
+ onStart();
+ }
+ compassListeners.add(compassListener);
+ }
+
+ @Override
+ public void removeCompassListener(@NonNull CompassListener compassListener) {
+ compassListeners.remove(compassListener);
+ if (compassListeners.isEmpty()) {
+ onStop();
+ }
+ }
+
+ @Override
+ public int getLastAccuracySensorStatus() {
+ return lastAccuracySensorStatus;
+ }
+
+ @Override
+ public float getLastHeading() {
+ return lastHeading;
+ }
+
+ @Override
+ public void onStart() {
+ registerSensorListeners();
+ }
+
+ @Override
+ public void onStop() {
+ unregisterSensorListeners();
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ // check when the last time the compass was updated, return if too soon.
+ long currentTime = SystemClock.elapsedRealtime();
+ if (currentTime < compassUpdateNextTimestamp) {
+ return;
+ }
+ if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_UNRELIABLE) {
+ Timber.d("Compass sensor is unreliable, device calibration is needed.");
+ return;
+ }
+ if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
+ rotationVectorValue = getRotationVectorFromSensorEvent(event);
+ updateOrientation();
+
+ // Update the compassUpdateNextTimestamp
+ compassUpdateNextTimestamp = currentTime + LocationComponentConstants.COMPASS_UPDATE_RATE_MS;
+ } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
+ notifyCompassChangeListeners((event.values[0] + 360) % 360);
+ } else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
+ gravityValues = lowPassFilter(getRotationVectorFromSensorEvent(event), gravityValues);
+ updateOrientation();
+ } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
+ magneticValues = lowPassFilter(getRotationVectorFromSensorEvent(event), magneticValues);
+ updateOrientation();
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ if (lastAccuracySensorStatus != accuracy) {
+ for (CompassListener compassListener : compassListeners) {
+ compassListener.onCompassAccuracyChange(accuracy);
+ }
+ lastAccuracySensorStatus = accuracy;
+ }
+ }
+
+ private boolean isGyroscopeAvailable() {
+ return sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null;
+ }
+
+ @SuppressWarnings("SuspiciousNameCombination")
+ private void updateOrientation() {
+ if (rotationVectorValue != null) {
+ SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVectorValue);
+ } else {
+ // Get rotation matrix given the gravity and geomagnetic matrices
+ SensorManager.getRotationMatrix(rotationMatrix, null, gravityValues, magneticValues);
+ }
+
+ final int worldAxisForDeviceAxisX;
+ final int worldAxisForDeviceAxisY;
+
+ // Remap the axes as if the device screen was the instrument panel,
+ // and adjust the rotation matrix for the device orientation.
+ switch (windowManager.getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_90:
+ worldAxisForDeviceAxisX = SensorManager.AXIS_Z;
+ worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X;
+ break;
+ case Surface.ROTATION_180:
+ worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X;
+ worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Z;
+ break;
+ case Surface.ROTATION_270:
+ worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Z;
+ worldAxisForDeviceAxisY = SensorManager.AXIS_X;
+ break;
+ case Surface.ROTATION_0:
+ default:
+ worldAxisForDeviceAxisX = SensorManager.AXIS_X;
+ worldAxisForDeviceAxisY = SensorManager.AXIS_Z;
+ break;
+ }
+
+ float[] adjustedRotationMatrix = new float[9];
+ SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX,
+ worldAxisForDeviceAxisY, adjustedRotationMatrix);
+
+ // Transform rotation matrix into azimuth/pitch/roll
+ float[] orientation = new float[3];
+ SensorManager.getOrientation(adjustedRotationMatrix, orientation);
+
+ // The x-axis is all we care about here.
+ notifyCompassChangeListeners((float) Math.toDegrees(orientation[0]));
+ }
+
+ private void notifyCompassChangeListeners(float heading) {
+ for (CompassListener compassListener : compassListeners) {
+ compassListener.onCompassChanged(heading);
+ }
+ lastHeading = heading;
+ }
+
+ private void registerSensorListeners() {
+ if (isCompassSensorAvailable()) {
+ // Does nothing if the sensors already registered.
+ sensorManager.registerListener(this, compassSensor, SENSOR_DELAY_MICROS);
+ } else {
+ sensorManager.registerListener(this, gravitySensor, SENSOR_DELAY_MICROS);
+ sensorManager.registerListener(this, magneticFieldSensor, SENSOR_DELAY_MICROS);
+ }
+ }
+
+ private void unregisterSensorListeners() {
+ if (isCompassSensorAvailable()) {
+ sensorManager.unregisterListener(this, compassSensor);
+ } else {
+ sensorManager.unregisterListener(this, gravitySensor);
+ sensorManager.unregisterListener(this, magneticFieldSensor);
+ }
+ }
+
+ private boolean isCompassSensorAvailable() {
+ return compassSensor != null;
+ }
+
+ /**
+ * Helper function, that filters newValues, considering previous values
+ *
+ * @param newValues array of float, that contains new data
+ * @param smoothedValues array of float, that contains previous state
+ * @return float filtered array of float
+ */
+ private float[] lowPassFilter(float[] newValues, float[] smoothedValues) {
+ if (smoothedValues == null) {
+ return newValues;
+ }
+ for (int i = 0; i < newValues.length; i++) {
+ smoothedValues[i] = smoothedValues[i] + ALPHA * (newValues[i] - smoothedValues[i]);
+ }
+ return smoothedValues;
+ }
+
+ /**
+ * Pulls out the rotation vector from a SensorEvent, with a maximum length
+ * vector of four elements to avoid potential compatibility issues.
+ *
+ * @param event the sensor event
+ * @return the events rotation vector, potentially truncated
+ */
+ @NonNull
+ private float[] getRotationVectorFromSensorEvent(@NonNull SensorEvent event) {
+ if (event.values.length > 4) {
+ // On some Samsung devices SensorManager.getRotationMatrixFromVector
+ // appears to throw an exception if rotation vector has length > 4.
+ // For the purposes of this class the first 4 values of the
+ // rotation vector are sufficient (see crbug.com/335298 for details).
+ // Only affects Android 4.3
+ System.arraycopy(event.values, 0, truncatedRotationVectorValue, 0, 4);
+ return truncatedRotationVectorValue;
+ } else {
+ return event.values;
+ }
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java
new file mode 100644
index 0000000000..854170d617
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java
@@ -0,0 +1,66 @@
+package com.mapbox.mapboxsdk.location;
+
+/**
+ * Contains all the constants being used for the Location layer.
+ */
+final class LocationComponentConstants {
+
+ static final String STATE_LOCATION_ENABLED = "mapbox_location_locationEnabled";
+ static final String STATE_LOCATION_OPTIONS = "mapbox_location_options";
+ static final String STATE_LOCATION_LAST_LOCATION = "mapbox_location_lastLocation";
+ static final String STATE_LOCATION_RENDER_MODE = "mapbox_location_renderMode";
+ static final String STATE_LOCATION_CAMERA_MODE = "mapbox_location_cameraMode";
+
+ // Controls the compass update rate in milliseconds
+ static final int COMPASS_UPDATE_RATE_MS = 500;
+
+ // Sets the transition animation duration when switching camera modes.
+ static final long TRANSITION_ANIMATION_DURATION_MS = 750;
+
+ // Sets the max allowed time for the location icon animation from one LatLng to another.
+ static final long MAX_ANIMATION_DURATION_MS = 2000;
+
+ // Sets the duration of change of accuracy radius when a different value is provided.
+ static final long ACCURACY_RADIUS_ANIMATION_DURATION = 250;
+
+ // Default animation duration for zooming while tracking.
+ static final long DEFAULT_TRACKING_ZOOM_ANIM_DURATION = 750;
+
+ // Default animation duration for tilting while tracking.
+ static final long DEFAULT_TRACKING_TILT_ANIM_DURATION = 1250;
+
+ // Sources
+ static final String LOCATION_SOURCE = "mapbox-location-source";
+ static final String PROPERTY_GPS_BEARING = "mapbox-property-gps-bearing";
+ static final String PROPERTY_COMPASS_BEARING = "mapbox-property-compass-bearing";
+ static final String PROPERTY_LOCATION_STALE = "mapbox-property-location-stale";
+ static final String PROPERTY_ACCURACY_RADIUS = "mapbox-property-accuracy-radius";
+ static final String PROPERTY_ACCURACY_COLOR = "mapbox-property-accuracy-color";
+ static final String PROPERTY_ACCURACY_ALPHA = "mapbox-property-accuracy-alpha";
+ static final String PROPERTY_FOREGROUND_ICON_OFFSET = "mapbox-property-foreground-icon-offset";
+ static final String PROPERTY_SHADOW_ICON_OFFSET = "mapbox-property-shadow-icon-offset";
+ static final String PROPERTY_FOREGROUND_ICON = "mapbox-property-foreground-icon";
+ static final String PROPERTY_BACKGROUND_ICON = "mapbox-property-background-icon";
+ static final String PROPERTY_FOREGROUND_STALE_ICON = "mapbox-property-foreground-stale-icon";
+ static final String PROPERTY_BACKGROUND_STALE_ICON = "mapbox-property-background-stale-icon";
+ static final String PROPERTY_BEARING_ICON = "mapbox-property-shadow-icon";
+
+ // Layers
+ static final String SHADOW_LAYER = "mapbox-location-shadow";
+ static final String FOREGROUND_LAYER = "mapbox-location-layer";
+ static final String BACKGROUND_LAYER = "mapbox-location-stroke-layer";
+ static final String ACCURACY_LAYER = "mapbox-location-accuracy-layer";
+ static final String BEARING_LAYER = "mapbox-location-bearing-layer";
+
+ // Icons
+ static final String FOREGROUND_ICON = "mapbox-location-icon";
+ static final String BACKGROUND_ICON = "mapbox-location-stroke-icon";
+ static final String FOREGROUND_STALE_ICON = "mapbox-location-stale-icon";
+ static final String BACKGROUND_STALE_ICON = "mapbox-location-background-stale-icon";
+ static final String SHADOW_ICON = "mapbox-location-shadow-icon";
+ static final String BEARING_ICON = "mapbox-location-bearing-icon";
+
+ private LocationComponentConstants() {
+ // Class should not be initialized
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java
new file mode 100644
index 0000000000..9a50840926
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java
@@ -0,0 +1,1561 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.Dimension;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StyleRes;
+
+import com.mapbox.mapboxsdk.R;
+import com.mapbox.mapboxsdk.constants.MapboxConstants;
+
+import java.util.Arrays;
+
+/**
+ * This class exposes options for the Location Layer Plugin. The options can be set by defining a
+ * style in your apps style.xml file and passing in directly into the {@link LocationComponent}
+ * class. Alternatively, if properties need to be changed at runtime depending on a specific state,
+ * you can build an instance of this class, setting the values you desire, and then passing it into
+ * either the {@link LocationComponent} constructor (if it isn't initialized yet) or
+ * {@link LocationComponent#applyStyle(LocationComponentOptions)}.
+ * <p>
+ * When the {@link #createFromAttributes(Context, int)} methods called, any attributes not found
+ * inside the style will revert back to using their default set values. Likewise, when building a
+ * new {@link LocationComponentOptions} class using the builder, any options neglecting to be set will
+ * reset to their default values.
+ * <p>
+ * If you would like to keep your custom style changes while modifying a single attribute, you can
+ * get the currently used options object using {@link LocationComponent#getLocationComponentOptions()}
+ * and it's {@code toBuilder} method to modify a single entry while also maintaining the other
+ * settings. Once your modifications have been made, you'll need to pass it back into the location
+ * layer plugin using {@link LocationComponent#applyStyle(LocationComponentOptions)}.
+ */
+public class LocationComponentOptions implements Parcelable {
+
+ /**
+ * Default accuracy alpha
+ */
+ private static final float ACCURACY_ALPHA_DEFAULT = 0.15f;
+
+ /**
+ * Default max map zoom
+ */
+ private static final float MAX_ZOOM_DEFAULT = 18;
+
+ /**
+ * Default min map zoom
+ */
+ private static final float MIN_ZOOM_DEFAULT = 2;
+
+ /**
+ * Default icon scale factor when the map is zoomed out
+ */
+ private static final float MIN_ZOOM_ICON_SCALE_DEFAULT = 0.6f;
+
+ /**
+ * Default icon scale factor when the map is zoomed in
+ */
+ private static final float MAX_ZOOM_ICON_SCALE_DEFAULT = 1f;
+
+ /**
+ * Default map padding
+ */
+ private static final int[] PADDING_DEFAULT = {0, 0, 0, 0};
+
+ /**
+ * The default value which is used when the stale state is enabled
+ */
+ private static final long STALE_STATE_DELAY_MS = 30000;
+
+ private float accuracyAlpha;
+ private int accuracyColor;
+ private int backgroundDrawableStale;
+ private String backgroundStaleName;
+ private int foregroundDrawableStale;
+ private String foregroundStaleName;
+ private int gpsDrawable;
+ private String gpsName;
+ private int foregroundDrawable;
+ private String foregroundName;
+ private int backgroundDrawable;
+ private String backgroundName;
+ private int bearingDrawable;
+ private String bearingName;
+ private Integer bearingTintColor;
+ private Integer foregroundTintColor;
+ private Integer backgroundTintColor;
+ private Integer foregroundStaleTintColor;
+ private Integer backgroundStaleTintColor;
+ private float elevation;
+ private boolean enableStaleState;
+ private long staleStateTimeout;
+ private int[] padding;
+ private double maxZoom;
+ private double minZoom;
+ private float maxZoomIconScale;
+ private float minZoomIconScale;
+ private float trackingInitialMoveThreshold;
+ private float trackingMultiFingerMoveThreshold;
+ private String layerBelow;
+
+ public LocationComponentOptions(
+ float accuracyAlpha,
+ int accuracyColor,
+ int backgroundDrawableStale,
+ @Nullable String backgroundStaleName,
+ int foregroundDrawableStale,
+ @Nullable String foregroundStaleName,
+ int gpsDrawable,
+ @Nullable String gpsName,
+ int foregroundDrawable,
+ @Nullable String foregroundName,
+ int backgroundDrawable,
+ @Nullable String backgroundName,
+ int bearingDrawable,
+ @Nullable String bearingName,
+ @Nullable Integer bearingTintColor,
+ @Nullable Integer foregroundTintColor,
+ @Nullable Integer backgroundTintColor,
+ @Nullable Integer foregroundStaleTintColor,
+ @Nullable Integer backgroundStaleTintColor,
+ float elevation,
+ boolean enableStaleState,
+ long staleStateTimeout,
+ int[] padding,
+ double maxZoom,
+ double minZoom,
+ float maxZoomIconScale,
+ float minZoomIconScale,
+ float trackingInitialMoveThreshold,
+ float trackingMultiFingerMoveThreshold,
+ String layerBelow) {
+ this.accuracyAlpha = accuracyAlpha;
+ this.accuracyColor = accuracyColor;
+ this.backgroundDrawableStale = backgroundDrawableStale;
+ this.backgroundStaleName = backgroundStaleName;
+ this.foregroundDrawableStale = foregroundDrawableStale;
+ this.foregroundStaleName = foregroundStaleName;
+ this.gpsDrawable = gpsDrawable;
+ this.gpsName = gpsName;
+ this.foregroundDrawable = foregroundDrawable;
+ this.foregroundName = foregroundName;
+ this.backgroundDrawable = backgroundDrawable;
+ this.backgroundName = backgroundName;
+ this.bearingDrawable = bearingDrawable;
+ this.bearingName = bearingName;
+ this.bearingTintColor = bearingTintColor;
+ this.foregroundTintColor = foregroundTintColor;
+ this.backgroundTintColor = backgroundTintColor;
+ this.foregroundStaleTintColor = foregroundStaleTintColor;
+ this.backgroundStaleTintColor = backgroundStaleTintColor;
+ this.elevation = elevation;
+ this.enableStaleState = enableStaleState;
+ this.staleStateTimeout = staleStateTimeout;
+ if (padding == null) {
+ throw new NullPointerException("Null padding");
+ }
+ this.padding = padding;
+ this.maxZoom = maxZoom;
+ this.minZoom = minZoom;
+ this.maxZoomIconScale = maxZoomIconScale;
+ this.minZoomIconScale = minZoomIconScale;
+ this.trackingInitialMoveThreshold = trackingInitialMoveThreshold;
+ this.trackingMultiFingerMoveThreshold = trackingMultiFingerMoveThreshold;
+ this.layerBelow = layerBelow;
+ }
+
+ /**
+ * Construct a new Location Layer Options class using the attributes found within a style
+ * resource. It's important to note that you only need to define the attributes you plan to
+ * change and can safely ignore the other attributes which will be set to their default value.
+ *
+ * @param context your activity's context used for acquiring resources
+ * @param styleRes the style id where your custom attributes are defined
+ * @return a new {@link LocationComponentOptions} object with the settings you defined in your style
+ * resource
+ */
+ public static LocationComponentOptions createFromAttributes(@NonNull Context context,
+ @StyleRes int styleRes) {
+
+ TypedArray typedArray = context.obtainStyledAttributes(
+ styleRes, R.styleable.mapbox_LocationComponent);
+
+ LocationComponentOptions.Builder builder = new LocationComponentOptions.Builder()
+ .enableStaleState(true)
+ .staleStateTimeout(STALE_STATE_DELAY_MS)
+ .maxZoom(MAX_ZOOM_DEFAULT)
+ .minZoom(MIN_ZOOM_DEFAULT)
+ .maxZoomIconScale(MAX_ZOOM_ICON_SCALE_DEFAULT)
+ .minZoomIconScale(MIN_ZOOM_ICON_SCALE_DEFAULT)
+ .padding(PADDING_DEFAULT);
+
+ builder.foregroundDrawable(typedArray.getResourceId(
+ R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawable, -1));
+ if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_foregroundTintColor)) {
+ builder.foregroundTintColor(typedArray.getColor(
+ R.styleable.mapbox_LocationComponent_mapbox_foregroundTintColor, -1));
+ }
+ builder.backgroundDrawable(typedArray.getResourceId(
+ R.styleable.mapbox_LocationComponent_mapbox_backgroundDrawable, -1));
+ if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_backgroundTintColor)) {
+ builder.backgroundTintColor(typedArray.getColor(
+ R.styleable.mapbox_LocationComponent_mapbox_backgroundTintColor, -1));
+ }
+ builder.foregroundDrawableStale(typedArray.getResourceId(
+ R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawableStale, -1));
+ if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_foregroundStaleTintColor)) {
+ builder.foregroundStaleTintColor(typedArray.getColor(
+ R.styleable.mapbox_LocationComponent_mapbox_foregroundStaleTintColor, -1));
+ }
+ builder.backgroundDrawableStale(typedArray.getResourceId(
+ R.styleable.mapbox_LocationComponent_mapbox_backgroundDrawableStale, -1));
+ if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_backgroundStaleTintColor)) {
+ builder.backgroundStaleTintColor(typedArray.getColor(
+ R.styleable.mapbox_LocationComponent_mapbox_backgroundStaleTintColor, -1));
+ }
+ builder.bearingDrawable(typedArray.getResourceId(
+ R.styleable.mapbox_LocationComponent_mapbox_bearingDrawable, -1));
+ if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_bearingTintColor)) {
+ builder.bearingTintColor(typedArray.getColor(
+ R.styleable.mapbox_LocationComponent_mapbox_bearingTintColor, -1));
+ }
+ if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_enableStaleState)) {
+ builder.enableStaleState(typedArray.getBoolean(
+ R.styleable.mapbox_LocationComponent_mapbox_enableStaleState, true));
+ }
+ if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_staleStateTimeout)) {
+ builder.staleStateTimeout(typedArray.getInteger(
+ R.styleable.mapbox_LocationComponent_mapbox_staleStateTimeout, (int) STALE_STATE_DELAY_MS));
+ }
+ builder.gpsDrawable(typedArray.getResourceId(
+ R.styleable.mapbox_LocationComponent_mapbox_gpsDrawable, -1));
+ float elevation = typedArray.getDimension(
+ R.styleable.mapbox_LocationComponent_mapbox_elevation, 0);
+ builder.accuracyColor(typedArray.getColor(
+ R.styleable.mapbox_LocationComponent_mapbox_accuracyColor, -1));
+ builder.accuracyAlpha(typedArray.getFloat(
+ R.styleable.mapbox_LocationComponent_mapbox_accuracyAlpha, ACCURACY_ALPHA_DEFAULT));
+ builder.elevation(elevation);
+
+ builder.trackingInitialMoveThreshold(typedArray.getDimension(
+ R.styleable.mapbox_LocationComponent_mapbox_trackingInitialMoveThreshold,
+ context.getResources().getDimension(R.dimen.mapbox_locationComponentTrackingInitialMoveThreshold)));
+
+ builder.trackingMultiFingerMoveThreshold(typedArray.getDimension(
+ R.styleable.mapbox_LocationComponent_mapbox_trackingMultiFingerMoveThreshold,
+ context.getResources().getDimension(R.dimen.mapbox_locationComponentTrackingMultiFingerMoveThreshold)));
+
+ builder.padding(new int[] {
+ typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingLeft, 0),
+ typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingTop, 0),
+ typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingRight, 0),
+ typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingBottom, 0),
+ });
+
+ float maxZoom
+ = typedArray.getFloat(R.styleable.mapbox_LocationComponent_mapbox_maxZoom, MAX_ZOOM_DEFAULT);
+ if (maxZoom < MapboxConstants.MINIMUM_ZOOM || maxZoom > MapboxConstants.MAXIMUM_ZOOM) {
+ throw new IllegalArgumentException("Max zoom value must be within "
+ + MapboxConstants.MINIMUM_ZOOM + " and " + MapboxConstants.MAXIMUM_ZOOM);
+ }
+
+ float minZoom
+ = typedArray.getFloat(R.styleable.mapbox_LocationComponent_mapbox_minZoom, MIN_ZOOM_DEFAULT);
+ if (minZoom < MapboxConstants.MINIMUM_ZOOM || minZoom > MapboxConstants.MAXIMUM_ZOOM) {
+ throw new IllegalArgumentException("Min zoom value must be within "
+ + MapboxConstants.MINIMUM_ZOOM + " and " + MapboxConstants.MAXIMUM_ZOOM);
+ }
+
+ builder.maxZoom(maxZoom);
+ builder.minZoom(minZoom);
+
+ builder.layerBelow(
+ typedArray.getString(R.styleable.mapbox_LocationComponent_mapbox_layer_below));
+
+ float minScale = typedArray.getFloat(
+ R.styleable.mapbox_LocationComponent_mapbox_minZoomIconScale, MIN_ZOOM_ICON_SCALE_DEFAULT);
+ float maxScale = typedArray.getFloat(
+ R.styleable.mapbox_LocationComponent_mapbox_maxZoomIconScale, MAX_ZOOM_ICON_SCALE_DEFAULT);
+ builder.minZoomIconScale(minScale);
+ builder.maxZoomIconScale(maxScale);
+
+ typedArray.recycle();
+
+ return builder.build();
+ }
+
+ /**
+ * Takes the currently constructed {@link LocationComponentOptions} object and provides it's builder
+ * with all the values set matching the values in this instance. This allows you to modify a
+ * single attribute and then rebuild the object.
+ *
+ * @return the Location Layer builder which contains the values defined in this current instance
+ * as defaults.
+ */
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Build a new instance of the {@link LocationComponentOptions} class with all the attributes set
+ * automatically to their defined defaults in this library. This allows you to adjust a few
+ * attributes while leaving the rest alone and maintaining their default behavior.
+ *
+ * @param context your activities context used to acquire the style resource
+ * @return the Location Layer builder which contains the default values defined by the style
+ * resource
+ */
+ public static Builder builder(Context context) {
+ return LocationComponentOptions.createFromAttributes(context,
+ R.style.mapbox_LocationComponent).toBuilder();
+ }
+
+ /**
+ * Set the opacity of the accuracy view to a value from 0 to 1, where 0 means the accuracy view is
+ * completely transparent and 1 means the view is completely opaque.
+ *
+ * @return the opacity of the accuracy view
+ * @attr ref R.styleable#LocationLayer_accuracyAlpha
+ */
+ public float accuracyAlpha() {
+ return accuracyAlpha;
+ }
+
+ /**
+ * Solid color to use as the accuracy view color property.
+ *
+ * @return the color of the accuracy view
+ * @attr ref R.styleable#LocationLayer_accuracyColor
+ */
+ @ColorInt
+ public int accuracyColor() {
+ return accuracyColor;
+ }
+
+ /**
+ * Defines the drawable used for the stale background icon.
+ *
+ * @return the drawable resource ID
+ * @attr ref R.styleable#LocationLayer_backgroundDrawableStale
+ */
+ @DrawableRes
+ public int backgroundDrawableStale() {
+ return backgroundDrawableStale;
+ }
+
+ /**
+ * String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_foregroundDrawableStale.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @return String icon or maki-icon name
+ */
+ @Nullable
+ public String backgroundStaleName() {
+ return backgroundStaleName;
+ }
+
+ /**
+ * Defines the drawable used for the stale foreground icon.
+ *
+ * @return the drawable resource ID
+ * @attr ref R.styleable#LocationLayer_foregroundDrawableStale
+ */
+ @DrawableRes
+ public int foregroundDrawableStale() {
+ return foregroundDrawableStale;
+ }
+
+ /**
+ * String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_foregroundDrawableStale.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @return String icon or maki-icon name
+ */
+ @Nullable
+ public String foregroundStaleName() {
+ return foregroundStaleName;
+ }
+
+ /**
+ * Defines the drawable used for the navigation state icon.
+ *
+ * @return the drawable resource ID
+ * @attr ref R.styleable#LocationLayer_gpsDrawable
+ */
+ @DrawableRes
+ public int gpsDrawable() {
+ return gpsDrawable;
+ }
+
+ /**
+ * String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_gpsDrawable.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @return String icon or maki-icon name
+ */
+ @Nullable
+ public String gpsName() {
+ return gpsName;
+ }
+
+ /**
+ * Supply a Drawable that is to be rendered on top of all of the content in the Location Layer
+ * Plugin layer stack.
+ *
+ * @return the drawable resource used for the foreground layer
+ * @attr ref R.styleable#LocationLayer_foregroundDrawable
+ */
+ @DrawableRes
+ public int foregroundDrawable() {
+ return foregroundDrawable;
+ }
+
+ /**
+ * String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_foregroundDrawable.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @return String icon or maki-icon name
+ */
+ @Nullable
+ public String foregroundName() {
+ return foregroundName;
+ }
+
+ /**
+ * Defines the drawable used for the background state icon.
+ *
+ * @return the drawable resource ID
+ * @attr ref R.styleable#LocationLayer_backgroundDrawable
+ */
+ @DrawableRes
+ public int backgroundDrawable() {
+ return backgroundDrawable;
+ }
+
+ /**
+ * String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_backgroundDrawable.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @return String icon or maki-icon name
+ */
+ @Nullable
+ public String backgroundName() {
+ return backgroundName;
+ }
+
+ /**
+ * Defines the drawable used for the bearing icon.
+ *
+ * @return the drawable resource ID
+ * @attr ref R.styleable#LocationLayer_bearingDrawable
+ */
+ @DrawableRes
+ public int bearingDrawable() {
+ return bearingDrawable;
+ }
+
+ /**
+ * String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_bearingDrawable.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @return String icon or maki-icon name
+ */
+ @Nullable
+ public String bearingName() {
+ return bearingName;
+ }
+
+ /**
+ * Defines the bearing icon color as an integer.
+ *
+ * @return the color integer resource
+ * @attr ref R.styleable#LocationLayer_bearingTintColor
+ */
+ @ColorInt
+ @Nullable
+ public Integer bearingTintColor() {
+ return bearingTintColor;
+ }
+
+ /**
+ * Defines the foreground color as an integer.
+ *
+ * @return the color integer resource
+ * @attr ref R.styleable#LocationLayer_foregroundTintColor
+ */
+ @ColorInt
+ @Nullable
+ public Integer foregroundTintColor() {
+ return foregroundTintColor;
+ }
+
+ /**
+ * Defines the background color as an integer.
+ *
+ * @return the color integer resource
+ * @attr ref R.styleable#LocationLayer_backgroundTintColor
+ */
+ @ColorInt
+ @Nullable
+ public Integer backgroundTintColor() {
+ return backgroundTintColor;
+ }
+
+ /**
+ * Defines the foreground stale color as an integer.
+ *
+ * @return the color integer resource
+ * @attr ref R.styleable#LocationLayer_foregroundStaleTintColor
+ */
+ @ColorInt
+ @Nullable
+ public Integer foregroundStaleTintColor() {
+ return foregroundStaleTintColor;
+ }
+
+ /**
+ * Defines the background stale color as an integer.
+ *
+ * @return the color integer resource
+ * @attr ref R.styleable#LocationLayer_backgroundStaleTintColor
+ */
+ @ColorInt
+ @Nullable
+ public Integer backgroundStaleTintColor() {
+ return backgroundStaleTintColor;
+ }
+
+ /**
+ * Sets the base elevation of this view, in pixels.
+ *
+ * @return the elevation currently set for the location layer icon
+ * @attr ref R.styleable#LocationLayer_elevation
+ */
+ @Dimension
+ public float elevation() {
+ return elevation;
+ }
+
+ /**
+ * Enable or disable to stale state mode. This mode indicates to the user that the location being
+ * displayed on the map hasn't been updated in a specific amount of time.
+ *
+ * @return whether the stale state mode is enabled or not
+ * @attr ref R.styleable#LocationLayer_enableStaleState
+ */
+ public boolean enableStaleState() {
+ return enableStaleState;
+ }
+
+ /**
+ * Set the delay before the location icon becomes stale. The timer begins approximately when a new
+ * location update comes in and using this defined time, if an update hasn't occured by the end,
+ * the location is considered stale.
+ *
+ * @return the duration in milliseconds which it should take before the location layer is
+ * considered stale
+ * @attr ref R.styleable#LocationLayer_staleStateDelay
+ */
+ public long staleStateTimeout() {
+ return staleStateTimeout;
+ }
+
+ /**
+ * Sets the distance from the edges of the map view’s frame to the edges of the map
+ * view’s logical viewport.
+ * </p>
+ * <p>
+ * When the value of this property is equal to {0,0,0,0}, viewport
+ * properties such as `centerCoordinate` assume a viewport that matches the map
+ * view’s frame. Otherwise, those properties are inset, excluding part of the
+ * frame from the viewport. For instance, if the only the top edge is inset, the
+ * map center is effectively shifted downward.
+ * </p>
+ *
+ * @return integer array of padding values
+ */
+ @SuppressWarnings("mutable")
+ public int[] padding() {
+ return padding;
+ }
+
+ /**
+ * The maximum zoom level the map can be displayed at.
+ *
+ * @return the maximum zoom level
+ */
+ public double maxZoom() {
+ return maxZoom;
+ }
+
+ /**
+ * The minimum zoom level the map can be displayed at.
+ *
+ * @return the minimum zoom level
+ */
+ public double minZoom() {
+ return minZoom;
+ }
+
+ /**
+ * The scale factor of the location icon when the map is zoomed in. Based on {@link #maxZoom()}.
+ * Scaling is linear.
+ *
+ * @return icon scale factor
+ */
+ public float maxZoomIconScale() {
+ return maxZoomIconScale;
+ }
+
+ /**
+ * The scale factor of the location icon when the map is zoomed out. Based on {@link #minZoom()}.
+ * Scaling is linear.
+ *
+ * @return icon scale factor
+ */
+ public float minZoomIconScale() {
+ return minZoomIconScale;
+ }
+
+ /**
+ * Minimum single pointer movement in pixels required to break camera tracking.
+ *
+ * @return the minimum movement
+ */
+ public float trackingInitialMoveThreshold() {
+ return trackingInitialMoveThreshold;
+ }
+
+ /**
+ * Minimum multi pointer movement in pixels required to break camera tracking (for example during scale gesture).
+ *
+ * @return the minimum movement
+ */
+ public float trackingMultiFingerMoveThreshold() {
+ return trackingMultiFingerMoveThreshold;
+ }
+
+ /**
+ * Gets the id of the layer to add the location layer above to.
+ *
+ * @return layerBelow the id of the layer to add the location layer above to
+ */
+ public String layerBelow() {
+ return layerBelow;
+ }
+
+ @Override
+ public String toString() {
+ return "LocationComponentOptions{"
+ + "accuracyAlpha=" + accuracyAlpha + ", "
+ + "accuracyColor=" + accuracyColor + ", "
+ + "backgroundDrawableStale=" + backgroundDrawableStale + ", "
+ + "backgroundStaleName=" + backgroundStaleName + ", "
+ + "foregroundDrawableStale=" + foregroundDrawableStale + ", "
+ + "foregroundStaleName=" + foregroundStaleName + ", "
+ + "gpsDrawable=" + gpsDrawable + ", "
+ + "gpsName=" + gpsName + ", "
+ + "foregroundDrawable=" + foregroundDrawable + ", "
+ + "foregroundName=" + foregroundName + ", "
+ + "backgroundDrawable=" + backgroundDrawable + ", "
+ + "backgroundName=" + backgroundName + ", "
+ + "bearingDrawable=" + bearingDrawable + ", "
+ + "bearingName=" + bearingName + ", "
+ + "bearingTintColor=" + bearingTintColor + ", "
+ + "foregroundTintColor=" + foregroundTintColor + ", "
+ + "backgroundTintColor=" + backgroundTintColor + ", "
+ + "foregroundStaleTintColor=" + foregroundStaleTintColor + ", "
+ + "backgroundStaleTintColor=" + backgroundStaleTintColor + ", "
+ + "elevation=" + elevation + ", "
+ + "enableStaleState=" + enableStaleState + ", "
+ + "staleStateTimeout=" + staleStateTimeout + ", "
+ + "padding=" + Arrays.toString(padding) + ", "
+ + "maxZoom=" + maxZoom + ", "
+ + "minZoom=" + minZoom + ", "
+ + "maxZoomIconScale=" + maxZoomIconScale + ", "
+ + "minZoomIconScale=" + minZoomIconScale + ", "
+ + "trackingInitialMoveThreshold=" + trackingInitialMoveThreshold + ", "
+ + "trackingMultiFingerMoveThreshold=" + trackingMultiFingerMoveThreshold + ", "
+ + "layerBelow=" + layerBelow
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof LocationComponentOptions) {
+ LocationComponentOptions that = (LocationComponentOptions) o;
+ return (Float.floatToIntBits(this.accuracyAlpha) == Float.floatToIntBits(that.accuracyAlpha()))
+ && (this.accuracyColor == that.accuracyColor())
+ && (this.backgroundDrawableStale == that.backgroundDrawableStale())
+ && ((this.backgroundStaleName == null) ? (that.backgroundStaleName() == null)
+ : this.backgroundStaleName.equals(that.backgroundStaleName()))
+ && (this.foregroundDrawableStale == that.foregroundDrawableStale())
+ && ((this.foregroundStaleName == null) ? (that.foregroundStaleName() == null)
+ : this.foregroundStaleName.equals(that.foregroundStaleName()))
+ && (this.gpsDrawable == that.gpsDrawable())
+ && ((this.gpsName == null) ? (that.gpsName() == null) : this.gpsName.equals(that.gpsName()))
+ && (this.foregroundDrawable == that.foregroundDrawable())
+ && ((this.foregroundName == null) ? (that.foregroundName() == null)
+ : this.foregroundName.equals(that.foregroundName()))
+ && (this.backgroundDrawable == that.backgroundDrawable())
+ && ((this.backgroundName == null) ? (that.backgroundName() == null)
+ : this.backgroundName.equals(that.backgroundName()))
+ && (this.bearingDrawable == that.bearingDrawable())
+ && ((this.bearingName == null) ? (that.bearingName() == null)
+ : this.bearingName.equals(that.bearingName()))
+ && ((this.bearingTintColor == null) ? (that.bearingTintColor() == null)
+ : this.bearingTintColor.equals(that.bearingTintColor()))
+ && ((this.foregroundTintColor == null) ? (that.foregroundTintColor() == null)
+ : this.foregroundTintColor.equals(that.foregroundTintColor()))
+ && ((this.backgroundTintColor == null) ? (that.backgroundTintColor() == null)
+ : this.backgroundTintColor.equals(that.backgroundTintColor()))
+ && ((this.foregroundStaleTintColor == null) ? (that.foregroundStaleTintColor() == null)
+ : this.foregroundStaleTintColor.equals(that.foregroundStaleTintColor()))
+ && ((this.backgroundStaleTintColor == null) ? (that.backgroundStaleTintColor() == null)
+ : this.backgroundStaleTintColor.equals(that.backgroundStaleTintColor()))
+ && (Float.floatToIntBits(this.elevation) == Float.floatToIntBits(that.elevation()))
+ && (this.enableStaleState == that.enableStaleState())
+ && (this.staleStateTimeout == that.staleStateTimeout())
+ && (Arrays.equals(this.padding, that.padding())
+ && (Double.doubleToLongBits(this.maxZoom) == Double.doubleToLongBits(that.maxZoom()))
+ && (Double.doubleToLongBits(this.minZoom) == Double.doubleToLongBits(that.minZoom()))
+ && (Float.floatToIntBits(this.maxZoomIconScale) == Float.floatToIntBits(that.maxZoomIconScale()))
+ && (Float.floatToIntBits(this.minZoomIconScale) == Float.floatToIntBits(that.minZoomIconScale()))
+ && (Float.floatToIntBits(this.trackingInitialMoveThreshold)
+ == Float.floatToIntBits(that.trackingInitialMoveThreshold()))
+ && (Float.floatToIntBits(this.trackingMultiFingerMoveThreshold)
+ == Float.floatToIntBits(that.trackingMultiFingerMoveThreshold()))
+ && layerBelow.equals(that.layerBelow));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h$ = 1;
+ h$ *= 1000003;
+ h$ ^= Float.floatToIntBits(accuracyAlpha);
+ h$ *= 1000003;
+ h$ ^= accuracyColor;
+ h$ *= 1000003;
+ h$ ^= backgroundDrawableStale;
+ h$ *= 1000003;
+ h$ ^= (backgroundStaleName == null) ? 0 : backgroundStaleName.hashCode();
+ h$ *= 1000003;
+ h$ ^= foregroundDrawableStale;
+ h$ *= 1000003;
+ h$ ^= (foregroundStaleName == null) ? 0 : foregroundStaleName.hashCode();
+ h$ *= 1000003;
+ h$ ^= gpsDrawable;
+ h$ *= 1000003;
+ h$ ^= (gpsName == null) ? 0 : gpsName.hashCode();
+ h$ *= 1000003;
+ h$ ^= foregroundDrawable;
+ h$ *= 1000003;
+ h$ ^= (foregroundName == null) ? 0 : foregroundName.hashCode();
+ h$ *= 1000003;
+ h$ ^= backgroundDrawable;
+ h$ *= 1000003;
+ h$ ^= (backgroundName == null) ? 0 : backgroundName.hashCode();
+ h$ *= 1000003;
+ h$ ^= bearingDrawable;
+ h$ *= 1000003;
+ h$ ^= (bearingName == null) ? 0 : bearingName.hashCode();
+ h$ *= 1000003;
+ h$ ^= (bearingTintColor == null) ? 0 : bearingTintColor.hashCode();
+ h$ *= 1000003;
+ h$ ^= (foregroundTintColor == null) ? 0 : foregroundTintColor.hashCode();
+ h$ *= 1000003;
+ h$ ^= (backgroundTintColor == null) ? 0 : backgroundTintColor.hashCode();
+ h$ *= 1000003;
+ h$ ^= (foregroundStaleTintColor == null) ? 0 : foregroundStaleTintColor.hashCode();
+ h$ *= 1000003;
+ h$ ^= (backgroundStaleTintColor == null) ? 0 : backgroundStaleTintColor.hashCode();
+ h$ *= 1000003;
+ h$ ^= Float.floatToIntBits(elevation);
+ h$ *= 1000003;
+ h$ ^= enableStaleState ? 1231 : 1237;
+ h$ *= 1000003;
+ h$ ^= (int) ((staleStateTimeout >>> 32) ^ staleStateTimeout);
+ h$ *= 1000003;
+ h$ ^= Arrays.hashCode(padding);
+ h$ *= 1000003;
+ h$ ^= (int) ((Double.doubleToLongBits(maxZoom) >>> 32) ^ Double.doubleToLongBits(maxZoom));
+ h$ *= 1000003;
+ h$ ^= (int) ((Double.doubleToLongBits(minZoom) >>> 32) ^ Double.doubleToLongBits(minZoom));
+ h$ *= 1000003;
+ h$ ^= Float.floatToIntBits(maxZoomIconScale);
+ h$ *= 1000003;
+ h$ ^= Float.floatToIntBits(minZoomIconScale);
+ h$ *= 1000003;
+ h$ ^= Float.floatToIntBits(trackingInitialMoveThreshold);
+ h$ *= 1000003;
+ h$ ^= Float.floatToIntBits(trackingMultiFingerMoveThreshold);
+ return h$;
+ }
+
+ public static final Parcelable.Creator<LocationComponentOptions> CREATOR =
+ new Parcelable.Creator<LocationComponentOptions>() {
+ @Override
+ public LocationComponentOptions createFromParcel(Parcel in) {
+ return new LocationComponentOptions(
+ in.readFloat(),
+ in.readInt(),
+ in.readInt(),
+ in.readInt() == 0 ? in.readString() : null,
+ in.readInt(),
+ in.readInt() == 0 ? in.readString() : null,
+ in.readInt(),
+ in.readInt() == 0 ? in.readString() : null,
+ in.readInt(),
+ in.readInt() == 0 ? in.readString() : null,
+ in.readInt(),
+ in.readInt() == 0 ? in.readString() : null,
+ in.readInt(),
+ in.readInt() == 0 ? in.readString() : null,
+ in.readInt() == 0 ? in.readInt() : null,
+ in.readInt() == 0 ? in.readInt() : null,
+ in.readInt() == 0 ? in.readInt() : null,
+ in.readInt() == 0 ? in.readInt() : null,
+ in.readInt() == 0 ? in.readInt() : null,
+ in.readFloat(),
+ in.readInt() == 1,
+ in.readLong(),
+ in.createIntArray(),
+ in.readDouble(),
+ in.readDouble(),
+ in.readFloat(),
+ in.readFloat(),
+ in.readFloat(),
+ in.readFloat(),
+ in.readString()
+ );
+ }
+
+ @Override
+ public LocationComponentOptions[] newArray(int size) {
+ return new LocationComponentOptions[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeFloat(accuracyAlpha());
+ dest.writeInt(accuracyColor());
+ dest.writeInt(backgroundDrawableStale());
+ if (backgroundStaleName() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeString(backgroundStaleName());
+ }
+ dest.writeInt(foregroundDrawableStale());
+ if (foregroundStaleName() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeString(foregroundStaleName());
+ }
+ dest.writeInt(gpsDrawable());
+ if (gpsName() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeString(gpsName());
+ }
+ dest.writeInt(foregroundDrawable());
+ if (foregroundName() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeString(foregroundName());
+ }
+ dest.writeInt(backgroundDrawable());
+ if (backgroundName() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeString(backgroundName());
+ }
+ dest.writeInt(bearingDrawable());
+ if (bearingName() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeString(bearingName());
+ }
+ if (bearingTintColor() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeInt(bearingTintColor());
+ }
+ if (foregroundTintColor() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeInt(foregroundTintColor());
+ }
+ if (backgroundTintColor() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeInt(backgroundTintColor());
+ }
+ if (foregroundStaleTintColor() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeInt(foregroundStaleTintColor());
+ }
+ if (backgroundStaleTintColor() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeInt(backgroundStaleTintColor());
+ }
+ dest.writeFloat(elevation());
+ dest.writeInt(enableStaleState() ? 1 : 0);
+ dest.writeLong(staleStateTimeout());
+ dest.writeIntArray(padding());
+ dest.writeDouble(maxZoom());
+ dest.writeDouble(minZoom());
+ dest.writeFloat(maxZoomIconScale());
+ dest.writeFloat(minZoomIconScale());
+ dest.writeFloat(trackingInitialMoveThreshold());
+ dest.writeFloat(trackingMultiFingerMoveThreshold());
+ dest.writeString(layerBelow());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Builder class for constructing a new instance of {@link LocationComponentOptions}.
+ */
+ public static class Builder {
+
+ /**
+ * Build a new instance of this {@link LocationComponentOptions} class.
+ *
+ * @return a new instance of {@link LocationComponentOptions}
+ */
+ public LocationComponentOptions build() {
+ LocationComponentOptions locationComponentOptions = autoBuild();
+ if (locationComponentOptions.accuracyAlpha() < 0 || locationComponentOptions.accuracyAlpha() > 1) {
+ throw new IllegalArgumentException(
+ "Accuracy alpha value must be between 0.0 and 1.0.");
+ }
+
+ if (locationComponentOptions.elevation() < 0f) {
+ throw new IllegalArgumentException("Invalid shadow size "
+ + locationComponentOptions.elevation() + ". Must be >= 0");
+ }
+
+ return locationComponentOptions;
+ }
+
+ private Float accuracyAlpha;
+ private Integer accuracyColor;
+ private Integer backgroundDrawableStale;
+ private String backgroundStaleName;
+ private Integer foregroundDrawableStale;
+ private String foregroundStaleName;
+ private Integer gpsDrawable;
+ private String gpsName;
+ private Integer foregroundDrawable;
+ private String foregroundName;
+ private Integer backgroundDrawable;
+ private String backgroundName;
+ private Integer bearingDrawable;
+ private String bearingName;
+ private Integer bearingTintColor;
+ private Integer foregroundTintColor;
+ private Integer backgroundTintColor;
+ private Integer foregroundStaleTintColor;
+ private Integer backgroundStaleTintColor;
+ private Float elevation;
+ private Boolean enableStaleState;
+ private Long staleStateTimeout;
+ private int[] padding;
+ private Double maxZoom;
+ private Double minZoom;
+ private Float maxZoomIconScale;
+ private Float minZoomIconScale;
+ private Float trackingInitialMoveThreshold;
+ private Float trackingMultiFingerMoveThreshold;
+ private String layerBelow;
+
+ Builder() {
+ }
+
+ private Builder(LocationComponentOptions source) {
+ this.accuracyAlpha = source.accuracyAlpha();
+ this.accuracyColor = source.accuracyColor();
+ this.backgroundDrawableStale = source.backgroundDrawableStale();
+ this.backgroundStaleName = source.backgroundStaleName();
+ this.foregroundDrawableStale = source.foregroundDrawableStale();
+ this.foregroundStaleName = source.foregroundStaleName();
+ this.gpsDrawable = source.gpsDrawable();
+ this.gpsName = source.gpsName();
+ this.foregroundDrawable = source.foregroundDrawable();
+ this.foregroundName = source.foregroundName();
+ this.backgroundDrawable = source.backgroundDrawable();
+ this.backgroundName = source.backgroundName();
+ this.bearingDrawable = source.bearingDrawable();
+ this.bearingName = source.bearingName();
+ this.bearingTintColor = source.bearingTintColor();
+ this.foregroundTintColor = source.foregroundTintColor();
+ this.backgroundTintColor = source.backgroundTintColor();
+ this.foregroundStaleTintColor = source.foregroundStaleTintColor();
+ this.backgroundStaleTintColor = source.backgroundStaleTintColor();
+ this.elevation = source.elevation();
+ this.enableStaleState = source.enableStaleState();
+ this.staleStateTimeout = source.staleStateTimeout();
+ this.padding = source.padding();
+ this.maxZoom = source.maxZoom();
+ this.minZoom = source.minZoom();
+ this.maxZoomIconScale = source.maxZoomIconScale();
+ this.minZoomIconScale = source.minZoomIconScale();
+ this.trackingInitialMoveThreshold = source.trackingInitialMoveThreshold();
+ this.trackingMultiFingerMoveThreshold = source.trackingMultiFingerMoveThreshold();
+ this.layerBelow = source.layerBelow();
+ }
+
+ /**
+ * Set the opacity of the accuracy view to a value from 0 to 1, where 0 means the accuracy view
+ * is completely transparent and 1 means the view is completely opaque.
+ *
+ * @param accuracyAlpha the opacity of the accuracy view
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_accuracyAlpha
+ */
+ public LocationComponentOptions.Builder accuracyAlpha(float accuracyAlpha) {
+ this.accuracyAlpha = accuracyAlpha;
+ return this;
+ }
+
+ /**
+ * Solid color to use as the accuracy view color property.
+ *
+ * @param accuracyColor the color of the accuracy view
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_accuracyColor
+ */
+ public LocationComponentOptions.Builder accuracyColor(int accuracyColor) {
+ this.accuracyColor = accuracyColor;
+ return this;
+ }
+
+ /**
+ * Defines the drawable used for the stale background icon.
+ *
+ * @param backgroundDrawableStale the drawable resource ID
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_backgroundDrawableStale
+ */
+ public LocationComponentOptions.Builder backgroundDrawableStale(int backgroundDrawableStale) {
+ this.backgroundDrawableStale = backgroundDrawableStale;
+ return this;
+ }
+
+ /**
+ * Given a String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_backgroundDrawableStale.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @param backgroundStaleName String icon or maki-icon name
+ * @return this builder for chaining options together
+ */
+ public LocationComponentOptions.Builder backgroundStaleName(@Nullable String backgroundStaleName) {
+ this.backgroundStaleName = backgroundStaleName;
+ return this;
+ }
+
+ /**
+ * Defines the drawable used for the stale foreground icon.
+ *
+ * @param foregroundDrawableStale the drawable resource ID
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_foregroundDrawableStale
+ */
+ public LocationComponentOptions.Builder foregroundDrawableStale(int foregroundDrawableStale) {
+ this.foregroundDrawableStale = foregroundDrawableStale;
+ return this;
+ }
+
+ /**
+ * Given a String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_foregroundDrawableStale.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @param foregroundStaleName String icon or maki-icon name
+ * @return this builder for chaining options together
+ */
+ public LocationComponentOptions.Builder foregroundStaleName(@Nullable String foregroundStaleName) {
+ this.foregroundStaleName = foregroundStaleName;
+ return this;
+ }
+
+ /**
+ * Defines the drawable used for the navigation state icon.
+ *
+ * @param gpsDrawable the drawable resource ID
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_gpsDrawable
+ */
+ public LocationComponentOptions.Builder gpsDrawable(int gpsDrawable) {
+ this.gpsDrawable = gpsDrawable;
+ return this;
+ }
+
+ /**
+ * Given a String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_gpsDrawable.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @param gpsName String icon or maki-icon name
+ * @return this builder for chaining options together
+ */
+ public LocationComponentOptions.Builder gpsName(@Nullable String gpsName) {
+ this.gpsName = gpsName;
+ return this;
+ }
+
+ /**
+ * Supply a Drawable that is to be rendered on top of all of the content in the Location Layer
+ * Plugin layer stack.
+ *
+ * @param foregroundDrawable the drawable resource used for the foreground layer
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_foregroundDrawable
+ */
+ public LocationComponentOptions.Builder foregroundDrawable(int foregroundDrawable) {
+ this.foregroundDrawable = foregroundDrawable;
+ return this;
+ }
+
+ /**
+ * Given a String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_foregroundDrawable.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @param foregroundName String icon or maki-icon name
+ * @return this builder for chaining options together
+ */
+ public LocationComponentOptions.Builder foregroundName(@Nullable String foregroundName) {
+ this.foregroundName = foregroundName;
+ return this;
+ }
+
+ /**
+ * Defines the drawable used for the background state icon.
+ *
+ * @param backgroundDrawable the drawable resource ID
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_backgroundDrawable
+ */
+ public LocationComponentOptions.Builder backgroundDrawable(int backgroundDrawable) {
+ this.backgroundDrawable = backgroundDrawable;
+ return this;
+ }
+
+ /**
+ * Given a String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_backgroundDrawable.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @param backgroundName String icon or maki-icon name
+ * @return this builder for chaining options together
+ */
+ public LocationComponentOptions.Builder backgroundName(@Nullable String backgroundName) {
+ this.backgroundName = backgroundName;
+ return this;
+ }
+
+ /**
+ * Defines the drawable used for the bearing icon.
+ *
+ * @param bearingDrawable the drawable resource ID
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_bearingDrawable
+ */
+ public LocationComponentOptions.Builder bearingDrawable(int bearingDrawable) {
+ this.bearingDrawable = bearingDrawable;
+ return this;
+ }
+
+ /**
+ * Given a String image name, identical to one used in
+ * the first parameter of {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}, the
+ * plugin, will used this image in place of the provided or default mapbox_bearingDrawable.
+ * <p>
+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded
+ * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style.
+ * </p>
+ *
+ * @param bearingName String icon or maki-icon name
+ * @return this builder for chaining options together
+ */
+ public LocationComponentOptions.Builder bearingName(@Nullable String bearingName) {
+ this.bearingName = bearingName;
+ return this;
+ }
+
+ /**
+ * Defines the bearing icon color as an integer.
+ *
+ * @param bearingTintColor the color integer resource
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_bearingTintColor
+ */
+ public LocationComponentOptions.Builder bearingTintColor(@Nullable Integer bearingTintColor) {
+ this.bearingTintColor = bearingTintColor;
+ return this;
+ }
+
+ /**
+ * Defines the foreground color as an integer.
+ *
+ * @param foregroundTintColor the color integer resource
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_foregroundTintColor
+ */
+ public LocationComponentOptions.Builder foregroundTintColor(@Nullable Integer foregroundTintColor) {
+ this.foregroundTintColor = foregroundTintColor;
+ return this;
+ }
+
+ /**
+ * Defines the background color as an integer.
+ *
+ * @param backgroundTintColor the color integer resource
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_backgroundTintColor
+ */
+ public LocationComponentOptions.Builder backgroundTintColor(@Nullable Integer backgroundTintColor) {
+ this.backgroundTintColor = backgroundTintColor;
+ return this;
+ }
+
+ /**
+ * Defines the foreground stale color as an integer.
+ *
+ * @param foregroundStaleTintColor the color integer resource
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_foregroundStaleTintColor
+ */
+ public LocationComponentOptions.Builder foregroundStaleTintColor(@Nullable Integer foregroundStaleTintColor) {
+ this.foregroundStaleTintColor = foregroundStaleTintColor;
+ return this;
+ }
+
+ /**
+ * Defines the background stale color as an integer.
+ *
+ * @param backgroundStaleTintColor the color integer resource
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_backgroundStaleTintColor
+ */
+ public LocationComponentOptions.Builder backgroundStaleTintColor(@Nullable Integer backgroundStaleTintColor) {
+ this.backgroundStaleTintColor = backgroundStaleTintColor;
+ return this;
+ }
+
+ /**
+ * Sets the base elevation of this view, in pixels.
+ *
+ * @param elevation the elevation currently set for the location layer icon
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_elevation
+ */
+ public LocationComponentOptions.Builder elevation(float elevation) {
+ this.elevation = elevation;
+ return this;
+ }
+
+ /**
+ * Enable or disable to stale state mode. This mode indicates to the user that the location
+ * being displayed on the map hasn't been updated in a specific amount of time.
+ *
+ * @param enabled whether the stale state mode is enabled or not
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_enableStaleState
+ */
+ public LocationComponentOptions.Builder enableStaleState(boolean enabled) {
+ this.enableStaleState = enabled;
+ return this;
+ }
+
+ /**
+ * Set the timeout before the location icon becomes stale. The timer begins approximately when a
+ * new location update comes in and using this defined time, if an update hasn't occurred by the
+ * end, the location is considered stale.
+ *
+ * @param timeout the duration in milliseconds which it should take before the location layer is
+ * considered stale
+ * @return this builder for chaining options together
+ * @attr ref R.styleable#LocationLayer_staleStateTimeout
+ */
+ public LocationComponentOptions.Builder staleStateTimeout(long timeout) {
+ this.staleStateTimeout = timeout;
+ return this;
+ }
+
+ /**
+ * Sets the distance from the edges of the map view’s frame to the edges of the map
+ * view’s logical viewport.
+ * </p>
+ * <p>
+ * When the value of this property is equal to {0,0,0,0}, viewport
+ * properties such as `centerCoordinate` assume a viewport that matches the map
+ * view’s frame. Otherwise, those properties are inset, excluding part of the
+ * frame from the viewport. For instance, if the only the top edge is inset, the
+ * map center is effectively shifted downward.
+ * </p>
+ *
+ * @param padding The margins for the map in pixels (left, top, right, bottom).
+ */
+ public LocationComponentOptions.Builder padding(int[] padding) {
+ if (padding == null) {
+ throw new NullPointerException("Null padding");
+ }
+ this.padding = padding;
+ return this;
+ }
+
+ /**
+ * Sets the maximum zoom level the map can be displayed at.
+ * <p>
+ * The default maximum zoomn level is 22. The upper bound for this value is 25.5.
+ *
+ * @param maxZoom The new maximum zoom level.
+ */
+ public LocationComponentOptions.Builder maxZoom(double maxZoom) {
+ this.maxZoom = maxZoom;
+ return this;
+ }
+
+ /**
+ * Sets the minimum zoom level the map can be displayed at.
+ *
+ * @param minZoom The new minimum zoom level.
+ */
+ public LocationComponentOptions.Builder minZoom(double minZoom) {
+ this.minZoom = minZoom;
+ return this;
+ }
+
+ /**
+ * Sets the scale factor of the location icon when the map is zoomed in. Based on {@link #maxZoom()}.
+ * Scaling is linear and the new pixel size of the image will be the original pixel size multiplied by the argument.
+ * <p>
+ * Set both this and {@link #minZoomIconScale(float)} to 1f to disable location icon scaling.
+ * </p>
+ *
+ * @param maxZoomIconScale icon scale factor
+ */
+ public LocationComponentOptions.Builder maxZoomIconScale(float maxZoomIconScale) {
+ this.maxZoomIconScale = maxZoomIconScale;
+ return this;
+ }
+
+ /**
+ * Sets the scale factor of the location icon when the map is zoomed out. Based on {@link #maxZoom()}.
+ * Scaling is linear and the new pixel size of the image will be the original pixel size multiplied by the argument.
+ * <p>
+ * Set both this and {@link #maxZoomIconScale(float)} to 1f to disable location icon scaling.
+ * </p>
+ *
+ * @param minZoomIconScale icon scale factor
+ */
+ public LocationComponentOptions.Builder minZoomIconScale(float minZoomIconScale) {
+ this.minZoomIconScale = minZoomIconScale;
+ return this;
+ }
+
+ /**
+ * Sets minimum single pointer movement (map pan) in pixels required to break camera tracking.
+ *
+ * @param moveThreshold the minimum movement
+ */
+ public LocationComponentOptions.Builder trackingInitialMoveThreshold(float moveThreshold) {
+ this.trackingInitialMoveThreshold = moveThreshold;
+ return this;
+ }
+
+ /**
+ * Sets minimum multi pointer movement (map pan) in pixels required to break camera tracking
+ * (for example during scale gesture).
+ *
+ * @param moveThreshold the minimum movement
+ */
+ public LocationComponentOptions.Builder trackingMultiFingerMoveThreshold(float moveThreshold) {
+ this.trackingMultiFingerMoveThreshold = moveThreshold;
+ return this;
+ }
+
+ /**
+ * Sets the layer id to set the location layer plugin below to.
+ *
+ * @param layerBelow the id to set the location layer plugin below to.
+ */
+ public LocationComponentOptions.Builder layerBelow(String layerBelow) {
+ this.layerBelow = layerBelow;
+ return this;
+ }
+
+ LocationComponentOptions autoBuild() {
+ String missing = "";
+ if (this.accuracyAlpha == null) {
+ missing += " accuracyAlpha";
+ }
+ if (this.accuracyColor == null) {
+ missing += " accuracyColor";
+ }
+ if (this.backgroundDrawableStale == null) {
+ missing += " backgroundDrawableStale";
+ }
+ if (this.foregroundDrawableStale == null) {
+ missing += " foregroundDrawableStale";
+ }
+ if (this.gpsDrawable == null) {
+ missing += " gpsDrawable";
+ }
+ if (this.foregroundDrawable == null) {
+ missing += " foregroundDrawable";
+ }
+ if (this.backgroundDrawable == null) {
+ missing += " backgroundDrawable";
+ }
+ if (this.bearingDrawable == null) {
+ missing += " bearingDrawable";
+ }
+ if (this.elevation == null) {
+ missing += " elevation";
+ }
+ if (this.enableStaleState == null) {
+ missing += " enableStaleState";
+ }
+ if (this.staleStateTimeout == null) {
+ missing += " staleStateTimeout";
+ }
+ if (this.padding == null) {
+ missing += " padding";
+ }
+ if (this.maxZoom == null) {
+ missing += " maxZoom";
+ }
+ if (this.minZoom == null) {
+ missing += " minZoom";
+ }
+ if (this.maxZoomIconScale == null) {
+ missing += " maxZoomIconScale";
+ }
+ if (this.minZoomIconScale == null) {
+ missing += " minZoomIconScale";
+ }
+ if (this.trackingInitialMoveThreshold == null) {
+ missing += " trackingInitialMoveThreshold";
+ }
+ if (this.trackingMultiFingerMoveThreshold == null) {
+ missing += " trackingMultiFingerMoveThreshold";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new LocationComponentOptions(
+ this.accuracyAlpha,
+ this.accuracyColor,
+ this.backgroundDrawableStale,
+ this.backgroundStaleName,
+ this.foregroundDrawableStale,
+ this.foregroundStaleName,
+ this.gpsDrawable,
+ this.gpsName,
+ this.foregroundDrawable,
+ this.foregroundName,
+ this.backgroundDrawable,
+ this.backgroundName,
+ this.bearingDrawable,
+ this.bearingName,
+ this.bearingTintColor,
+ this.foregroundTintColor,
+ this.backgroundTintColor,
+ this.foregroundStaleTintColor,
+ this.backgroundStaleTintColor,
+ this.elevation,
+ this.enableStaleState,
+ this.staleStateTimeout,
+ this.padding,
+ this.maxZoom,
+ this.minZoom,
+ this.maxZoomIconScale,
+ this.minZoomIconScale,
+ this.trackingInitialMoveThreshold,
+ this.trackingMultiFingerMoveThreshold,
+ this.layerBelow);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java
new file mode 100644
index 0000000000..75826f911b
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java
@@ -0,0 +1,394 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.location.modes.RenderMode;
+import com.mapbox.mapboxsdk.style.layers.Layer;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.ACCURACY_LAYER;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_LAYER;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_STALE_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_LAYER;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_LAYER;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_STALE_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.LOCATION_SOURCE;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_ALPHA;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_COLOR;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_RADIUS;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_STALE_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BEARING_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_COMPASS_BEARING;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON_OFFSET;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_STALE_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_LOCATION_STALE;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.linear;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
+import static com.mapbox.mapboxsdk.style.layers.Property.NONE;
+import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.colorToRgbaString;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
+
+final class LocationLayerController implements MapboxAnimator.OnLayerAnimationsValuesChangeListener {
+
+ @RenderMode.Mode
+ private int renderMode;
+
+ private final MapboxMap mapboxMap;
+ private final LayerSourceProvider layerSourceProvider;
+ private final LayerBitmapProvider bitmapProvider;
+ private LocationComponentOptions options;
+
+ private final List<String> layerMap = new ArrayList<>();
+ private Feature locationFeature;
+ private GeoJsonSource locationSource;
+
+ private boolean isHidden = true;
+
+ LocationLayerController(MapboxMap mapboxMap, LayerSourceProvider layerSourceProvider,
+ LayerFeatureProvider featureProvider, LayerBitmapProvider bitmapProvider,
+ LocationComponentOptions options) {
+ this.mapboxMap = mapboxMap;
+ this.layerSourceProvider = layerSourceProvider;
+ this.bitmapProvider = bitmapProvider;
+ this.locationFeature = featureProvider.generateLocationFeature(locationFeature, options);
+ initializeComponents(options);
+ setRenderMode(RenderMode.NORMAL);
+ }
+
+ void initializeComponents(LocationComponentOptions options) {
+ addLocationSource();
+ addLayers(options.layerBelow());
+ applyStyle(options);
+
+ if (isHidden) {
+ hide();
+ } else {
+ show();
+ }
+ }
+
+ void applyStyle(@NonNull LocationComponentOptions options) {
+ this.options = options;
+
+ float elevation = options.elevation();
+ // Only add icon elevation if the values greater than 0.
+ if (elevation > 0) {
+ styleShadow(options);
+ }
+ styleForeground(options);
+ styleBackground(options);
+ styleBearing(options);
+ styleAccuracy(options.accuracyAlpha(), options.accuracyColor());
+ styleScaling(options);
+ determineIconsSource(options);
+ }
+
+ void setRenderMode(@RenderMode.Mode int renderMode) {
+ this.renderMode = renderMode;
+
+ if (!isHidden) {
+ boolean isStale = locationFeature.getBooleanProperty(PROPERTY_LOCATION_STALE);
+ switch (renderMode) {
+ case RenderMode.NORMAL:
+ styleForeground(options);
+ setLayerVisibility(SHADOW_LAYER, true);
+ setLayerVisibility(FOREGROUND_LAYER, true);
+ setLayerVisibility(BACKGROUND_LAYER, true);
+ setLayerVisibility(ACCURACY_LAYER, !isStale);
+ setLayerVisibility(BEARING_LAYER, false);
+ break;
+ case RenderMode.COMPASS:
+ styleForeground(options);
+ setLayerVisibility(SHADOW_LAYER, true);
+ setLayerVisibility(FOREGROUND_LAYER, true);
+ setLayerVisibility(BACKGROUND_LAYER, true);
+ setLayerVisibility(ACCURACY_LAYER, !isStale);
+ setLayerVisibility(BEARING_LAYER, true);
+ break;
+ case RenderMode.GPS:
+ styleForeground(options);
+ setLayerVisibility(SHADOW_LAYER, false);
+ setLayerVisibility(FOREGROUND_LAYER, true);
+ setLayerVisibility(BACKGROUND_LAYER, true);
+ setLayerVisibility(ACCURACY_LAYER, false);
+ setLayerVisibility(BEARING_LAYER, false);
+ break;
+ default:
+ break;
+ }
+
+ determineIconsSource(options);
+ }
+ }
+
+ int getRenderMode() {
+ return renderMode;
+ }
+
+ //
+ // Layer action
+ //
+
+ void show() {
+ isHidden = false;
+ setRenderMode(renderMode);
+ }
+
+ void hide() {
+ isHidden = true;
+ for (String layerId : layerMap) {
+ setLayerVisibility(layerId, false);
+ }
+ }
+
+ void updateForegroundOffset(double tilt) {
+ JsonArray foregroundJsonArray = new JsonArray();
+ foregroundJsonArray.add(0f);
+ foregroundJsonArray.add((float) (-0.05 * tilt));
+ locationFeature.addProperty(PROPERTY_FOREGROUND_ICON_OFFSET, foregroundJsonArray);
+
+ JsonArray backgroundJsonArray = new JsonArray();
+ backgroundJsonArray.add(0f);
+ backgroundJsonArray.add((float) (0.05 * tilt));
+ locationFeature.addProperty(PROPERTY_SHADOW_ICON_OFFSET, backgroundJsonArray);
+
+ refreshSource();
+ }
+
+ void updateForegroundBearing(float bearing) {
+ if (renderMode != RenderMode.GPS) {
+ setBearingProperty(PROPERTY_GPS_BEARING, bearing);
+ }
+ }
+
+ private void setLayerVisibility(String layerId, boolean visible) {
+ Layer layer = mapboxMap.getLayer(layerId);
+ if (layer != null) {
+ String targetVisibility = visible ? VISIBLE : NONE;
+ if (!layer.getVisibility().value.equals(targetVisibility)) {
+ layer.setProperties(visibility(visible ? VISIBLE : NONE));
+ }
+ }
+ }
+
+ private void addLayers(String idBelowLayer) {
+ addSymbolLayer(BEARING_LAYER, idBelowLayer);
+ addSymbolLayer(FOREGROUND_LAYER, BEARING_LAYER);
+ addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER);
+ addSymbolLayer(SHADOW_LAYER, BACKGROUND_LAYER);
+ addAccuracyLayer();
+ }
+
+ private void addSymbolLayer(String layerId, String beforeLayerId) {
+ Layer layer = layerSourceProvider.generateLayer(layerId);
+ addLayerToMap(layer, beforeLayerId);
+ }
+
+ private void addAccuracyLayer() {
+ Layer accuracyLayer = layerSourceProvider.generateAccuracyLayer();
+ addLayerToMap(accuracyLayer, BACKGROUND_LAYER);
+ }
+
+ private void addLayerToMap(Layer layer, @NonNull String idBelowLayer) {
+ mapboxMap.addLayerBelow(layer, idBelowLayer);
+ layerMap.add(layer.getId());
+ }
+
+ private void setBearingProperty(String propertyId, float bearing) {
+ locationFeature.addNumberProperty(propertyId, bearing);
+ refreshSource();
+ }
+
+ private void updateAccuracyRadius(float accuracy) {
+ if (renderMode == RenderMode.COMPASS || renderMode == RenderMode.NORMAL) {
+ locationFeature.addNumberProperty(PROPERTY_ACCURACY_RADIUS, accuracy);
+ refreshSource();
+ }
+ }
+
+ //
+ // Source actions
+ //
+
+ private void addLocationSource() {
+ locationSource = layerSourceProvider.generateSource(locationFeature);
+ mapboxMap.addSource(locationSource);
+ }
+
+ private void refreshSource() {
+ GeoJsonSource source = mapboxMap.getSourceAs(LOCATION_SOURCE);
+ if (source != null) {
+ locationSource.setGeoJson(locationFeature);
+ }
+ }
+
+ private void setLocationPoint(Point locationPoint) {
+ JsonObject properties = locationFeature.properties();
+ if (properties != null) {
+ locationFeature = Feature.fromGeometry(locationPoint, properties);
+ refreshSource();
+ }
+ }
+
+ //
+ // Styling
+ //
+
+ private void styleBackground(LocationComponentOptions options) {
+ Bitmap backgroundBitmap = bitmapProvider.generateBitmap(
+ options.backgroundDrawable(), options.backgroundTintColor()
+ );
+ Bitmap backgroundStaleBitmap = bitmapProvider.generateBitmap(
+ options.backgroundDrawableStale(), options.backgroundStaleTintColor()
+ );
+ mapboxMap.addImage(BACKGROUND_ICON, backgroundBitmap);
+ mapboxMap.addImage(BACKGROUND_STALE_ICON, backgroundStaleBitmap);
+ }
+
+ private void styleShadow(LocationComponentOptions options) {
+ mapboxMap.addImage(SHADOW_ICON, bitmapProvider.generateShadowBitmap(options));
+ }
+
+ private void styleBearing(LocationComponentOptions options) {
+ Bitmap bearingBitmap = bitmapProvider.generateBitmap(options.bearingDrawable(), options.bearingTintColor());
+ mapboxMap.addImage(BEARING_ICON, bearingBitmap);
+ }
+
+ private void styleAccuracy(float accuracyAlpha, @ColorInt int accuracyColor) {
+ locationFeature.addNumberProperty(PROPERTY_ACCURACY_ALPHA, accuracyAlpha);
+ locationFeature.addStringProperty(PROPERTY_ACCURACY_COLOR, colorToRgbaString(accuracyColor));
+ refreshSource();
+ }
+
+ private void styleForeground(LocationComponentOptions options) {
+ Bitmap foregroundBitmap = bitmapProvider.generateBitmap(
+ options.foregroundDrawable(), options.foregroundTintColor()
+ );
+ Bitmap foregroundBitmapStale = bitmapProvider.generateBitmap(
+ options.foregroundDrawableStale(), options.foregroundStaleTintColor()
+ );
+ if (renderMode == RenderMode.GPS) {
+ foregroundBitmap = bitmapProvider.generateBitmap(
+ options.gpsDrawable(), options.foregroundTintColor()
+ );
+ foregroundBitmapStale = bitmapProvider.generateBitmap(
+ options.gpsDrawable(), options.foregroundStaleTintColor()
+ );
+ }
+ mapboxMap.addImage(FOREGROUND_ICON, foregroundBitmap);
+ mapboxMap.addImage(FOREGROUND_STALE_ICON, foregroundBitmapStale);
+ }
+
+ private void styleScaling(LocationComponentOptions options) {
+ for (String layerId : layerMap) {
+ Layer layer = mapboxMap.getLayer(layerId);
+ if (layer != null && layer instanceof SymbolLayer) {
+ layer.setProperties(
+ iconSize(
+ interpolate(linear(), zoom(),
+ stop(options.minZoom(), options.minZoomIconScale()),
+ stop(options.maxZoom(), options.maxZoomIconScale())
+ )
+ )
+ );
+ }
+ }
+ }
+
+ private void determineIconsSource(LocationComponentOptions options) {
+ String foregroundIconString = buildIconString(
+ renderMode == RenderMode.GPS ? options.gpsName() : options.foregroundName(), FOREGROUND_ICON);
+ String foregroundStaleIconString = buildIconString(options.foregroundStaleName(), FOREGROUND_STALE_ICON);
+ String backgroundIconString = buildIconString(options.backgroundName(), BACKGROUND_ICON);
+ String backgroundStaleIconString = buildIconString(options.backgroundStaleName(), BACKGROUND_STALE_ICON);
+ String bearingIconString = buildIconString(options.bearingName(), BEARING_ICON);
+
+ locationFeature.addStringProperty(PROPERTY_FOREGROUND_ICON, foregroundIconString);
+ locationFeature.addStringProperty(PROPERTY_BACKGROUND_ICON, backgroundIconString);
+ locationFeature.addStringProperty(PROPERTY_FOREGROUND_STALE_ICON, foregroundStaleIconString);
+ locationFeature.addStringProperty(PROPERTY_BACKGROUND_STALE_ICON, backgroundStaleIconString);
+ locationFeature.addStringProperty(PROPERTY_BEARING_ICON, bearingIconString);
+ refreshSource();
+ }
+
+ private String buildIconString(@Nullable String bitmapName, @NonNull String drawableName) {
+ if (bitmapName != null) {
+ return bitmapName;
+ }
+ return drawableName;
+ }
+
+ void setLocationsStale(boolean isStale) {
+ locationFeature.addBooleanProperty(PROPERTY_LOCATION_STALE, isStale);
+ refreshSource();
+ if (renderMode != RenderMode.GPS) {
+ setLayerVisibility(ACCURACY_LAYER, !isStale);
+ }
+ }
+
+ //
+ // Map click event
+ //
+
+ boolean onMapClick(LatLng point) {
+ PointF screenLoc = mapboxMap.getProjection().toScreenLocation(point);
+ List<Feature> features = mapboxMap.queryRenderedFeatures(screenLoc,
+ BACKGROUND_LAYER,
+ FOREGROUND_LAYER,
+ BEARING_LAYER
+ );
+ return !features.isEmpty();
+ }
+
+ @Override
+ public void onNewLatLngValue(LatLng latLng) {
+ Point point = Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude());
+ setLocationPoint(point);
+ }
+
+ @Override
+ public void onNewGpsBearingValue(float gpsBearing) {
+ if (renderMode == RenderMode.GPS) {
+ setBearingProperty(PROPERTY_GPS_BEARING, gpsBearing);
+ }
+ }
+
+ @Override
+ public void onNewCompassBearingValue(float compassBearing) {
+ if (renderMode == RenderMode.COMPASS) {
+ setBearingProperty(PROPERTY_COMPASS_BEARING, compassBearing);
+ }
+ }
+
+ @Override
+ public void onNewAccuracyRadiusValue(float accuracyRadiusValue) {
+ updateAccuracyRadius(accuracyRadiusValue);
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java
new file mode 100644
index 0000000000..b22601a15c
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java
@@ -0,0 +1,92 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
+import android.support.annotation.IntDef;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Abstract class for all of the plugin animators.
+ *
+ * @param <K> Data type that will be animated.
+ * @param <L> Listener of animation updates.
+ */
+abstract class MapboxAnimator<K, L> extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef( {
+ ANIMATOR_LAYER_LATLNG,
+ ANIMATOR_CAMERA_LATLNG,
+ ANIMATOR_LAYER_GPS_BEARING,
+ ANIMATOR_LAYER_COMPASS_BEARING,
+ ANIMATOR_CAMERA_GPS_BEARING,
+ ANIMATOR_CAMERA_COMPASS_BEARING,
+ ANIMATOR_LAYER_ACCURACY,
+ ANIMATOR_ZOOM,
+ ANIMATOR_TILT
+ })
+ @interface Type {
+ }
+
+ static final int ANIMATOR_LAYER_LATLNG = 0;
+ static final int ANIMATOR_CAMERA_LATLNG = 1;
+ static final int ANIMATOR_LAYER_GPS_BEARING = 2;
+ static final int ANIMATOR_LAYER_COMPASS_BEARING = 3;
+ static final int ANIMATOR_CAMERA_GPS_BEARING = 4;
+ static final int ANIMATOR_CAMERA_COMPASS_BEARING = 5;
+ static final int ANIMATOR_LAYER_ACCURACY = 6;
+ static final int ANIMATOR_ZOOM = 7;
+ static final int ANIMATOR_TILT = 8;
+
+ private final int animatorType = provideAnimatorType();
+ final List<L> updateListeners;
+ private final K target;
+
+ MapboxAnimator(K previous, K target, List<L> updateListeners) {
+ setObjectValues(previous, target);
+ setEvaluator(provideEvaluator());
+ this.updateListeners = updateListeners;
+ this.target = target;
+ addUpdateListener(this);
+ }
+
+ K getTarget() {
+ return target;
+ }
+
+ @Type
+ int getAnimatorType() {
+ return animatorType;
+ }
+
+ @Type
+ abstract int provideAnimatorType();
+
+ abstract TypeEvaluator provideEvaluator();
+
+ interface OnLayerAnimationsValuesChangeListener {
+ void onNewLatLngValue(LatLng latLng);
+
+ void onNewGpsBearingValue(float gpsBearing);
+
+ void onNewCompassBearingValue(float compassBearing);
+
+ void onNewAccuracyRadiusValue(float accuracyRadiusValue);
+ }
+
+ interface OnCameraAnimationsValuesChangeListener {
+ void onNewLatLngValue(LatLng latLng);
+
+ void onNewGpsBearingValue(float gpsBearing);
+
+ void onNewCompassBearingValue(float compassBearing);
+
+ void onNewZoomValue(float zoom);
+
+ void onNewTiltValue(float tilt);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxCameraAnimatorAdapter.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxCameraAnimatorAdapter.java
new file mode 100644
index 0000000000..89d27a38fa
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxCameraAnimatorAdapter.java
@@ -0,0 +1,38 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.support.annotation.Nullable;
+
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+
+import java.util.List;
+
+abstract class MapboxCameraAnimatorAdapter extends
+ MapboxFloatAnimator<MapboxAnimator.OnCameraAnimationsValuesChangeListener> {
+ private final MapboxMap.CancelableCallback cancelableCallback;
+
+ MapboxCameraAnimatorAdapter(Float previous, Float target,
+ List<OnCameraAnimationsValuesChangeListener> updateListeners,
+ @Nullable MapboxMap.CancelableCallback cancelableCallback) {
+ super(previous, target, updateListeners);
+ this.cancelableCallback = cancelableCallback;
+ addListener(new MapboxAnimatorListener());
+ }
+
+ private final class MapboxAnimatorListener extends AnimatorListenerAdapter {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (cancelableCallback != null) {
+ cancelableCallback.onCancel();
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (cancelableCallback != null) {
+ cancelableCallback.onFinish();
+ }
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxFloatAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxFloatAnimator.java
new file mode 100644
index 0000000000..4a6d8c3b73
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxFloatAnimator.java
@@ -0,0 +1,17 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.FloatEvaluator;
+import android.animation.TypeEvaluator;
+
+import java.util.List;
+
+abstract class MapboxFloatAnimator<L> extends MapboxAnimator<Float, L> {
+ MapboxFloatAnimator(Float previous, Float target, List<L> updateListeners) {
+ super(previous, target, updateListeners);
+ }
+
+ @Override
+ TypeEvaluator provideEvaluator() {
+ return new FloatEvaluator();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxLatLngAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxLatLngAnimator.java
new file mode 100644
index 0000000000..b2f1b61a2d
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxLatLngAnimator.java
@@ -0,0 +1,19 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.TypeEvaluator;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import java.util.List;
+
+abstract class MapboxLatLngAnimator<L> extends MapboxAnimator<LatLng, L> {
+
+ MapboxLatLngAnimator(LatLng previous, LatLng target, List<L> updateListeners) {
+ super(previous, target, updateListeners);
+ }
+
+ @Override
+ TypeEvaluator provideEvaluator() {
+ return new LatLngEvaluator();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraMoveInvalidateListener.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraMoveInvalidateListener.java
new file mode 100644
index 0000000000..6594e543ff
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraMoveInvalidateListener.java
@@ -0,0 +1,7 @@
+package com.mapbox.mapboxsdk.location;
+
+interface OnCameraMoveInvalidateListener {
+
+ void onInvalidateCameraMove();
+
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraTrackingChangedListener.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraTrackingChangedListener.java
new file mode 100644
index 0000000000..1ea80d26aa
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraTrackingChangedListener.java
@@ -0,0 +1,21 @@
+package com.mapbox.mapboxsdk.location;
+
+import com.mapbox.mapboxsdk.location.modes.CameraMode;
+
+/**
+ * Listener that gets invoked when camera tracking state changes.
+ */
+public interface OnCameraTrackingChangedListener {
+ /**
+ * Invoked whenever camera tracking is broken.
+ * This callback gets invoked just after {@link #onCameraTrackingChanged(int)}, if needed.
+ */
+ void onCameraTrackingDismissed();
+
+ /**
+ * Invoked on every {@link CameraMode} change.
+ *
+ * @param currentMode current active {@link CameraMode}.
+ */
+ void onCameraTrackingChanged(@CameraMode.Mode int currentMode);
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationComponentClickListener.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationComponentClickListener.java
new file mode 100644
index 0000000000..c592ae3a25
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationComponentClickListener.java
@@ -0,0 +1,11 @@
+package com.mapbox.mapboxsdk.location;
+
+/**
+ * The Location Layer Plugin exposes an API for listening to when the user clicks on the location
+ * layer icon visible on the map. when this event occurs, the {@link #onLocationComponentClick()} method
+ * gets invoked.
+ */
+public interface OnLocationComponentClickListener {
+
+ void onLocationComponentClick();
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationComponentLongClickListener.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationComponentLongClickListener.java
new file mode 100644
index 0000000000..dc63e48a58
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationComponentLongClickListener.java
@@ -0,0 +1,11 @@
+package com.mapbox.mapboxsdk.location;
+
+/**
+ * The Location Layer Plugin exposes an API for listening to when the user long clicks on the location
+ * layer icon visible on the map. when this event occurs, the {@link #onLocationComponentLongClick()} method
+ * gets invoked.
+ */
+public interface OnLocationComponentLongClickListener {
+
+ void onLocationComponentLongClick();
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationStaleListener.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationStaleListener.java
new file mode 100644
index 0000000000..d42eddf277
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationStaleListener.java
@@ -0,0 +1,13 @@
+package com.mapbox.mapboxsdk.location;
+
+/**
+ * Listener that can be added as a callback when the last location update
+ * is considered stale.
+ * <p>
+ * The time from the last location update that determines if a location update
+ * is stale or not is provided by {@link LocationComponentOptions#staleStateTimeout()}.
+ */
+public interface OnLocationStaleListener {
+
+ void onStaleStateChange(boolean isStale);
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/StaleStateManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/StaleStateManager.java
new file mode 100644
index 0000000000..c2d50610fa
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/StaleStateManager.java
@@ -0,0 +1,80 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.os.Handler;
+
+/**
+ * Class controls the location layer stale state when the {@link android.location.Location} hasn't
+ * been updated in 'x' amount of time. {@link LocationComponentOptions#staleStateTimeout()} can be used to
+ * control the amount of time before the locations considered stale.
+ * {@link LocationComponentOptions#enableStaleState()} is available for disabling this behaviour.
+ */
+class StaleStateManager {
+
+ private boolean isEnabled;
+ private final OnLocationStaleListener innerOnLocationStaleListeners;
+ private final Handler handler;
+ private boolean isStale = true;
+ private long delayTime;
+
+ StaleStateManager(OnLocationStaleListener innerListener, LocationComponentOptions options) {
+ innerOnLocationStaleListeners = innerListener;
+ handler = new Handler();
+ isEnabled = options.enableStaleState();
+ delayTime = options.staleStateTimeout();
+ }
+
+ private Runnable staleStateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ setState(true);
+ }
+ };
+
+ void setEnabled(boolean enabled) {
+ if (enabled) {
+ setState(isStale);
+ } else if (isEnabled) {
+ onStop();
+ innerOnLocationStaleListeners.onStaleStateChange(false);
+ }
+ isEnabled = enabled;
+ }
+
+ boolean isStale() {
+ return isStale;
+ }
+
+ void updateLatestLocationTime() {
+ setState(false);
+ postTheCallback();
+ }
+
+ void setDelayTime(long delayTime) {
+ this.delayTime = delayTime;
+ postTheCallback();
+ }
+
+ void onStart() {
+ if (!isStale) {
+ postTheCallback();
+ }
+ }
+
+ void onStop() {
+ handler.removeCallbacksAndMessages(null);
+ }
+
+ private void postTheCallback() {
+ handler.removeCallbacksAndMessages(null);
+ handler.postDelayed(staleStateRunnable, delayTime);
+ }
+
+ private void setState(boolean stale) {
+ if (stale != isStale) {
+ isStale = stale;
+ if (isEnabled) {
+ innerOnLocationStaleListeners.onStaleStateChange(stale);
+ }
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/TiltAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/TiltAnimator.java
new file mode 100644
index 0000000000..8ff0f97a70
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/TiltAnimator.java
@@ -0,0 +1,27 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.ValueAnimator;
+import android.support.annotation.Nullable;
+
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+
+import java.util.List;
+
+class TiltAnimator extends MapboxCameraAnimatorAdapter {
+ TiltAnimator(Float previous, Float target, List<OnCameraAnimationsValuesChangeListener> updateListeners,
+ @Nullable MapboxMap.CancelableCallback cancelableCallback) {
+ super(previous, target, updateListeners, cancelableCallback);
+ }
+
+ @Override
+ int provideAnimatorType() {
+ return ANIMATOR_TILT;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (OnCameraAnimationsValuesChangeListener listener : updateListeners) {
+ listener.onNewTiltValue((Float) animation.getAnimatedValue());
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java
new file mode 100644
index 0000000000..aa01c914dd
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java
@@ -0,0 +1,102 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.Build;
+import android.support.annotation.ColorInt;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+
+public final class Utils {
+
+ private Utils() {
+ // Class should not be initialized
+ }
+
+ /**
+ * Util for finding the shortest path from the current icon rotated degree to the new degree.
+ *
+ * @param magneticHeading the new position of the rotation
+ * @param previousMagneticHeading the current position of the rotation
+ * @return the shortest degree of rotation possible
+ */
+ public static float shortestRotation(float magneticHeading, float previousMagneticHeading) {
+ double diff = previousMagneticHeading - magneticHeading;
+ if (diff > 180.0f) {
+ magneticHeading += 360.0f;
+ } else if (diff < -180.0f) {
+ magneticHeading -= 360.f;
+ }
+ return magneticHeading;
+ }
+
+ static Bitmap getBitmapFromDrawable(Drawable drawable) {
+ if (drawable instanceof BitmapDrawable) {
+ return ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ // width and height are equal for all assets since they are ovals.
+ Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bitmap;
+ }
+ }
+
+ static Bitmap generateShadow(Drawable drawable, float elevation) {
+ int width = drawable.getIntrinsicWidth();
+ int height = drawable.getIntrinsicHeight();
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ bitmap = Bitmap.createScaledBitmap(bitmap,
+ toEven(width + elevation), toEven(height + elevation), false);
+ return bitmap;
+ }
+
+ static Drawable getDrawable(@NonNull Context context, @DrawableRes int drawableRes,
+ @ColorInt Integer tintColor) {
+ Drawable drawable = ContextCompat.getDrawable(context, drawableRes);
+ if (tintColor == null) {
+ return drawable;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ drawable.setTint(tintColor);
+ } else {
+ drawable.mutate().setColorFilter(tintColor, PorterDuff.Mode.SRC_IN);
+ }
+ return drawable;
+ }
+
+ static float calculateZoomLevelRadius(@NonNull MapboxMap mapboxMap, @Nullable Location location) {
+ if (location == null) {
+ return 0;
+ }
+ double metersPerPixel = mapboxMap.getProjection().getMetersPerPixelAtLatitude(
+ location.getLatitude());
+ return (float) (location.getAccuracy() * (1 / metersPerPixel));
+ }
+
+ /**
+ * Casts the value to an even integer.
+ */
+ private static int toEven(float value) {
+ int i = (int) (value + .5f);
+ if (i % 2 == 1) {
+ return i - 1;
+ }
+ return i;
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/ZoomAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/ZoomAnimator.java
new file mode 100644
index 0000000000..204a1457dc
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/ZoomAnimator.java
@@ -0,0 +1,29 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.animation.ValueAnimator;
+import android.support.annotation.Nullable;
+
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+
+import java.util.List;
+
+class ZoomAnimator extends MapboxCameraAnimatorAdapter {
+
+ ZoomAnimator(Float previous, Float target, List<OnCameraAnimationsValuesChangeListener> updateListeners,
+ @Nullable MapboxMap.CancelableCallback cancelableCallback) {
+ super(previous, target, updateListeners, cancelableCallback);
+ }
+
+ @Override
+ int provideAnimatorType() {
+ return ANIMATOR_ZOOM;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ for (OnCameraAnimationsValuesChangeListener listener : updateListeners) {
+ listener.onNewZoomValue((Float) animation.getAnimatedValue());
+ }
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/CameraMode.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/CameraMode.java
new file mode 100644
index 0000000000..51806a33cd
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/CameraMode.java
@@ -0,0 +1,66 @@
+package com.mapbox.mapboxsdk.location.modes;
+
+import android.location.Location;
+import android.support.annotation.IntDef;
+
+import com.mapbox.mapboxsdk.location.LocationComponent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Contains the variety of camera modes which determine how the camera will track
+ * the user location.
+ */
+public final class CameraMode {
+
+ private CameraMode() {
+ // Class should not be initialized
+ }
+
+ /**
+ * Determine the camera tracking behavior in the {@link LocationComponent}.
+ */
+ @IntDef( {NONE, NONE_COMPASS, NONE_GPS, TRACKING, TRACKING_COMPASS, TRACKING_GPS, TRACKING_GPS_NORTH})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Mode {
+ }
+
+ /**
+ * No camera tracking.
+ */
+ public static final int NONE = 0x00000008;
+
+ /**
+ * Camera does not track location, but does track compass bearing.
+ */
+ public static final int NONE_COMPASS = 0x00000010;
+
+ /**
+ * Camera does not track location, but does track GPS {@link Location} bearing.
+ */
+ public static final int NONE_GPS = 0x00000016;
+
+ /**
+ * Camera tracks the user location.
+ */
+ public static final int TRACKING = 0x00000018;
+
+ /**
+ * Camera tracks the user location, with bearing
+ * provided by a compass.
+ */
+ public static final int TRACKING_COMPASS = 0x00000020;
+
+ /**
+ * Camera tracks the user location, with bearing
+ * provided by a normalized {@link Location#getBearing()}.
+ */
+ public static final int TRACKING_GPS = 0x00000022;
+
+ /**
+ * Camera tracks the user location, with bearing
+ * always set to north (0).
+ */
+ public static final int TRACKING_GPS_NORTH = 0x00000024;
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/RenderMode.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/RenderMode.java
new file mode 100644
index 0000000000..0f7fa24008
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/RenderMode.java
@@ -0,0 +1,45 @@
+package com.mapbox.mapboxsdk.location.modes;
+
+import android.support.annotation.IntDef;
+
+import com.mapbox.mapboxsdk.location.CompassEngine;
+import com.mapbox.mapboxsdk.location.LocationComponent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Contains the variety of ways the user location can be rendered on the map.
+ */
+public final class RenderMode {
+
+ private RenderMode() {
+ // Class should not be initialized
+ }
+
+ /**
+ * One of these constants should be used with {@link LocationComponent#setRenderMode(int)}.
+ * mode can be switched at anytime by calling the {@code setLocationLayerMode} method passing
+ * in the new mode you'd like the location layer to be in.
+ */
+ @IntDef( {COMPASS, GPS, NORMAL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Mode {
+ }
+
+ /**
+ * Basic tracking is enabled, bearing ignored.
+ */
+ public static final int NORMAL = 0x00000012;
+
+ /**
+ * Tracking the user location with bearing considered
+ * from a {@link CompassEngine}.
+ */
+ public static final int COMPASS = 0x00000004;
+
+ /**
+ * Tracking the user location with bearing considered from {@link android.location.Location}.
+ */
+ public static final int GPS = 0x00000008;
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/package-info.java
new file mode 100644
index 0000000000..c871a16ebe
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains the Mapbox Location layer plugin.
+ */
+package com.mapbox.mapboxsdk.location; \ No newline at end of file