summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorŁukasz Paczos <lukas.paczos@gmail.com>2019-02-07 10:26:25 +0100
committerŁukasz Paczos <lukas.paczos@gmail.com>2019-02-11 16:55:49 +0100
commitf56d56b3c60881a45aaa5a1b1ef1280ef37dd045 (patch)
tree5acd5948f7cebd7097f6d95f7fdd06347be5aec5
parent7acaa3b467552e952354b17bd39b913a02a90167 (diff)
downloadqtlocation-mapboxgl-upstream/lp-13770-gestures-bounds.tar.gz
[android] limit the acceptable gestures offset if camera bounds are presentupstream/lp-13770-gestures-bounds
-rw-r--r--include/mbgl/map/map.hpp1
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java125
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMap.java11
-rwxr-xr-xplatform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java36
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java13
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java15
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/MathUtils.java59
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapGestureDetectorTest.kt189
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt19
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/LatLngBoundsForCameraActivity.java1
-rwxr-xr-xplatform/android/src/native_map_view.cpp20
-rwxr-xr-xplatform/android/src/native_map_view.hpp6
-rw-r--r--src/mbgl/map/map.cpp6
13 files changed, 475 insertions, 26 deletions
diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp
index fec67eb281..7c6274caf2 100644
--- a/include/mbgl/map/map.hpp
+++ b/include/mbgl/map/map.hpp
@@ -141,6 +141,7 @@ public:
// Projection
ScreenCoordinate pixelForLatLng(const LatLng&) const;
+ ScreenCoordinate pixelForLatLngRaw(const LatLng&) const;
LatLng latLngForPixel(const ScreenCoordinate&) const;
// Annotations
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java
index cf2d20179f..2b6c1fdd01 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java
@@ -8,6 +8,8 @@ import android.graphics.PointF;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.Pair;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.animation.DecelerateInterpolator;
@@ -26,6 +28,7 @@ import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.constants.MapboxConstants;
import com.mapbox.mapboxsdk.constants.TelemetryConstants;
import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.utils.MathUtils;
import java.util.ArrayList;
@@ -34,6 +37,7 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
+import static com.mapbox.mapboxsdk.constants.MapboxConstants.ANIMATION_DURATION_FLING_BASE;
import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_ANIMATION;
import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE;
@@ -42,6 +46,9 @@ import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.RE
*/
final class MapGestureDetector {
+ @VisibleForTesting
+ static final int BOUND_REPEL_RATIO = 1000;
+
private final Transform transform;
private final Projection projection;
private final UiSettings uiSettings;
@@ -108,6 +115,18 @@ final class MapGestureDetector {
}
}
+ @VisibleForTesting
+ MapGestureDetector(Transform transform, Projection projection, UiSettings uiSettings,
+ AnnotationManager annotationManager, CameraChangeDispatcher cameraChangeDispatcher,
+ AndroidGesturesManager androidGesturesManager) {
+ this.annotationManager = annotationManager;
+ this.transform = transform;
+ this.projection = projection;
+ this.uiSettings = uiSettings;
+ this.cameraChangeDispatcher = cameraChangeDispatcher;
+ this.gesturesManager = androidGesturesManager;
+ }
+
private void initializeGestureListeners(@NonNull Context context, boolean attachDefaultListeners) {
if (attachDefaultListeners) {
StandardGestureListener standardGestureListener = new StandardGestureListener();
@@ -412,11 +431,42 @@ final class MapGestureDetector {
// tilt results in a bigger translation, limiting input for #5281
double tilt = transform.getTilt();
double tiltFactor = 1.5 + ((tilt != 0) ? (tilt / 10) : 0);
- double offsetX = velocityX / tiltFactor / screenDensity;
- double offsetY = velocityY / tiltFactor / screenDensity;
+ float offsetX = (float) (velocityX / tiltFactor / screenDensity);
+ float offsetY = (float) (velocityY / tiltFactor / screenDensity);
+
+ double animationTimeDivider = 1;
+
+ LatLngBounds bounds = transform.getLatLngBounds();
+ if (bounds != null) {
+ // camera movement is limited by bounds, calculate the acceptable offset that doesn't cross the bounds
+ Pair<Float, Float> offset = findBoundsLimitedOffset(projection, transform, bounds, -offsetX, -offsetY);
+ if (offset != null) {
+ float targetOffsetX = -offset.first;
+ float targetOffsetY = -offset.second;
+
+ // check how much did we cut from the original offset and shorten animation time by the same ratio
+ float offsetDiffRatioX = 0f;
+ if (targetOffsetX != 0) {
+ offsetDiffRatioX = offsetX / targetOffsetX;
+ }
+ float offsetDiffRatioY = 0f;
+ if (targetOffsetY != 0) {
+ offsetDiffRatioY = offsetY / targetOffsetY;
+ }
+
+ // pick the highest ratio as a divider
+ animationTimeDivider = offsetDiffRatioX > offsetDiffRatioY ? offsetDiffRatioX : offsetDiffRatioY;
+ // default to 1 if necessary
+ animationTimeDivider = animationTimeDivider > 1 ? animationTimeDivider : 1;
+
+ offsetX = targetOffsetX;
+ offsetY = targetOffsetY;
+ }
+ }
- // calculate animation time based on displacement
- long animationTime = (long) (velocityXY / 7 / tiltFactor + MapboxConstants.ANIMATION_DURATION_FLING_BASE);
+ // calculate animation time based on displacement and make it shorter if we've hit the bound
+ long animationTime =
+ (long) ((velocityXY / 7 / tiltFactor + ANIMATION_DURATION_FLING_BASE) / animationTimeDivider);
// update transformation
transform.moveBy(offsetX, offsetY, animationTime);
@@ -445,6 +495,16 @@ final class MapGestureDetector {
// dispatching camera start event only when the movement actually occurred
cameraChangeDispatcher.onCameraMoveStarted(CameraChangeDispatcher.REASON_API_GESTURE);
+ LatLngBounds bounds = transform.getLatLngBounds();
+ if (bounds != null) {
+ // camera movement is limited by bounds, calculate the acceptable offset that doesn't cross the bounds
+ Pair<Float, Float> offset = findBoundsLimitedOffset(projection, transform, bounds, distanceX, distanceY);
+ if (offset != null) {
+ distanceX = offset.first;
+ distanceY = offset.second;
+ }
+ }
+
// Scroll the map
transform.moveBy(-distanceX, -distanceY, 0 /*no duration*/);
@@ -914,6 +974,63 @@ final class MapGestureDetector {
return mapZoom >= MapboxConstants.MINIMUM_ZOOM && mapZoom <= MapboxConstants.MAXIMUM_ZOOM;
}
+ /**
+ * Finds acceptable camera offset based on the bounds that are limiting the camera movement.
+ * <p>
+ * To find the offset, we're hit testing all bounding box edges against the segment that is created from the current
+ * screen center and a target calculated from the provided offset.
+ * Because we are converting LatLng to unwrapped screen coordinates, we are going to find only one collision, or none,
+ * and return an acceptable offset based on the distance from the original point to bounds intersection,
+ * or the original target.
+ */
+ @Nullable
+ @VisibleForTesting
+ static Pair<Float, Float> findBoundsLimitedOffset(@NonNull Projection projection,
+ @NonNull Transform transform,
+ @NonNull LatLngBounds bounds,
+ float offsetX, float offsetY) {
+ // because core returns number with big precision, LatLngBounds#contains can fail when using raw values
+ LatLng coreScreenCenter = transform.getCenterCoordinate(false);
+ LatLng screenCenter = new LatLng(
+ MathUtils.round(coreScreenCenter.getLatitude(), 6),
+ MathUtils.round(coreScreenCenter.getLongitude(), 6));
+ if (!bounds.contains(screenCenter) || bounds.equals(LatLngBounds.world())) {
+ // return when screen center is not within bounds or the camera is not limited
+ return null;
+ }
+
+ PointF referencePixel = projection.toScreenLocationRaw(screenCenter);
+ PointF nwPixel = projection.toScreenLocationRaw(bounds.getNorthWest());
+ PointF nePixel = projection.toScreenLocationRaw(bounds.getNorthEast());
+ PointF sePixel = projection.toScreenLocationRaw(bounds.getSouthEast());
+ PointF swPixel = projection.toScreenLocationRaw(bounds.getSouthWest());
+
+ Pair[] edges = new Pair[] {
+ new Pair<>(nwPixel, nePixel), // north edge
+ new Pair<>(nePixel, sePixel), // east edge
+ new Pair<>(sePixel, swPixel), // south edge
+ new Pair<>(swPixel, nwPixel) // west edge
+ };
+
+ // to prevent the screen center from being permanently attracted to the bound we need to push the origin
+ // so that it isn't a part of the bound's edge segment, otherwise, we'd have a constant collision
+ PointF boundsCenter = projection.toScreenLocationRaw(bounds.getCenter());
+ PointF toCenterVector = new PointF(boundsCenter.x - referencePixel.x, boundsCenter.y - referencePixel.y);
+ referencePixel.offset(toCenterVector.x / BOUND_REPEL_RATIO, toCenterVector.y / BOUND_REPEL_RATIO);
+ PointF target = new PointF(referencePixel.x + offsetX, referencePixel.y + offsetY);
+
+ for (Pair edge : edges) {
+ PointF intersection =
+ MathUtils.findSegmentsIntersection(referencePixel, target, (PointF) edge.first, (PointF) edge.second);
+ if (intersection != null) {
+ // returning a limited offset vector
+ return new Pair<>(intersection.x - referencePixel.x, intersection.y - referencePixel.y);
+ }
+ }
+ // no collisions, returning the original offset vector
+ return new Pair<>(target.x - referencePixel.x, target.y - referencePixel.y);
+ }
+
void notifyOnMapClickListeners(@NonNull PointF tapPoint) {
for (MapboxMap.OnMapClickListener listener : onMapClickListenerList) {
if (listener.onMapClick(projection.fromScreenLocation(tapPoint))) {
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMap.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMap.java
index cf5961a313..e9b6e5b5ef 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMap.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMap.java
@@ -6,6 +6,7 @@ import android.graphics.RectF;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.Geometry;
import com.mapbox.mapboxsdk.annotations.Marker;
@@ -63,9 +64,12 @@ interface NativeMap {
void setLatLng(@NonNull LatLng latLng, long duration);
- LatLng getLatLng();
+ LatLng getLatLng(boolean padded);
+
+ void setLatLngBounds(@Nullable LatLngBounds latLngBounds);
- void setLatLngBounds(@NonNull LatLngBounds latLngBounds);
+ @NonNull
+ LatLngBounds getLatLngBounds();
void setVisibleCoordinateBounds(@NonNull LatLng[] coordinates, @NonNull RectF padding,
double direction, long duration);
@@ -194,6 +198,9 @@ interface NativeMap {
@NonNull
PointF pixelForLatLng(@NonNull LatLng latLng);
+ @NonNull
+ PointF pixelForLatLngRaw(@NonNull LatLng latLng);
+
LatLng latLngForPixel(@NonNull PointF pixel);
//
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java
index 10942d521c..ed06dc605c 100755
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java
@@ -10,6 +10,7 @@ import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
+
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.Geometry;
import com.mapbox.mapboxsdk.LibraryLoader;
@@ -214,13 +215,22 @@ final class NativeMapView implements NativeMap {
}
@Override
- public void setLatLngBounds(LatLngBounds latLngBounds) {
+ public void setLatLngBounds(@Nullable LatLngBounds latLngBounds) {
if (checkState("setLatLngBounds")) {
return;
}
nativeSetLatLngBounds(latLngBounds);
}
+ @NonNull
+ @Override
+ public LatLngBounds getLatLngBounds() {
+ if (checkState("getLatLngBounds")) {
+ return LatLngBounds.world();
+ }
+ return nativeGetLatLngBounds();
+ }
+
@Override
public void cancelTransitions() {
if (checkState("cancelTransitions")) {
@@ -254,11 +264,11 @@ final class NativeMapView implements NativeMap {
}
@Override
- public LatLng getLatLng() {
+ public LatLng getLatLng(boolean padded) {
if (checkState("")) {
return new LatLng();
}
- return nativeGetLatLng();
+ return nativeGetLatLng(padded);
}
@Override
@@ -665,6 +675,17 @@ final class NativeMapView implements NativeMap {
}
@Override
+ @NonNull
+ public PointF pixelForLatLngRaw(@NonNull LatLng latLng) {
+ if (checkState("pixelForLatLngRaw")) {
+ return new PointF();
+ }
+ PointF pointF = nativePixelForLatLngRaw(latLng.getLatitude(), latLng.getLongitude());
+ pointF.set(pointF.x * pixelRatio, pointF.y * pixelRatio);
+ return pointF;
+ }
+
+ @Override
public LatLng latLngForPixel(@NonNull PointF pixel) {
if (checkState("latLngForPixel")) {
return new LatLng();
@@ -1095,6 +1116,9 @@ final class NativeMapView implements NativeMap {
private native void nativeSetLatLngBounds(LatLngBounds latLngBounds);
@Keep
+ private native LatLngBounds nativeGetLatLngBounds();
+
+ @Keep
private native void nativeCancelTransitions();
@Keep
@@ -1108,7 +1132,7 @@ final class NativeMapView implements NativeMap {
@NonNull
@Keep
- private native LatLng nativeGetLatLng();
+ private native LatLng nativeGetLatLng(boolean padded);
@NonNull
@Keep
@@ -1242,6 +1266,10 @@ final class NativeMapView implements NativeMap {
@NonNull
@Keep
+ private native PointF nativePixelForLatLngRaw(double lat, double lon);
+
+ @NonNull
+ @Keep
private native LatLng nativeLatLngForPixel(float x, float y);
@Keep
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java
index cbf5426012..dbeabe2be6 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java
@@ -3,6 +3,7 @@ package com.mapbox.mapboxsdk.maps;
import android.graphics.PointF;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
+
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.constants.GeometryConstants;
import com.mapbox.mapboxsdk.geometry.LatLng;
@@ -257,6 +258,18 @@ public class Projection {
return nativeMapView.pixelForLatLng(location);
}
+ /**
+ * Similarly to {@link #toScreenLocation(LatLng)} this method returns a screen location of a coordinate, however,
+ * the values are always relative to the screens center and do not unwrap when crossing the antimeridian.
+ *
+ * @param location A LatLng on the map to convert to a screen location.
+ * @return A Point representing the screen location in screen pixels, always relative to screen's center.
+ */
+ @NonNull
+ PointF toScreenLocationRaw(@NonNull LatLng location) {
+ return nativeMapView.pixelForLatLngRaw(location);
+ }
+
float getHeight() {
return nativeMapView.getHeight();
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java
index c40994d7ca..2d2302912d 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java
@@ -11,6 +11,7 @@ import com.mapbox.mapboxsdk.camera.CameraUpdate;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.constants.MapboxConstants;
import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.log.Logger;
import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener;
@@ -248,13 +249,9 @@ final class Transform implements MapView.OnCameraDidChangeListener {
nativeMap.setBearing(bearing, focalX, focalY, duration);
}
-
- //
- // LatLng / CenterCoordinate
- //
-
- LatLng getLatLng() {
- return nativeMap.getLatLng();
+ @Nullable
+ LatLngBounds getLatLngBounds() {
+ return nativeMap.getLatLngBounds();
}
//
@@ -273,8 +270,8 @@ final class Transform implements MapView.OnCameraDidChangeListener {
// Center coordinate
//
- LatLng getCenterCoordinate() {
- return nativeMap.getLatLng();
+ LatLng getCenterCoordinate(boolean padded) {
+ return nativeMap.getLatLng(padded);
}
void setCenterCoordinate(LatLng centerCoordinate) {
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/MathUtils.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/MathUtils.java
index 0c90e4b244..fbcf9afb28 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/MathUtils.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/MathUtils.java
@@ -1,5 +1,8 @@
package com.mapbox.mapboxsdk.utils;
+import android.graphics.PointF;
+import android.support.annotation.Nullable;
+
// TODO Remove this class if we finally include it within MAS 3.x (GeoJSON)
public class MathUtils {
@@ -46,4 +49,60 @@ public class MathUtils {
return secondMod + min;
}
+
+ /**
+ * Look for the intersection point of two segments, if exist.
+ *
+ * @param point0 first point of the first segment
+ * @param point1 second point of the first segment
+ * @param point2 first point of the second segment
+ * @param point3 second point of the second segment
+ * @return intersection point, if exists
+ */
+ @Nullable
+ public static PointF findSegmentsIntersection(PointF point0, PointF point1, PointF point2, PointF point3) {
+ // Java port of one of the algorithms discussed in https://stackoverflow.com/questions/563198/
+ float dx1 = point1.x - point0.x;
+ float dy1 = point1.y - point0.y;
+ float dx2 = point3.x - point2.x;
+ float dy2 = point3.y - point2.y;
+ float dx3 = point0.x - point2.x;
+ float dy3 = point0.y - point2.y;
+
+ float d = dx1 * dy2 - dx2 * dy1;
+
+ if (d != 0) {
+ float s = dx1 * dy3 - dx3 * dy1;
+ if ((s <= 0 && d < 0 && s >= d) || (s >= 0 && d > 0 && s <= d)) {
+ float t = dx2 * dy3 - dx3 * dy2;
+ if ((t <= 0 && d < 0 && t > d) || (t >= 0 && d > 0 && t < d)) {
+ t = t / d;
+ return new PointF(point0.x + t * dx1, point0.y + t * dy1);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a distance between two points.
+ *
+ * @param point0 first point
+ * @param point1 second point
+ * @return distance in pixels
+ */
+ public static float distance(PointF point0, PointF point1) {
+ return (float) Math.sqrt(Math.pow(point1.x - point0.x, 2) + Math.pow(point1.y - point0.y, 2));
+ }
+
+ /**
+ * Rounds a number.
+ * @param value original value
+ * @param precision expected precision
+ * @return rounded number
+ */
+ public static double round(double value, int precision) {
+ int scale = (int) Math.pow(10, precision);
+ return (double) Math.round(value * scale) / scale;
+ }
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapGestureDetectorTest.kt b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapGestureDetectorTest.kt
new file mode 100644
index 0000000000..eb4e8c00e5
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapGestureDetectorTest.kt
@@ -0,0 +1,189 @@
+package com.mapbox.mapboxsdk.maps
+
+import android.graphics.PointF
+import android.util.Pair
+import com.mapbox.mapboxsdk.geometry.LatLng
+import com.mapbox.mapboxsdk.geometry.LatLngBounds
+import com.mapbox.mapboxsdk.maps.MapGestureDetector.BOUND_REPEL_RATIO
+import com.mapbox.mapboxsdk.utils.MathUtils
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class MapGestureDetectorTest {
+
+ companion object {
+ private val BOUNDS = LatLngBounds.Builder()
+ .include(LatLng(10.0, -10.0))
+ .include(LatLng(-10.0, 10.0))
+ .build()
+ }
+
+ private lateinit var mapGestureDetector: MapGestureDetector
+ private lateinit var transform: Transform
+ private lateinit var projection: Projection
+ private lateinit var uiSettings: UiSettings
+
+ @Before
+ fun setup() {
+ transform = mockk(relaxed = true)
+ projection = mockk(relaxed = true)
+ uiSettings = mockk(relaxed = true)
+ mapGestureDetector = MapGestureDetector(transform, projection, uiSettings, mockk(), mockk(), mockk())
+ }
+
+ @Test
+ fun limitedOffset_returnWhenCenterOutside() {
+ val outsideCoordinate = LatLng(25.0, 0.0)
+ every { transform.getCenterCoordinate(false) } answers { outsideCoordinate }
+ val limitedOffset =
+ MapGestureDetector.findBoundsLimitedOffset(projection, transform, BOUNDS, 0f, 0f)
+ assertNull(limitedOffset)
+ }
+
+ @Test
+ fun limitedOffset_returnWhenWholeWorld() {
+ val coordinate = LatLng(25.0, 0.0)
+ every { transform.getCenterCoordinate(false) } answers { coordinate }
+ val limitedOffset =
+ MapGestureDetector.findBoundsLimitedOffset(projection, transform, LatLngBounds.world(), 0f, 0f)
+ assertNull(limitedOffset)
+ }
+
+ @Test
+ fun limitedOffset_referencePixelRepelledOfBound() {
+ val coordinate = LatLng(10.0, 0.0)
+ every { transform.getCenterCoordinate(false) } returns coordinate
+ val referencePixel = PointF(100f, 200f)
+ every { projection.toScreenLocationRaw(coordinate) } returns referencePixel
+ val boundsCenterPoint = PointF(-100f, 300f)
+ every { projection.toScreenLocationRaw(BOUNDS.center) } answers { boundsCenterPoint }
+
+ val expected = PointF(
+ referencePixel.x + (boundsCenterPoint.x - referencePixel.x) / BOUND_REPEL_RATIO,
+ referencePixel.y + (boundsCenterPoint.y - referencePixel.y) / BOUND_REPEL_RATIO)
+ MapGestureDetector.findBoundsLimitedOffset(projection, transform, BOUNDS, 0f, 0f)
+ assertEquals(expected.x, referencePixel.x)
+ assertEquals(expected.y, referencePixel.y)
+ }
+
+ @Test
+ fun limitedOffset_noIntersection() {
+ val coordinate = LatLng(1.0, -1.0)
+ every { transform.getCenterCoordinate(false) } returns coordinate
+ val referencePixel = PointF(50f, 50f)
+ every { projection.toScreenLocationRaw(coordinate) } returns referencePixel
+ val boundsCenterPoint = PointF(100f, 100f)
+ every { projection.toScreenLocationRaw(BOUNDS.center) } returns boundsCenterPoint
+ every { projection.toScreenLocationRaw(BOUNDS.northWest) } returns PointF(0f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.northEast) } returns PointF(200f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.southEast) } returns PointF(200f, 200f)
+ every { projection.toScreenLocationRaw(BOUNDS.southWest) } returns PointF(0f, 200f)
+ val acceptableVector =
+ MapGestureDetector.findBoundsLimitedOffset(projection, transform, BOUNDS, 25f, 30f)
+ val target = PointF(referencePixel.x + 25f, referencePixel.y + 30f)
+ assertEquals(Pair(target.x - referencePixel.x, target.y - referencePixel.y), acceptableVector)
+ }
+
+ @Test
+ fun limitedOffset_northIntersection() {
+ val coordinate = LatLng(1.0, -1.0)
+ every { transform.getCenterCoordinate(false) } returns coordinate
+ val referencePixel = PointF(50f, 50f)
+ every { projection.toScreenLocationRaw(coordinate) } returns referencePixel
+ val boundsCenterPoint = PointF(100f, 100f)
+ every { projection.toScreenLocationRaw(BOUNDS.center) } returns boundsCenterPoint
+ every { projection.toScreenLocationRaw(BOUNDS.northWest) } returns PointF(0f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.northEast) } returns PointF(200f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.southEast) } returns PointF(200f, 200f)
+ every { projection.toScreenLocationRaw(BOUNDS.southWest) } returns PointF(0f, 200f)
+ val acceptableVector =
+ MapGestureDetector.findBoundsLimitedOffset(projection, transform, BOUNDS, 25f, -200f)
+ val target = PointF(referencePixel.x + 25f, referencePixel.y - 200f)
+ val closestPoint = MathUtils.findSegmentsIntersection(
+ referencePixel,
+ target,
+ projection.toScreenLocationRaw(BOUNDS.northWest),
+ projection.toScreenLocationRaw(BOUNDS.northEast)
+ )
+ assertEquals(Pair(closestPoint!!.x - referencePixel.x, closestPoint.y - referencePixel.y), acceptableVector)
+ }
+
+ @Test
+ fun limitedOffset_eastIntersection() {
+ val coordinate = LatLng(1.0, -1.0)
+ every { transform.getCenterCoordinate(false) } returns coordinate
+ val referencePixel = PointF(50f, 50f)
+ every { projection.toScreenLocationRaw(coordinate) } returns referencePixel
+ val boundsCenterPoint = PointF(100f, 100f)
+ every { projection.toScreenLocationRaw(BOUNDS.center) } returns boundsCenterPoint
+ every { projection.toScreenLocationRaw(BOUNDS.northWest) } returns PointF(0f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.northEast) } returns PointF(200f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.southEast) } returns PointF(200f, 200f)
+ every { projection.toScreenLocationRaw(BOUNDS.southWest) } returns PointF(0f, 200f)
+ val acceptableVector =
+ MapGestureDetector.findBoundsLimitedOffset(projection, transform, BOUNDS, 200f, 25f)
+ val target = PointF(referencePixel.x + 200f, referencePixel.y + 25f)
+ val closestPoint = MathUtils.findSegmentsIntersection(
+ referencePixel,
+ target,
+ projection.toScreenLocationRaw(BOUNDS.northEast),
+ projection.toScreenLocationRaw(BOUNDS.southEast)
+ )
+ assertEquals(Pair(closestPoint!!.x - referencePixel.x, closestPoint.y - referencePixel.y), acceptableVector)
+ }
+
+ @Test
+ fun limitedOffset_southIntersection() {
+ val coordinate = LatLng(1.0, -1.0)
+ every { transform.getCenterCoordinate(false) } returns coordinate
+ val referencePixel = PointF(50f, 50f)
+ every { projection.toScreenLocationRaw(coordinate) } returns referencePixel
+ val boundsCenterPoint = PointF(100f, 100f)
+ every { projection.toScreenLocationRaw(BOUNDS.center) } returns boundsCenterPoint
+ every { projection.toScreenLocationRaw(BOUNDS.northWest) } returns PointF(0f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.northEast) } returns PointF(200f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.southEast) } returns PointF(200f, 200f)
+ every { projection.toScreenLocationRaw(BOUNDS.southWest) } returns PointF(0f, 200f)
+ val acceptableVector =
+ MapGestureDetector.findBoundsLimitedOffset(projection, transform, BOUNDS, 25f, 200f)
+ val target = PointF(referencePixel.x + 25f, referencePixel.y + 200f)
+ val closestPoint = MathUtils.findSegmentsIntersection(
+ referencePixel,
+ target,
+ projection.toScreenLocationRaw(BOUNDS.southEast),
+ projection.toScreenLocationRaw(BOUNDS.southWest)
+ )
+ assertEquals(Pair(closestPoint!!.x - referencePixel.x, closestPoint.y - referencePixel.y), acceptableVector)
+ }
+
+ @Test
+ fun limitedOffset_westIntersection() {
+ val coordinate = LatLng(1.0, -1.0)
+ every { transform.getCenterCoordinate(false) } returns coordinate
+ val referencePixel = PointF(50f, 50f)
+ every { projection.toScreenLocationRaw(coordinate) } returns referencePixel
+ val boundsCenterPoint = PointF(100f, 100f)
+ every { projection.toScreenLocationRaw(BOUNDS.center) } returns boundsCenterPoint
+ every { projection.toScreenLocationRaw(BOUNDS.northWest) } returns PointF(0f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.northEast) } returns PointF(200f, 0f)
+ every { projection.toScreenLocationRaw(BOUNDS.southEast) } returns PointF(200f, 200f)
+ every { projection.toScreenLocationRaw(BOUNDS.southWest) } returns PointF(0f, 200f)
+ val acceptableVector =
+ MapGestureDetector.findBoundsLimitedOffset(projection, transform, BOUNDS, -200f, 25f)
+ val target = PointF(referencePixel.x - 200f, referencePixel.y + 25f)
+ val closestPoint = MathUtils.findSegmentsIntersection(
+ referencePixel,
+ target,
+ projection.toScreenLocationRaw(BOUNDS.southWest),
+ projection.toScreenLocationRaw(BOUNDS.northWest)
+ )
+ assertEquals(Pair(closestPoint!!.x - referencePixel.x, closestPoint.y - referencePixel.y), acceptableVector)
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt
index 6958a3519c..f6b08ef5cc 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt
@@ -55,7 +55,21 @@ class NativeMapViewTest {
fun testLatLng() {
val expected = LATLNG_TEST
nativeMapView.setLatLng(expected, 0)
- val actual = nativeMapView.latLng
+ val actual = nativeMapView.getLatLng(true)
+ assertEquals("Latitude should match", expected.latitude, actual.latitude, DELTA)
+ assertEquals("Longitude should match", expected.longitude, actual.longitude, DELTA)
+ }
+
+ @Test
+ @UiThreadTest
+ fun testLatLngPadded() {
+ val expected = LATLNG_TEST
+ nativeMapView.contentPadding = FloatArray(4).also {
+ it[0] = 200f
+ it[1] = 50f
+ }
+ nativeMapView.setLatLng(expected, 0)
+ val actual = nativeMapView.getLatLng(true)
assertEquals("Latitude should match", expected.latitude, actual.latitude, DELTA)
assertEquals("Longitude should match", expected.longitude, actual.longitude, DELTA)
}
@@ -64,12 +78,11 @@ class NativeMapViewTest {
@UiThreadTest
fun testLatLngDefault() {
val expected = LatLng()
- val actual = nativeMapView.latLng
+ val actual = nativeMapView.getLatLng(true)
assertEquals("Latitude should match", expected.latitude, actual.latitude, DELTA)
assertEquals("Longitude should match", expected.longitude, actual.longitude, DELTA)
}
-
@Test
@UiThreadTest
fun testBearingDefault() {
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/LatLngBoundsForCameraActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/LatLngBoundsForCameraActivity.java
index 1a9d3d300b..d5cf6ff9f4 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/LatLngBoundsForCameraActivity.java
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/LatLngBoundsForCameraActivity.java
@@ -56,7 +56,6 @@ public class LatLngBoundsForCameraActivity extends AppCompatActivity implements
this.mapboxMap = mapboxMap;
mapboxMap.setStyle(Style.SATELLITE_STREETS);
mapboxMap.setMinZoomPreference(2);
- mapboxMap.getUiSettings().setFlingVelocityAnimationEnabled(false);
showCrosshair();
setupBounds(ICELAND_BOUNDS);
}
diff --git a/platform/android/src/native_map_view.cpp b/platform/android/src/native_map_view.cpp
index e8bc14c8c2..4a81a33b9b 100755
--- a/platform/android/src/native_map_view.cpp
+++ b/platform/android/src/native_map_view.cpp
@@ -288,6 +288,15 @@ void NativeMapView::setLatLngBounds(jni::JNIEnv& env, const jni::Object<mbgl::an
}
}
+jni::Local<jni::Object<LatLngBounds>> NativeMapView::getLatLngBounds(jni::JNIEnv& env) {
+ optional<mbgl::LatLngBounds> bounds = map->getLatLngBounds();
+ if (bounds) {
+ return LatLngBounds::New(env, bounds.value());
+ } else {
+ return LatLngBounds::New(env, mbgl::LatLngBounds::world());
+ }
+}
+
void NativeMapView::cancelTransitions(jni::JNIEnv&) {
map->cancelTransitions();
}
@@ -365,8 +374,8 @@ void NativeMapView::flyTo(jni::JNIEnv&, jni::jdouble angle, jni::jdouble latitud
map->flyTo(cameraOptions, animationOptions);
}
-jni::Local<jni::Object<LatLng>> NativeMapView::getLatLng(JNIEnv& env) {
- return LatLng::New(env, map->getLatLng(insets));
+jni::Local<jni::Object<LatLng>> NativeMapView::getLatLng(JNIEnv& env, jni::jboolean padded) {
+ return LatLng::New(env, map->getLatLng(padded ? insets : EdgeInsets{}));
}
void NativeMapView::setLatLng(jni::JNIEnv&, jni::jdouble latitude, jni::jdouble longitude, jni::jlong duration) {
@@ -588,6 +597,11 @@ jni::Local<jni::Object<PointF>> NativeMapView::pixelForLatLng(JNIEnv& env, jdoub
return PointF::New(env, static_cast<float>(pixel.x), static_cast<float>(pixel.y));
}
+jni::Local<jni::Object<PointF>> NativeMapView::pixelForLatLngRaw(JNIEnv& env, jdouble latitude, jdouble longitude) {
+ mbgl::ScreenCoordinate pixel = map->pixelForLatLngRaw(mbgl::LatLng(latitude, longitude));
+ return PointF::New(env, static_cast<float>(pixel.x), static_cast<float>(pixel.y));
+}
+
jni::Local<jni::Object<LatLng>> NativeMapView::latLngForPixel(JNIEnv& env, jfloat x, jfloat y) {
return LatLng::New(env, map->latLngForPixel(mbgl::ScreenCoordinate(x, y)));
}
@@ -1076,6 +1090,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::getMetersPerPixelAtLatitude, "nativeGetMetersPerPixelAtLatitude"),
METHOD(&NativeMapView::projectedMetersForLatLng, "nativeProjectedMetersForLatLng"),
METHOD(&NativeMapView::pixelForLatLng, "nativePixelForLatLng"),
+ METHOD(&NativeMapView::pixelForLatLngRaw, "nativePixelForLatLngRaw"),
METHOD(&NativeMapView::latLngForProjectedMeters, "nativeLatLngForProjectedMeters"),
METHOD(&NativeMapView::latLngForPixel, "nativeLatLngForPixel"),
METHOD(&NativeMapView::addPolylines, "nativeAddPolylines"),
@@ -1109,6 +1124,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::removeImage, "nativeRemoveImage"),
METHOD(&NativeMapView::getImage, "nativeGetImage"),
METHOD(&NativeMapView::setLatLngBounds, "nativeSetLatLngBounds"),
+ METHOD(&NativeMapView::getLatLngBounds, "nativeGetLatLngBounds"),
METHOD(&NativeMapView::setPrefetchTiles, "nativeSetPrefetchTiles"),
METHOD(&NativeMapView::getPrefetchTiles, "nativeGetPrefetchTiles")
);
diff --git a/platform/android/src/native_map_view.hpp b/platform/android/src/native_map_view.hpp
index 1bb4f23cbe..f7aa076bf6 100755
--- a/platform/android/src/native_map_view.hpp
+++ b/platform/android/src/native_map_view.hpp
@@ -85,6 +85,8 @@ public:
void setLatLngBounds(jni::JNIEnv&, const jni::Object<mbgl::android::LatLngBounds>&);
+ jni::Local<jni::Object<LatLngBounds>> getLatLngBounds(jni::JNIEnv&);
+
void cancelTransitions(jni::JNIEnv&);
void setGestureInProgress(jni::JNIEnv&, jni::jboolean);
@@ -97,7 +99,7 @@ public:
void flyTo(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jdouble, jni::jlong, jni::jdouble, jni::jdouble);
- jni::Local<jni::Object<LatLng>> getLatLng(JNIEnv&);
+ jni::Local<jni::Object<LatLng>> getLatLng(JNIEnv&, jni::jboolean);
void setLatLng(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jlong);
@@ -167,6 +169,8 @@ public:
jni::Local<jni::Object<PointF>> pixelForLatLng(JNIEnv&, jdouble, jdouble);
+ jni::Local<jni::Object<PointF>> pixelForLatLngRaw(JNIEnv&, jdouble, jdouble);
+
jni::Local<jni::Object<LatLng>> latLngForProjectedMeters(JNIEnv&, jdouble, jdouble);
jni::Local<jni::Object<LatLng>> latLngForPixel(JNIEnv&, jfloat, jfloat);
diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp
index 1b4176db7a..32bc07995d 100644
--- a/src/mbgl/map/map.cpp
+++ b/src/mbgl/map/map.cpp
@@ -634,6 +634,12 @@ ScreenCoordinate Map::pixelForLatLng(const LatLng& latLng) const {
return impl->transform.latLngToScreenCoordinate(unwrappedLatLng);
}
+ScreenCoordinate Map::pixelForLatLngRaw(const LatLng& latLng) const {
+ // This method, in comparison to the Map::pixelForLatLng, will not unwrap point's longitude
+ // and always return values relative to the center of the screen
+ return impl->transform.latLngToScreenCoordinate(latLng);
+}
+
LatLng Map::latLngForPixel(const ScreenCoordinate& pixel) const {
return impl->transform.screenCoordinateToLatLng(pixel);
}