diff options
Diffstat (limited to 'platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps')
5 files changed, 181 insertions, 19 deletions
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) { |