From f56d56b3c60881a45aaa5a1b1ef1280ef37dd045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Paczos?= Date: Thu, 7 Feb 2019 10:26:25 +0100 Subject: [android] limit the acceptable gestures offset if camera bounds are present --- include/mbgl/map/map.hpp | 1 + .../mapbox/mapboxsdk/maps/MapGestureDetector.java | 125 +++++++++++++- .../java/com/mapbox/mapboxsdk/maps/NativeMap.java | 11 +- .../com/mapbox/mapboxsdk/maps/NativeMapView.java | 36 +++- .../java/com/mapbox/mapboxsdk/maps/Projection.java | 13 ++ .../java/com/mapbox/mapboxsdk/maps/Transform.java | 15 +- .../java/com/mapbox/mapboxsdk/utils/MathUtils.java | 59 +++++++ .../mapboxsdk/maps/MapGestureDetectorTest.kt | 189 +++++++++++++++++++++ .../com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt | 19 ++- .../maplayout/LatLngBoundsForCameraActivity.java | 1 - platform/android/src/native_map_view.cpp | 20 ++- platform/android/src/native_map_view.hpp | 6 +- src/mbgl/map/map.cpp | 6 + 13 files changed, 475 insertions(+), 26 deletions(-) create mode 100644 platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapGestureDetectorTest.kt 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 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 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. + *

+ * 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 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 @@ -664,6 +674,17 @@ final class NativeMapView implements NativeMap { return pointF; } + @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")) { @@ -1094,6 +1115,9 @@ final class NativeMapView implements NativeMap { @Keep private native void nativeSetLatLngBounds(LatLngBounds latLngBounds); + @Keep + private native LatLngBounds nativeGetLatLngBounds(); + @Keep private native void nativeCancelTransitions(); @@ -1108,7 +1132,7 @@ final class NativeMapView implements NativeMap { @NonNull @Keep - private native LatLng nativeGetLatLng(); + private native LatLng nativeGetLatLng(boolean padded); @NonNull @Keep @@ -1240,6 +1264,10 @@ final class NativeMapView implements NativeMap { @Keep private native PointF nativePixelForLatLng(double lat, double lon); + @NonNull + @Keep + private native PointF nativePixelForLatLngRaw(double lat, double lon); + @NonNull @Keep private native LatLng nativeLatLngForPixel(float x, float y); 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> NativeMapView::getLatLngBounds(jni::JNIEnv& env) { + optional 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> NativeMapView::getLatLng(JNIEnv& env) { - return LatLng::New(env, map->getLatLng(insets)); +jni::Local> 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> NativeMapView::pixelForLatLng(JNIEnv& env, jdoub return PointF::New(env, static_cast(pixel.x), static_cast(pixel.y)); } +jni::Local> NativeMapView::pixelForLatLngRaw(JNIEnv& env, jdouble latitude, jdouble longitude) { + mbgl::ScreenCoordinate pixel = map->pixelForLatLngRaw(mbgl::LatLng(latitude, longitude)); + return PointF::New(env, static_cast(pixel.x), static_cast(pixel.y)); +} + jni::Local> 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&); + jni::Local> 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> getLatLng(JNIEnv&); + jni::Local> getLatLng(JNIEnv&, jni::jboolean); void setLatLng(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jlong); @@ -167,6 +169,8 @@ public: jni::Local> pixelForLatLng(JNIEnv&, jdouble, jdouble); + jni::Local> pixelForLatLngRaw(JNIEnv&, jdouble, jdouble); + jni::Local> latLngForProjectedMeters(JNIEnv&, jdouble, jdouble); jni::Local> 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); } -- cgit v1.2.1