diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java index fc945bcff..e45f0fda5 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java @@ -89,13 +89,9 @@ void updateAnimatorListenerHolders(@NonNull Set listener } } - void feedNewLocation(@NonNull Location newLocation, @NonNull CameraPosition currentCameraPosition, - boolean isGpsNorth) { - feedNewLocation(new Location[] {newLocation}, currentCameraPosition, isGpsNorth, false); - } - void feedNewLocation(@NonNull @Size(min = 1) Location[] newLocations, - @NonNull CameraPosition currentCameraPosition, boolean isGpsNorth, boolean lookAheadUpdate) { + @NonNull CameraPosition currentCameraPosition, boolean isGpsNorth, + @Nullable Long animationDuration) { Location newLocation = newLocations[newLocations.length - 1]; if (previousLocation == null) { previousLocation = newLocation; @@ -125,28 +121,20 @@ void feedNewLocation(@NonNull @Size(min = 1) Location[] newLocations, boolean snap = immediateAnimation(projection, previousCameraLatLng, targetLatLng) || immediateAnimation(projection, previousLayerLatLng, targetLatLng); - long animationDuration = 0; - if (!snap) { - long previousUpdateTimeStamp = locationUpdateTimestamp; - locationUpdateTimestamp = SystemClock.elapsedRealtime(); - - if (previousUpdateTimeStamp == 0) { - animationDuration = 0; - } else if (lookAheadUpdate) { - long currentTimestamp = System.currentTimeMillis(); - if (currentTimestamp > newLocation.getTime()) { - animationDuration = 0; - Logger.e("LocationAnimatorCoordinator", - "Lookahead enabled, but the target location's timestamp is smaller than current timestamp"); + long previousUpdateTimeStamp = locationUpdateTimestamp; + locationUpdateTimestamp = SystemClock.elapsedRealtime(); + if (animationDuration == null) { + animationDuration = 0L; + if (!snap) { + if (previousUpdateTimeStamp == 0) { + animationDuration = 0L; } else { - animationDuration = newLocation.getTime() - currentTimestamp; + animationDuration = (long) ((locationUpdateTimestamp - previousUpdateTimeStamp) * durationMultiplier) + /* make animation slightly longer with durationMultiplier, defaults to 1.1f */; } - } else { - animationDuration = (long) ((locationUpdateTimestamp - previousUpdateTimeStamp) * durationMultiplier) - /* make animation slightly longer with durationMultiplier, defaults to 1.1f */; - } - animationDuration = Math.min(animationDuration, MAX_ANIMATION_DURATION_MS); + animationDuration = Math.min(animationDuration, MAX_ANIMATION_DURATION_MS); + } } playAnimators(animationDuration, diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java index 14bc45140..7c25ec2bd 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java @@ -4,6 +4,7 @@ import android.content.Context; import android.hardware.SensorManager; import android.location.Location; +import android.os.Build; import android.os.Looper; import android.os.SystemClock; import android.view.WindowManager; @@ -41,6 +42,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; @@ -185,7 +187,7 @@ public final class LocationComponent { private final CopyOnWriteArrayList onRenderModeChangedListeners = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList onIndicatorPositionChangedListener - = new CopyOnWriteArrayList<>(); + = new CopyOnWriteArrayList<>(); // Workaround for too frequent updates, see https://github.com/mapbox/mapbox-gl-native/issues/13587 private long fastestInterval; @@ -239,6 +241,34 @@ public LocationComponent(@NonNull MapboxMap mapboxMap, isComponentInitialized = true; } + @VisibleForTesting + LocationComponent(@NonNull MapboxMap mapboxMap, + @NonNull Transform transform, + @NonNull List developerAnimationListeners, + @NonNull LocationEngineCallback currentListener, + @NonNull LocationLayerController locationLayerController, + @NonNull LocationCameraController locationCameraController, + @NonNull LocationAnimatorCoordinator locationAnimatorCoordinator, + @NonNull StaleStateManager staleStateManager, + @NonNull CompassEngine compassEngine, + @NonNull InternalLocationEngineProvider internalLocationEngineProvider, + boolean useSpecializedLocationLayer, + @NonNull LocationEngineRequest locationEngineRequest) { + this.mapboxMap = mapboxMap; + this.transform = transform; + developerAnimationListeners.add(developerAnimationListener); + this.currentLocationEngineListener = currentListener; + this.locationLayerController = locationLayerController; + this.locationCameraController = locationCameraController; + this.locationAnimatorCoordinator = locationAnimatorCoordinator; + this.staleStateManager = staleStateManager; + this.compassEngine = compassEngine; + this.internalLocationEngineProvider = internalLocationEngineProvider; + this.useSpecializedLocationLayer = useSpecializedLocationLayer; + this.locationEngineRequest = locationEngineRequest; + isComponentInitialized = true; + } + /** * This method initializes the component and needs to be called before any other operations are performed. * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. @@ -885,19 +915,19 @@ public void paddingWhileTracking(double[] padding, long animationDuration) { * @param callback The callback with finish/cancel information */ public void paddingWhileTracking(double[] padding, long animationDuration, - @Nullable MapboxMap.CancelableCallback callback) { + @Nullable MapboxMap.CancelableCallback callback) { checkActivationState(); if (!isLayerReady) { notifyUnsuccessfulCameraOperation(callback, null); return; } else if (getCameraMode() == CameraMode.NONE) { notifyUnsuccessfulCameraOperation(callback, String.format("%s%s", - "LocationComponent#paddingWhileTracking method can only be used", - " when a camera mode other than CameraMode#NONE is engaged.")); + "LocationComponent#paddingWhileTracking method can only be used", + " when a camera mode other than CameraMode#NONE is engaged.")); return; } else if (locationCameraController.isTransitioning()) { notifyUnsuccessfulCameraOperation(callback, - "LocationComponent#paddingWhileTracking method call is ignored because the camera mode is transitioning"); + "LocationComponent#paddingWhileTracking method call is ignored because the camera mode is transitioning"); return; } @@ -996,7 +1026,9 @@ public void cancelTiltWhileTrackingAnimation() { * updated. * * @param location where the location icon is placed on the map + * @deprecated use {@link #forceLocationUpdate(LocationUpdate)} instead */ + @Deprecated public void forceLocationUpdate(@Nullable Location location) { checkActivationState(); updateLocation(location, false); @@ -1014,20 +1046,62 @@ public void forceLocationUpdate(@Nullable Location location) { * @param lookAheadUpdate If set to true, the last location's timestamp has to be greater than current timestamp and * should represent the time at which the animation should actually reach this position, * cutting out the time interpolation delay. + * @deprecated use {@link #forceLocationUpdate(LocationUpdate)} instead */ + @Deprecated public void forceLocationUpdate(@Nullable List locations, boolean lookAheadUpdate) { checkActivationState(); if (locations != null && locations.size() >= 1) { + Location targetLocation = locations.get(locations.size() - 1); + if (targetLocation == null) { + return; + } + LocationUpdate.Builder builder = new LocationUpdate.Builder() + .location(targetLocation) + .intermediatePoints(locations.subList(0, locations.size() - 1)); + + if (lookAheadUpdate) { + long currentTimestampNs; + long locationTimestampNs; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + currentTimestampNs = SystemClock.elapsedRealtimeNanos(); + locationTimestampNs = targetLocation.getElapsedRealtimeNanos(); + if (locationTimestampNs == 0) { + // fallback in case the elapsed realtime is not set + currentTimestampNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + locationTimestampNs = TimeUnit.MILLISECONDS.toNanos(targetLocation.getTime()); + } + } else { + currentTimestampNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + locationTimestampNs = TimeUnit.MILLISECONDS.toNanos(targetLocation.getTime()); + } + + if (currentTimestampNs > locationTimestampNs) { + builder.animationDuration(0L); + Logger.e("LocationAnimatorCoordinator", + "Lookahead enabled, but the target location's timestamp is smaller than current timestamp"); + } else { + builder.animationDuration(locationTimestampNs - currentTimestampNs); + } + } updateLocation( - locations.get(locations.size() - 1), // target location - locations.subList(0, locations.size() - 1), // intermediate locations - false, - lookAheadUpdate); - } else { - updateLocation(null, false); + builder.build(), + false + ); } } + /** + * Use to either force a location update or to manually control when the user location gets + * updated. + * + * @param locationUpdate location update + * @see LocationUpdate.Builder + */ + public void forceLocationUpdate(@NonNull LocationUpdate locationUpdate) { + updateLocation(locationUpdate, false); + } + /** * Set max FPS at which location animators can output updates. The throttling will only impact the location puck * and camera tracking smooth animations. @@ -1515,15 +1589,19 @@ private void updateMapWithOptions(@NonNull LocationComponentOptions options) { * @param location the latest user location */ private void updateLocation(@Nullable final Location location, boolean fromLastLocation) { - updateLocation(location, null, fromLastLocation, false); + if (location != null) { + updateLocation( + new LocationUpdate.Builder() + .location(location) + .build(), + fromLastLocation + ); + } } - private void updateLocation(@Nullable final Location location, @Nullable List intermediatePoints, - boolean fromLastLocation, boolean lookAheadUpdate) { - if (location == null) { - return; - } else if (!isLayerReady) { - lastLocation = location; + private void updateLocation(@NonNull LocationUpdate locationUpdate, boolean fromLastLocation) { + if (!isLayerReady) { + lastLocation = locationUpdate.getLocation(); return; } else { long currentTime = SystemClock.elapsedRealtime(); @@ -1541,17 +1619,13 @@ private void updateLocation(@Nullable final Location location, @Nullable List intermediatePoints) { @@ -1800,7 +1874,8 @@ public void onRenderModeChanged(int currentMode) { @NonNull @VisibleForTesting OnIndicatorPositionChangedListener indicatorPositionChangedListener = new OnIndicatorPositionChangedListener() { - @Override public void onIndicatorPositionChanged(@NonNull Point point) { + @Override + public void onIndicatorPositionChanged(@NonNull Point point) { for (OnIndicatorPositionChangedListener listener : onIndicatorPositionChangedListener) { listener.onIndicatorPositionChanged(point); } diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java index 671012a75..06fced655 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java @@ -1867,6 +1867,8 @@ public LocationComponentOptions.Builder layerBelow(String layerBelow) { /** * Sets the tracking animation duration multiplier. + *

+ * This value is ignored if {@link LocationUpdate.Builder#animationDuration(Long)} is provided. * * @param trackingAnimationDurationMultiplier the tracking animation duration multiplier */ diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationUpdate.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationUpdate.java new file mode 100644 index 000000000..1fd17c5ea --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationUpdate.java @@ -0,0 +1,151 @@ +package com.mapbox.mapboxsdk.location; + +import android.location.Location; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * A class that contains the location update configuration. + * + * @see LocationComponent#forceLocationUpdate(LocationUpdate) + */ +public class LocationUpdate { + + @NonNull + private final Location location; + @NonNull + private final List intermediatePoints; + @Nullable + private final Long animationDuration; + + private LocationUpdate( + @NonNull Location location, + @NonNull List intermediatePoints, + @Nullable Long animationDuration + ) { + this.location = location; + this.intermediatePoints = intermediatePoints; + this.animationDuration = animationDuration; + } + + /** + * @return target location of the transition + */ + @NonNull + public Location getLocation() { + return location; + } + + /** + * @return list of locations that are on the path to the target location for animation interpolation + */ + @NonNull + public List getIntermediatePoints() { + return intermediatePoints; + } + + /** + * @return If set, all of the transitions to this update (puck's and possibly camera's if tracking mode is engaged) + * will have the provided duration. If null, the duration will be calculated internally. + *

+ * {@link LocationComponentOptions.Builder#trackingAnimationDurationMultiplier(float)} + * is ignored if this value is provided. + */ + @Nullable + public Long getAnimationDuration() { + return animationDuration; + } + + @Override + public String toString() { + return "LocationUpdate{" + "location=" + location + ", intermediatePoints=" + intermediatePoints + + ", animationDuration=" + animationDuration + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LocationUpdate that = (LocationUpdate) o; + + if (!location.equals(that.location)) { + return false; + } + if (!intermediatePoints.equals(that.intermediatePoints)) { + return false; + } + return animationDuration != null + ? animationDuration.equals(that.animationDuration) + : that.animationDuration == null; + } + + @Override + public int hashCode() { + int result = location.hashCode(); + result = 31 * result + intermediatePoints.hashCode(); + result = 31 * result + (animationDuration != null ? animationDuration.hashCode() : 0); + return result; + } + + public static class Builder { + + @Nullable + private Location location; + @NonNull + private List intermediatePoints = Collections.emptyList(); + @Nullable + private Long animationDuration; + + /** + * Target location. + */ + public Builder location(@Nullable Location location) { + this.location = location; + return this; + } + + /** + * This method can be used to provide the list of locations that are on the path to the target location. + * Those intermediate points are used as the animation path. + * The puck and the camera will be animated between each of the points linearly until reaching the target. + */ + public Builder intermediatePoints(@NonNull List intermediatePoints) { + this.intermediatePoints = intermediatePoints; + return this; + } + + /** + * If set, all of the transitions to this update (puck's and possibly camera's if tracking mode is engaged) will + * have the provided duration. If null, the duration will be calculated internally. + *

+ * This can also be used to disable transition animation by providing a duration equal to zero. + *

+ * {@link LocationComponentOptions.Builder#trackingAnimationDurationMultiplier(float)} + * is ignored if this value is provided. + */ + public Builder animationDuration(@Nullable Long animationDuration) { + this.animationDuration = animationDuration; + return this; + } + + /** + * Builds a new {@link LocationUpdate}. + */ + public LocationUpdate build() { + if (location == null) { + throw new IllegalArgumentException("target location has to be provided when constructing the LocationUpdate"); + } + + return new LocationUpdate(location, intermediatePoints, animationDuration); + } + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt index 732ac8b66..78f331574 100644 --- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt @@ -7,9 +7,7 @@ import android.util.SparseArray import android.view.animation.LinearInterpolator import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.geometry.LatLng -import com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_PADDING_ANIM_DURATION -import com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_TILT_ANIM_DURATION -import com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_ZOOM_ANIM_DURATION +import com.mapbox.mapboxsdk.location.LocationComponentConstants.* import com.mapbox.mapboxsdk.location.MapboxAnimator.* import com.mapbox.mapboxsdk.location.modes.RenderMode import com.mapbox.mapboxsdk.maps.MapboxMap @@ -108,7 +106,7 @@ class LocationAnimatorCoordinatorTest { @Test fun feedNewLocation_animatorsAreCreated() { - locationAnimatorCoordinator.feedNewLocation(Location(""), cameraPosition, false) + feedNewLocation(Location(""), cameraPosition, false) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG] != null) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING] != null) @@ -122,7 +120,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.0 location.longitude = 17.0 location.bearing = 35f - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val cameraLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG]?.target as LatLng assertEquals(location.latitude, cameraLatLngTarget.latitude) @@ -154,7 +152,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.2 location.longitude = 17.2 location.bearing = 36f - locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, false) + locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, null) val cameraLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG]?.target as LatLng assertEquals(location.latitude, cameraLatLngTarget.latitude) @@ -198,7 +196,7 @@ class LocationAnimatorCoordinatorTest { current.longitude = 17.2 current.bearing = 0f - locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, false, false) + locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, false, null) verify { animatorProvider.floatAnimator( @@ -206,7 +204,7 @@ class LocationAnimatorCoordinatorTest { ) } - locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, true, false) + locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, true, null) verify { animatorProvider.floatAnimator( @@ -216,7 +214,7 @@ class LocationAnimatorCoordinatorTest { } @Test - fun feedNewLocation_animatorValue_multiplePoints_animationDuration() { + fun feedNewLocation_animatorValue_multiplePoints_animationDurationDefaultsToZero() { every { projection.getMetersPerPixelAtLatitude(any()) } answers { 10000.0 } // disable snap val locationInter = Location("") locationInter.latitude = 51.1 @@ -226,7 +224,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.2 location.longitude = 17.2 location.bearing = 36f - locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, false) + locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, null) verify { animatorSetProvider.startAnimation(eq(listOf( @@ -239,7 +237,7 @@ class LocationAnimatorCoordinatorTest { } @Test - fun feedNewLocation_animatorValue_multiplePoints_animationDuration_lookAhead() { + fun feedNewLocation_animatorValue_multiplePoints_externalAnimationDuration() { every { projection.getMetersPerPixelAtLatitude(any()) } answers { 10000.0 } // disable snap val locationInter = Location("") locationInter.latitude = 51.1 @@ -249,8 +247,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.2 location.longitude = 17.2 location.bearing = 36f - location.time = System.currentTimeMillis() + 2000 - locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, true) + locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, 1300) verify { animatorSetProvider.startAnimation(eq(listOf( @@ -258,7 +255,7 @@ class LocationAnimatorCoordinatorTest { locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING], locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG], locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING] - )), any(), more(1500L)) + )), any(), 1300) } } @@ -273,7 +270,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns 270f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(360f, layerBearingTarget) @@ -290,7 +287,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns 280f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(450f, layerBearingTarget) @@ -307,7 +304,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns 450f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(-60f, layerBearingTarget) @@ -324,7 +321,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns 10f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(-10f, layerBearingTarget) @@ -341,7 +338,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns -280f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(90f, layerBearingTarget) @@ -358,7 +355,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns -350f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(-90f, layerBearingTarget) @@ -370,7 +367,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.0 location.longitude = 17.0 location.bearing = 35f - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG] != null) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING] != null) @@ -384,7 +381,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.0 location.longitude = 17.0 location.bearing = 35f - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, true) + feedNewLocation(location, cameraPosition, true) val cameraLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG]?.target as LatLng assertEquals(cameraLatLngTarget.latitude, cameraLatLngTarget.latitude) @@ -534,7 +531,7 @@ class LocationAnimatorCoordinatorTest { @Test fun cancelAllAnimators() { - locationAnimatorCoordinator.feedNewLocation(Location(""), cameraPosition, true) + feedNewLocation(Location(""), cameraPosition, true) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG].isStarted) locationAnimatorCoordinator.cancelAllAnimations() @@ -729,7 +726,7 @@ class LocationAnimatorCoordinatorTest { @Test fun maxFps_givenToAnimator() { locationAnimatorCoordinator.setMaxAnimationFps(5) - locationAnimatorCoordinator.feedNewLocation(Location(""), cameraPosition, false) + feedNewLocation(Location(""), cameraPosition, false) verify { animatorProvider.latLngAnimator(any(), any(), 5) } verify { animatorProvider.floatAnimator(any(), any(), 5) } } @@ -750,6 +747,14 @@ class LocationAnimatorCoordinatorTest { } } } + + private fun feedNewLocation( + newLocation: Location, + currentCameraPosition: CameraPosition, + isGpsNorth: Boolean + ) { + locationAnimatorCoordinator.feedNewLocation(arrayOf(newLocation), currentCameraPosition, isGpsNorth, null) + } } private fun SparseArray.contains(listener: AnimationsValueChangeListener<*>?): Boolean { diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt index 3c9d4865e..5ddb5df0d 100644 --- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt @@ -6,7 +6,9 @@ import android.content.res.TypedArray import android.location.Location import android.os.Looper import com.mapbox.android.core.location.LocationEngine +import com.mapbox.android.core.location.LocationEngineCallback import com.mapbox.android.core.location.LocationEngineRequest +import com.mapbox.android.core.location.LocationEngineResult import com.mapbox.mapboxsdk.R import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS @@ -20,10 +22,8 @@ import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Mock +import org.mockito.* import org.mockito.Mockito.* -import org.mockito.MockitoAnnotations import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -671,4 +671,112 @@ class LocationComponentTest { verify(locationAnimatorCoordinator).feedNewAccuracyRadius(location.accuracy, false) } + + @Test + fun newLocation_lookAhead_animationDurationGenerated() { + val location = Location("test") + location.accuracy = 50f + location.time = System.currentTimeMillis() + 2000 + `when`(style.isFullyLoaded).thenReturn(true) + locationComponent = LocationComponent(mapboxMap, transform, developerAnimationListeners, currentListener, lastListener, locationLayerController, locationCameraController, locationAnimatorCoordinator, staleStateManager, compassEngine, locationEngineProvider, true) + locationComponent.activateLocationComponent( + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useSpecializedLocationLayer(true) + .useDefaultLocationEngine(false) + .build() + ) + locationComponent.isLocationComponentEnabled = true + locationComponent.onStart() + val locations: List = listOf( + mock(Location::class.java), + location + ) + locationComponent.forceLocationUpdate(locations, true) + + verify(locationAnimatorCoordinator).feedNewLocation(eq(locations.toTypedArray()), any(), eq(false), AdditionalMatchers.gt(1500L)) + } + + @Test + fun newLocation_nullAnimationDurationPassed() { + val location = Location("test") + location.accuracy = 50f + `when`(style.isFullyLoaded).thenReturn(true) + locationComponent = LocationComponent(mapboxMap, transform, developerAnimationListeners, currentListener, lastListener, locationLayerController, locationCameraController, locationAnimatorCoordinator, staleStateManager, compassEngine, locationEngineProvider, true) + locationComponent.activateLocationComponent( + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useSpecializedLocationLayer(true) + .useDefaultLocationEngine(false) + .build() + ) + locationComponent.isLocationComponentEnabled = true + locationComponent.onStart() + locationComponent.forceLocationUpdate( + LocationUpdate.Builder() + .location(location) + .build() + ) + verify(locationAnimatorCoordinator).feedNewLocation(eq(listOf(location).toTypedArray()), any(), eq(false), isNull()) + } + + @Test + fun whenComponentReEnabled_noEngine_animationDurationZero() { + val location = Location("test") + location.accuracy = 50f + `when`(style.isFullyLoaded).thenReturn(true) + locationComponent = LocationComponent(mapboxMap, transform, developerAnimationListeners, currentListener, lastListener, locationLayerController, locationCameraController, locationAnimatorCoordinator, staleStateManager, compassEngine, locationEngineProvider, true) + locationComponent.activateLocationComponent( + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useSpecializedLocationLayer(true) + .useDefaultLocationEngine(false) + .build() + ) + locationComponent.isLocationComponentEnabled = true + locationComponent.onStart() + locationComponent.forceLocationUpdate( + LocationUpdate.Builder() + .location(location) + .animationDuration(1500L) + .build() + ) + verify(locationAnimatorCoordinator).feedNewLocation(eq(listOf(location).toTypedArray()), any(), eq(false), eq(1500L)) + + locationComponent.onStop() + locationComponent.onStart() + verify(locationAnimatorCoordinator).feedNewLocation(eq(listOf(location).toTypedArray()), any(), eq(false), eq(0L)) + } + + @Test + fun whenComponentReEnabled_hasEngine_animationDurationZero() { + val engineRequest = LocationEngineRequest.Builder( + 1000L + ) + .setFastestInterval(0L) + .build() + val location = Location("test") + location.accuracy = 50f + val locationEngine = mock(LocationEngine::class.java) + `when`(style.isFullyLoaded).thenReturn(true) + locationComponent = LocationComponent(mapboxMap, transform, developerAnimationListeners, currentListener, locationLayerController, locationCameraController, locationAnimatorCoordinator, staleStateManager, compassEngine, locationEngineProvider, true, engineRequest) + locationComponent.activateLocationComponent( + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useSpecializedLocationLayer(true) + .useDefaultLocationEngine(false) + .locationEngine(locationEngine) + .build() + ) + locationComponent.isLocationComponentEnabled = true + locationComponent.onStart() + + val capture: ArgumentCaptor> = ArgumentCaptor.forClass(LocationEngineCallback::class.java as Class>) + verify(locationEngine).getLastLocation(capture.capture()) + val result = mock(LocationEngineResult::class.java) + `when`(result.lastLocation).thenReturn(location) + capture.value.onSuccess(result) + + verify(locationAnimatorCoordinator).feedNewLocation(eq(listOf(location).toTypedArray()), any(), eq(false), eq(0L)) + } } \ No newline at end of file