diff --git a/android/cpp/jni.cpp b/android/cpp/jni.cpp index 34bef770d..140cc9f3c 100644 --- a/android/cpp/jni.cpp +++ b/android/cpp/jni.cpp @@ -1227,6 +1227,14 @@ jobject JNICALL nativeLatLngForPixel(JNIEnv *env, jobject obj, jlong nativeMapVi return ret; } +jdouble JNICALL nativeGetTopOffsetPixelsForAnnotationSymbol(JNIEnv *env, jobject obj, jlong nativeMapViewPtr, jstring symbolName) { + mbgl::Log::Debug(mbgl::Event::JNI, "nativeGetTopOffsetPixelsForAnnotationSymbol"); + assert(nativeMapViewPtr != 0); + NativeMapView *nativeMapView = reinterpret_cast(nativeMapViewPtr); + return nativeMapView->getMap().getTopOffsetPixelsForAnnotationSymbol(std_string_from_jstring(env, symbolName)); +} + + } extern "C" { @@ -1708,6 +1716,8 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { reinterpret_cast(&nativePixelForLatLng)}, {"nativeLatLngForPixel", "(JLandroid/graphics/PointF;)Lcom/mapbox/mapboxgl/geometry/LatLng;", reinterpret_cast(&nativeLatLngForPixel)}, + {"nativeGetTopOffsetPixelsForAnnotationSymbol", "(JLjava/lang/String;)D", + reinterpret_cast(&nativeGetTopOffsetPixelsForAnnotationSymbol)}, }; if (env->RegisterNatives(nativeMapViewClass, methods.data(), methods.size()) < 0) { diff --git a/android/java/MapboxGLAndroidSDK/build.gradle b/android/java/MapboxGLAndroidSDK/build.gradle index ea0be4fa1..00ea98820 100644 --- a/android/java/MapboxGLAndroidSDK/build.gradle +++ b/android/java/MapboxGLAndroidSDK/build.gradle @@ -28,9 +28,9 @@ repositories { } dependencies { - compile 'com.android.support:support-annotations:23.0.0' - compile 'com.android.support:support-v4:23.0.0' - compile 'com.android.support:design:23.0.0' + compile 'com.android.support:support-annotations:23.0.1' + compile 'com.android.support:support-v4:23.0.1' + compile 'com.android.support:design:23.0.1' compile 'com.squareup.okhttp:okhttp:2.4.0' compile 'com.mapzen.android:lost:1.0.1' } diff --git a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/Annotation.java b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/Annotation.java index 4f172c6e2..9cddf9984 100644 --- a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/Annotation.java +++ b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/Annotation.java @@ -2,8 +2,7 @@ package com.mapbox.mapboxgl.annotations; import com.mapbox.mapboxgl.views.MapView; - -public abstract class Annotation { +public abstract class Annotation implements Comparable { /** * The annotation id @@ -11,7 +10,7 @@ public abstract class Annotation { * Internal C++ id is stored as unsigned int. */ protected long id = -1; // -1 unless added to a MapView - private MapView mapView; + protected MapView mapView; float alpha = 1.0f; boolean visible = true; @@ -60,4 +59,35 @@ public abstract class Annotation { // public void setZIndex(float zIndex) { // // } + + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + if (o instanceof Annotation) { + Annotation comp = (Annotation) o; + return id == comp.id; + } + return false; + } + + @Override + public int compareTo(Annotation annotation) { + + if (annotation == null) { + return -1; + } + + if (id < annotation.getId()) { + return 1; + } else if (id > annotation.getId()) { + return -1; + } + + // Equal + return 0; + } } diff --git a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/InfoWindow.java b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/InfoWindow.java new file mode 100644 index 000000000..56eae1951 --- /dev/null +++ b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/InfoWindow.java @@ -0,0 +1,199 @@ +package com.mapbox.mapboxgl.annotations; + +import android.content.Context; +import android.graphics.PointF; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import com.mapbox.mapboxgl.geometry.LatLng; +import com.mapbox.mapboxgl.views.MapView; + +/** + * A tooltip view + */ +public class InfoWindow { + + private Marker boundMarker; + private MapView mMapView; + private boolean mIsVisible; + protected View mView; + + static int mTitleId = 0; + static int mDescriptionId = 0; + static int mSubDescriptionId = 0; + static int mImageId = 0; + + public InfoWindow(int layoutResId, MapView mapView) { + mMapView = mapView; + mIsVisible = false; + Context context = mapView.getContext(); + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mView = inflater.inflate(layoutResId, (ViewGroup) mapView, false); + + if (mTitleId == 0) { + setResIds(mapView.getContext()); + } + + // default behavior: close it when clicking on the tooltip: + setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_UP) { + close(); + } + return true; + } + }); + } + + public InfoWindow(View view, MapView mapView) { + mMapView = mapView; + mIsVisible = false; + ViewGroup parent = (ViewGroup) mapView.getParent(); + Context context = mapView.getContext(); + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mView = view; + + // default behavior: close it when clicking on the tooltip: + setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_UP) { + close(); + } + return true; + } + }); + } + + /** + * open the window at the specified position. + * + * @param object the graphical object on which is hooked the view + * @param position to place the window on the map + * @param offsetX (&offsetY) the offset of the view to the position, in pixels. + * This allows to offset the view from the object position. + * @return this infowindow + */ + public InfoWindow open(Marker object, LatLng position, int offsetX, int offsetY) { + onOpen(object); + MapView.LayoutParams lp = new MapView.LayoutParams(MapView.LayoutParams.WRAP_CONTENT, MapView.LayoutParams.WRAP_CONTENT); + mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + + PointF coords = mMapView.toScreenLocation(position); + double y = mMapView.getTopOffsetPixelsForAnnotationSymbol(object.sprite); + y = y * mMapView.getScreenDensity(); + + // Flip y coordinate as Android view origin is upper left corner + coords.y = mMapView.getHeight() - coords.y; + lp.leftMargin = (int) coords.x - (mView.getMeasuredWidth() / 2); + // Add y because it's a negative value + lp.topMargin = (int) coords.y - mView.getMeasuredHeight() + (int) y; + + close(); //if it was already opened + mMapView.addView(mView, lp); + mIsVisible = true; + return this; + } + + /** + * Close this InfoWindow if it is visible, otherwise don't do anything. + * + * @return this info window + */ + public InfoWindow close() { + if (mIsVisible) { + mIsVisible = false; + ((ViewGroup) mView.getParent()).removeView(mView); +// this.boundMarker.blur(); + setBoundMarker(null); + onClose(); + } + return this; + } + + /** + * Returns the Android view. This allows to set its content. + * + * @return the Android view + */ + public View getView() { + return mView; + } + + /** + * Returns the mapView this InfoWindow is bound to + * + * @return the mapView + */ + public MapView getMapView() { + return mMapView; + } + + /** + * Constructs the view that is displayed when the InfoWindow opens. + * This retrieves data from overlayItem and shows it in the tooltip. + * + * @param overlayItem the tapped overlay item + */ + public void onOpen(Marker overlayItem) { + String title = overlayItem.getTitle(); + ((TextView) mView.findViewById(mTitleId /*R.id.title*/)).setText(title); + String snippet = overlayItem.getSnippet(); + ((TextView) mView.findViewById(mDescriptionId /*R.id.description*/)).setText(snippet); + +/* + //handle sub-description, hiding or showing the text view: + TextView subDescText = (TextView) mView.findViewById(mSubDescriptionId); + String subDesc = overlayItem.getSubDescription(); + if ("".equals(subDesc)) { + subDescText.setVisibility(View.GONE); + } else { + subDescText.setText(subDesc); + subDescText.setVisibility(View.VISIBLE); + } +*/ + } + + public void onClose() { + //by default, do nothing + } + + public InfoWindow setBoundMarker(Marker aBoundMarker) { + this.boundMarker = aBoundMarker; + return this; + } + + public Marker getBoundMarker() { + return boundMarker; + } + + /** + * Given a context, set the resource ids for the layout + * of the InfoWindow. + * + * @param context + */ + private static void setResIds(Context context) { + String packageName = context.getPackageName(); //get application package name + mTitleId = context.getResources().getIdentifier("id/infowindow_title", null, packageName); + mDescriptionId = + context.getResources().getIdentifier("id/infowindow_description", null, packageName); + mSubDescriptionId = context.getResources() + .getIdentifier("id/infowindow_subdescription", null, packageName); + mImageId = context.getResources().getIdentifier("id/infowindow_image", null, packageName); + } + + /** + * Use to override default touch events handling on InfoWindow (ie, close automatically) + * + * @param listener New View.OnTouchListener to use + */ + public void setOnTouchListener(View.OnTouchListener listener) { + mView.setOnTouchListener(listener); + } +} diff --git a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/InfoWindowTipView.java b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/InfoWindowTipView.java new file mode 100644 index 000000000..657e63a84 --- /dev/null +++ b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/InfoWindowTipView.java @@ -0,0 +1,40 @@ +package com.mapbox.mapboxgl.annotations; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.AttributeSet; +import android.view.View; + +public class InfoWindowTipView extends View { + + private Paint mPaint; + private Path mPath; + + public InfoWindowTipView(Context context, AttributeSet attrs) { + super(context, attrs); + + mPath = new Path(); + + this.mPaint = new Paint(); + this.mPaint.setColor(Color.WHITE); + this.mPaint.setAntiAlias(true); + this.mPaint.setStrokeWidth(0.0f); + this.mPaint.setStyle(Paint.Style.FILL); + } + + @Override + protected void onDraw(Canvas canvas) { + int height = getMeasuredHeight(); + int width = getMeasuredWidth(); + + mPath.rewind(); + mPath.moveTo((width / 2) - height, 0); + mPath.lineTo((width / 2) + height, 0); + mPath.lineTo((width / 2), height); + mPath.lineTo((width / 2) - height, 0); + canvas.drawPath(mPath, this.mPaint); + } +} diff --git a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/Marker.java b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/Marker.java index 515852ae6..17317bffb 100644 --- a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/Marker.java +++ b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/annotations/Marker.java @@ -1,7 +1,9 @@ package com.mapbox.mapboxgl.annotations; +import android.graphics.Point; +import android.view.View; import com.mapbox.mapboxgl.geometry.LatLng; - +import com.mapbox.mapboxgl.views.R; public class Marker extends Annotation { @@ -16,10 +18,16 @@ public class Marker extends Annotation { String snippet; String sprite = "default_marker"; String title; + private InfoWindow infoWindow = null; private boolean infoWindowShown = false; - public Marker() {} + /** + * Constructor + */ + public Marker() { + super(); + } /** * If two markers have the same LatLng, they are equal. @@ -41,6 +49,10 @@ public class Marker extends Annotation { return alpha; } + public Point getAnchor() { + return new Point((int)anchorU, (int)anchorV); + } + public float getAnchorU() { return anchorU; } @@ -66,7 +78,7 @@ public class Marker extends Annotation { } public void hideInfoWindow() { - //TODO hideInfoWindow + infoWindow.close(); infoWindowShown = false; } @@ -82,33 +94,33 @@ public class Marker extends Annotation { return infoWindowShown; } - void setAnchor(float u, float v) { + public void setAnchor(float u, float v) { this.anchorU = u; this.anchorV = v; } - void setDraggable(boolean draggable) { + public void setDraggable(boolean draggable) { this.draggable = draggable; } - void setFlat(boolean flat) { + public void setFlat(boolean flat) { this.flat = flat; } - void setInfoWindowAnchor(float u, float v) { + public void setInfoWindowAnchor(float u, float v) { infoWindowAnchorU = u; infoWindowAnchorV = v; } - void setPosition(LatLng latlng) { + public void setPosition(LatLng latlng) { this.position = position; } - void setRotation(float rotation) { + public void setRotation(float rotation) { this.rotation = rotation; } - void setSnippet(String snippet) { + public void setSnippet(String snippet) { this.snippet = snippet; } @@ -120,18 +132,55 @@ public class Marker extends Annotation { * * @param sprite */ - void setSprite(String sprite) { + public void setSprite(String sprite) { this.sprite = sprite; } - void setTitle(String title) { + public void setTitle(String title) { this.title = title; } - void showInfoWindow() { + public void showInfoWindow() { + + if (!isVisible() || mapView == null) { + return; + } + + getInfoWindow().open(this, getPosition(), (int) anchorU, (int) anchorV); + getInfoWindow().setBoundMarker(this); infoWindowShown = true; } + /** + * Use to set a custom OnTouchListener for the InfoWindow. + * By default the InfoWindow will close on touch. + * @param listener Custom OnTouchListener + */ + public void setInfoWindowOnTouchListener(View.OnTouchListener listener) { + if (listener == null) { + return; + } + getInfoWindow().setOnTouchListener(listener); + } + + /** + * Common internal InfoWindow initialization method + * @return InfoWindow for Marker + */ + private InfoWindow getInfoWindow() { + if (infoWindow == null) { + infoWindow = new InfoWindow(R.layout.infowindow, mapView); + } + return infoWindow; + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (!visible && infoWindowShown) { + hideInfoWindow(); + } + } // TODO Method in Google Maps Android API // public int hashCode() diff --git a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/MapView.java b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/MapView.java index 0d472e971..e96cf3ed0 100644 --- a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/MapView.java +++ b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/MapView.java @@ -1,6 +1,5 @@ package com.mapbox.mapboxgl.views; -import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -11,6 +10,7 @@ import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.PointF; +import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.hardware.GeomagneticField; import android.hardware.Sensor; @@ -27,7 +27,6 @@ import android.support.v4.content.ContextCompat; import android.support.v4.view.GestureDetectorCompat; import android.support.v4.view.ScaleGestureDetectorCompat; import android.text.TextUtils; -import android.util.Log; import android.view.GestureDetector; import android.view.Gravity; import android.view.ScaleGestureDetector; @@ -100,6 +99,8 @@ public class MapView extends FrameLayout implements LocationListener { * Every annotation that has been added to the map. */ private List mAnnotations = new ArrayList<>(); + private List mAnnotationsNearLastTap = new ArrayList<>(); + private Annotation mSelectedAnnotation = null; // // Instance members @@ -479,6 +480,9 @@ public class MapView extends FrameLayout implements LocationListener { private void removeAnnotationsWithId(long annotationId){ for (Iterator iterator = mAnnotations.iterator(); iterator.hasNext();) { Annotation annotation = iterator.next(); + if (annotation instanceof Marker) { + ((Marker)annotation).hideInfoWindow(); + } if (annotation.getId() == annotationId) { iterator.remove(); } @@ -486,6 +490,9 @@ public class MapView extends FrameLayout implements LocationListener { } public void removeAnnotation(Annotation annotation) { + if (annotation instanceof Marker) { + ((Marker)annotation).hideInfoWindow(); + } long id = annotation.getId(); mNativeMapView.removeAnnotation(id); mAnnotations.remove(annotation); @@ -499,8 +506,12 @@ public class MapView extends FrameLayout implements LocationListener { public void removeAnnotations() { long[] ids = new long[mAnnotations.size()]; for(int i = 0; i < mAnnotations.size(); i++) { - long id = mAnnotations.get(i).getId(); + Annotation annotation = mAnnotations.get(i); + long id = annotation.getId(); ids[i] = id; + if (annotation instanceof Marker) { + ((Marker)annotation).hideInfoWindow(); + } } mNativeMapView.removeAnnotations(ids); mAnnotations.clear(); @@ -523,9 +534,6 @@ public class MapView extends FrameLayout implements LocationListener { annotations.add(annotation); } } - for(int i = 0; i < annotations.size(); i++) { - Log.d(TAG, "tapped: " + Long.toString(annotations.get(i).getId())); - } return annotations; } @@ -731,6 +739,18 @@ public class MapView extends FrameLayout implements LocationListener { return new PointF(point.x * mScreenDensity, point.y * mScreenDensity); } + public double getTopOffsetPixelsForAnnotationSymbol(@NonNull String symbolName) { + return mNativeMapView.getTopOffsetPixelsForAnnotationSymbol(symbolName); + } + + /** + * Common Screen Density + * @return Screen Density + */ + public float getScreenDensity() { + return mScreenDensity; + } + // // Lifecycle events // @@ -1026,32 +1046,104 @@ public class MapView extends FrameLayout implements LocationListener { public boolean onSingleTapUp(MotionEvent e) { // Cancel any animation mNativeMapView.cancelTransitions(); - - // Select or deselect point annotations - PointF tapPoint = new PointF(e.getX(), e.getY()); - - float toleranceWidth = 60 * mScreenDensity; - float toleranceHeight = 80 * mScreenDensity; - - PointF tr = new PointF(tapPoint.x + toleranceWidth / 2, tapPoint.y + 2 * toleranceHeight / 3); - PointF bl = new PointF(tapPoint.x - toleranceWidth / 2, tapPoint.y - 1 * toleranceHeight / 3); - - LatLng sw = fromScreenLocation(bl); - LatLng ne = fromScreenLocation(tr); - - BoundingBox bbox = new BoundingBox(ne, sw); - - List annotations = getAnnotationsInBounds(bbox); - - performClick(); - return true; } - // Called for single taps after a delay @Override public boolean onSingleTapConfirmed(MotionEvent e) { - return false; + // Open / Close InfoWindow + + float x = e.getX(); + float y = e.getY(); + + // flip y direction vertically to match core GL + y = getHeight() - y; + + PointF tapPoint = new PointF(x, y); + + float toleranceWidth = 40 * mScreenDensity; + float toleranceHeight = 60 * mScreenDensity; + + RectF tapRect = new RectF(tapPoint.x - toleranceWidth / 2, tapPoint.y + 2 * toleranceHeight / 3, + tapPoint.x + toleranceWidth / 2, tapPoint.y - 1 * toleranceHeight / 3); + + List corners = Arrays.asList( + fromScreenLocation(new PointF(tapRect.left, tapRect.bottom)), + fromScreenLocation(new PointF(tapRect.left, tapRect.top)), + fromScreenLocation(new PointF(tapRect.right, tapRect.top)), + fromScreenLocation(new PointF(tapRect.right, tapRect.bottom)) + ); + + BoundingBox tapBounds = BoundingBox.fromLatLngs(corners); + + List nearbyAnnotations = getAnnotationsInBounds(tapBounds); + + long newSelectedAnnotationID = -1; + + if (nearbyAnnotations.size() > 0) { + + // there is at least one nearby annotation; select one + // + // first, sort for comparison and iteration + Collections.sort(nearbyAnnotations); + + if (nearbyAnnotations == mAnnotationsNearLastTap) + { + // the selection candidates haven't changed; cycle through them + if (mSelectedAnnotation != null && (mSelectedAnnotation.getId() == mAnnotationsNearLastTap.get(mAnnotationsNearLastTap.size() - 1).getId())) + { + // the selected annotation is the last in the set; cycle back to the first + // note: this could be the selected annotation if only one in set + newSelectedAnnotationID = mAnnotationsNearLastTap.get(0).getId(); + } + else if (mSelectedAnnotation != null) + { + // otherwise increment the selection through the candidates + long currentID = mSelectedAnnotation.getId(); + long result = mAnnotationsNearLastTap.indexOf(mSelectedAnnotation); + newSelectedAnnotationID = mAnnotationsNearLastTap.get((int) result + 1).getId(); + } + else + { + // no current selection; select the first one + newSelectedAnnotationID = mAnnotationsNearLastTap.get(0).getId(); + } + } + else + { + // start tracking a new set of nearby annotations + mAnnotationsNearLastTap = nearbyAnnotations; + + // select the first one + newSelectedAnnotationID = mAnnotationsNearLastTap.get(0).getId(); + } + + } else { + // there are no nearby annotations; deselect if necessary + newSelectedAnnotationID = -1; + } + + if (newSelectedAnnotationID >= 0) { + + for (Annotation annotation : mAnnotations) { + if (annotation instanceof Marker) { + if (annotation.getId() == newSelectedAnnotationID) { + if (mSelectedAnnotation == null || annotation.getId() != mSelectedAnnotation.getId()) { + selectAnnotation(annotation); + } + break; + } + } + } + + } else { + // deselect any selected annotation + if (mSelectedAnnotation != null) { + deselectAnnotation(); + } + } + + return true; } // Called for a long press @@ -1485,7 +1577,7 @@ public class MapView extends FrameLayout implements LocationListener { // Called for events that don't fit the other handlers // such as mouse scroll events, mouse moves, joystick, trackpad - @Override @TargetApi(12) + @Override public boolean onGenericMotionEvent(MotionEvent event) { // Mouse events //if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) { // this is not available before API 18 @@ -1891,5 +1983,39 @@ public class MapView extends FrameLayout implements LocationListener { mGpsMarker.setVisibility(View.INVISIBLE); } } + + if (change.equals(MapChange.MapChangeRegionWillChange) || change.equals(MapChange.MapChangeRegionWillChangeAnimated)) { + deselectAnnotation(); + } + + } + + private void selectAnnotation(Annotation annotation) { + + if (annotation == null) { + return; + } + + if (annotation == mSelectedAnnotation) { + return; + } + + if (annotation instanceof Marker) { + // Need to deselect any currently selected annotation first + deselectAnnotation(); + + ((Marker)annotation).showInfoWindow(); + mSelectedAnnotation = annotation; + } + } + + private void deselectAnnotation() { + if (mSelectedAnnotation != null && mSelectedAnnotation instanceof Marker) { + Marker marker = (Marker) mSelectedAnnotation; + if (marker.isInfoWindowShown()) { + marker.hideInfoWindow(); + mSelectedAnnotation = null; + } + } } } diff --git a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/NativeMapView.java b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/NativeMapView.java index ca5704987..3cf56a8de 100644 --- a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/NativeMapView.java +++ b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/NativeMapView.java @@ -3,7 +3,6 @@ package com.mapbox.mapboxgl.views; import android.graphics.PointF; import android.view.Surface; -import com.mapbox.mapboxgl.annotations.Annotation; import com.mapbox.mapboxgl.annotations.Marker; import com.mapbox.mapboxgl.annotations.Polygon; import com.mapbox.mapboxgl.annotations.Polyline; @@ -11,8 +10,6 @@ import com.mapbox.mapboxgl.geometry.BoundingBox; import com.mapbox.mapboxgl.geometry.LatLng; import com.mapbox.mapboxgl.geometry.LatLngZoom; import com.mapbox.mapboxgl.geometry.ProjectedMeters; - -import java.util.ArrayList; import java.util.List; // Class that wraps the native methods for convenience @@ -424,6 +421,10 @@ class NativeMapView { return nativeLatLngForPixel(mNativeMapViewPtr, pixel); } + public double getTopOffsetPixelsForAnnotationSymbol(String symbolName) { + return nativeGetTopOffsetPixelsForAnnotationSymbol(mNativeMapViewPtr, symbolName); + } + // // Callbacks // @@ -606,4 +607,6 @@ class NativeMapView { private native PointF nativePixelForLatLng(long nativeMapViewPtr, LatLng latLng); private native LatLng nativeLatLngForPixel(long nativeMapViewPtr, PointF pixel); + + private native double nativeGetTopOffsetPixelsForAnnotationSymbol(long nativeMapViewPtr, String symbolName); } diff --git a/android/java/MapboxGLAndroidSDK/src/main/res/layout/infowindow.xml b/android/java/MapboxGLAndroidSDK/src/main/res/layout/infowindow.xml new file mode 100644 index 000000000..af05e2fd4 --- /dev/null +++ b/android/java/MapboxGLAndroidSDK/src/main/res/layout/infowindow.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + diff --git a/android/java/MapboxGLAndroidSDK/src/main/res/values/colors.xml b/android/java/MapboxGLAndroidSDK/src/main/res/values/colors.xml new file mode 100644 index 000000000..bf3f48431 --- /dev/null +++ b/android/java/MapboxGLAndroidSDK/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FFFFFF + #000000 + diff --git a/android/java/MapboxGLAndroidSDK/src/main/res/values/strings.xml b/android/java/MapboxGLAndroidSDK/src/main/res/values/strings.xml index df022a467..ff91a160d 100644 --- a/android/java/MapboxGLAndroidSDK/src/main/res/values/strings.xml +++ b/android/java/MapboxGLAndroidSDK/src/main/res/values/strings.xml @@ -1,4 +1,7 @@ Map compass. Click to reset the map rotation to North. + Title + Description + Address diff --git a/android/java/MapboxGLAndroidSDKTestApp/build.gradle b/android/java/MapboxGLAndroidSDKTestApp/build.gradle index b0c0f1a3e..84f627d9e 100644 --- a/android/java/MapboxGLAndroidSDKTestApp/build.gradle +++ b/android/java/MapboxGLAndroidSDKTestApp/build.gradle @@ -82,10 +82,10 @@ dependencies { compile(project(':MapboxGLAndroidSDK')) { transitive = true } - compile 'com.android.support:support-annotations:23.0.0' - compile 'com.android.support:support-v4:23.0.0' - compile 'com.android.support:appcompat-v7:23.0.0' - compile 'com.android.support:design:23.0.0' + compile 'com.android.support:support-annotations:23.0.1' + compile 'com.android.support:support-v4:23.0.1' + compile 'com.android.support:appcompat-v7:23.0.1' + compile 'com.android.support:design:23.0.1' compile('com.crashlytics.sdk.android:crashlytics:2.5.1@aar') { transitive = true; } diff --git a/android/java/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxgl/testapp/MainActivity.java b/android/java/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxgl/testapp/MainActivity.java index a4358c773..ed9b7ef06 100644 --- a/android/java/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxgl/testapp/MainActivity.java +++ b/android/java/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxgl/testapp/MainActivity.java @@ -22,7 +22,9 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; +import android.widget.Toast; import com.crashlytics.android.Crashlytics; +import com.mapbox.mapboxgl.annotations.Marker; import com.mapbox.mapboxgl.annotations.MarkerOptions; import com.mapbox.mapboxgl.annotations.PolygonOptions; import com.mapbox.mapboxgl.annotations.PolylineOptions; @@ -32,6 +34,7 @@ import io.fabric.sdk.android.Fabric; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.text.DecimalFormat; import java.util.ArrayList; public class MainActivity extends AppCompatActivity { @@ -121,6 +124,8 @@ public class MainActivity extends AppCompatActivity { mMapView.addMarker(new MarkerOptions() .position(position) .title("Dropped Pin") + .snippet(new DecimalFormat("#.#####").format(position.getLatitude()) + ", " + + new DecimalFormat("#.#####").format(position.getLongitude())) .sprite("default_marker")); } }); @@ -392,10 +397,18 @@ public class MainActivity extends AppCompatActivity { private void addMarkers() { LatLng backLot = new LatLng(38.649441, -121.369064); - mMapView.addMarker(new MarkerOptions() + final Marker marker = mMapView.addMarker(new MarkerOptions() .position(backLot) .title("Back Lot") .snippet("The back lot behind my house")); + marker.setInfoWindowOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + Toast.makeText(getApplicationContext(), "Custom Info Touch Listener!!", Toast.LENGTH_SHORT).show(); + marker.hideInfoWindow(); + return true; + } + }); LatLng cheeseRoom = new LatLng(38.531577,-122.010646); mMapView.addMarker(new MarkerOptions() diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index 516d5e832..6234f4d88 100644 --- a/gyp/platform-ios.gypi +++ b/gyp/platform-ios.gypi @@ -20,6 +20,8 @@ '../include/mbgl/ios/Mapbox.h', '../platform/ios/MGLMapboxEvents.h', '../platform/ios/MGLMapboxEvents.m', + '../include/mbgl/ios/MGLMapCamera.h', + '../platform/ios/MGLMapCamera.mm', '../include/mbgl/ios/MGLMapView.h', '../include/mbgl/ios/MGLMapView+IBAdditions.h', '../platform/ios/MGLMapView.mm', diff --git a/include/mbgl/ios/MGLMapCamera.h b/include/mbgl/ios/MGLMapCamera.h new file mode 100644 index 000000000..68f3923fd --- /dev/null +++ b/include/mbgl/ios/MGLMapCamera.h @@ -0,0 +1,36 @@ +#import "Mapbox.h" + +#pragma once + +NS_ASSUME_NONNULL_BEGIN + +/** An `MGLMapCamera` object represents a viewpoint from which the user observes some point on an `MGLMapView`. */ +@interface MGLMapCamera : NSObject + +/** Coordinate at the center of the map view. */ +@property (nonatomic) CLLocationCoordinate2D centerCoordinate; + +/** Heading measured in degrees clockwise from true north. */ +@property (nonatomic) CLLocationDirection heading; + +/** Pitch toward the horizon measured in degrees, with 0 degrees resulting in a two-dimensional map. */ +@property (nonatomic) CGFloat pitch; + +/** Meters above ground level. */ +@property (nonatomic) CLLocationDistance altitude; + +/** Returns a new camera with all properties set to 0. */ ++ (instancetype)camera; + ++ (instancetype)cameraLookingAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + fromEyeCoordinate:(CLLocationCoordinate2D)eyeCoordinate + eyeAltitude:(CLLocationDistance)eyeAltitude; + ++ (instancetype)cameraLookingAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + fromDistance:(CLLocationDistance)distance + pitch:(CGFloat)pitch + heading:(CLLocationDirection)heading; + +@end + +NS_ASSUME_NONNULL_END diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index ba332d984..da500f1ef 100644 --- a/include/mbgl/ios/MGLMapView.h +++ b/include/mbgl/ios/MGLMapView.h @@ -1,4 +1,5 @@ #import "MGLGeometry.h" +#import "MGLMapCamera.h" #import #import @@ -6,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN @class MGLAnnotationImage; +@class MGLMapCamera; @class MGLUserLocation; @class MGLPolyline; @class MGLPolygon; @@ -136,6 +138,8 @@ IB_DESIGNABLE * @param animated Specify `YES` if you want the map view to animate scrolling and zooming to the new location or `NO` if you want the map to display the new location immediately. */ - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated; +- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction animated:(BOOL)animated; + /** The coordinate bounds visible in the receiver’s viewport. * * Changing the value of this property updates the receiver immediately. If you want to animate the change, call `setVisibleCoordinateBounds:animated:` instead. */ @@ -181,19 +185,19 @@ IB_DESIGNABLE /** Resets the map rotation to a northern heading. */ - (IBAction)resetNorth; -/** The pitch of the map (measured in degrees). - * - * The default value `0` shows a completely flat map. Maximum value is `60`. */ -@property (nonatomic) double pitch; +/** A camera representing the current viewpoint of the map. */ +@property (nonatomic, copy) MGLMapCamera *camera; -/** Changes the pitch of the map. - * @param pitch The pitch of the map (measured in degrees) relative to top-down. - * - * Changing the pitch tilts the map without changing the current center coordinate or zoom level. */ -- (void)setPitch:(double)pitch; +/** Moves the viewpoint to a different location with respect to the map with an optional transition animation. +* @param camera The new viewpoint. +* @param animated Specify `YES` if you want the map view to animate the change to the new viewpoint or `NO` if you want the map to display the new viewpoint immediately. */ +- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated; -/** Resets the map pitch to head-on. */ -- (IBAction)resetPitch; +/** Moves the viewpoint to a different location with respect to the map with an optional transition duration and timing function. +* @param camera The new viewpoint. +* @param duration The amount of time, measured in seconds, that the transition animation should take. Specify `0` to jump to the new viewpoint instantaneously. +* @param function A timing function used for the animation. Set this parameter to `nil` for a transition that matches most system animations. If the duration is `0`, this parameter is ignored. */ +- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function; #pragma mark - Converting Map Coordinates diff --git a/include/mbgl/ios/Mapbox.h b/include/mbgl/ios/Mapbox.h index 401a62e82..f05f0c842 100644 --- a/include/mbgl/ios/Mapbox.h +++ b/include/mbgl/ios/Mapbox.h @@ -1,6 +1,7 @@ #import "MGLAccountManager.h" #import "MGLAnnotation.h" #import "MGLAnnotationImage.h" +#import "MGLMapCamera.h" #import "MGLGeometry.h" #import "MGLMapView.h" #import "MGLMultiPoint.h" diff --git a/include/mbgl/map/camera.hpp b/include/mbgl/map/camera.hpp new file mode 100644 index 000000000..bd0b353ba --- /dev/null +++ b/include/mbgl/map/camera.hpp @@ -0,0 +1,22 @@ +#ifndef MBGL_MAP_CAMERA +#define MBGL_MAP_CAMERA + +#include +#include +#include +#include + +namespace mbgl { + +struct CameraOptions { + mapbox::util::optional center; + mapbox::util::optional zoom; + mapbox::util::optional angle; + mapbox::util::optional pitch; + mapbox::util::optional duration; + mapbox::util::optional easing; +}; + +} + +#endif /* MBGL_MAP_CAMERA */ diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index ddf86045e..4cd9293c9 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -2,6 +2,7 @@ #define MBGL_MAP_MAP #include +#include #include #include #include @@ -94,6 +95,10 @@ public: void cancelTransitions(); void setGestureInProgress(bool); + // Camera + void jumpTo(CameraOptions options); + void easeTo(CameraOptions options); + // Position void moveBy(double dx, double dy, const Duration& = Duration::zero()); void setLatLng(LatLng latLng, const Duration& = Duration::zero()); @@ -107,8 +112,8 @@ public: void setZoom(double zoom, const Duration& = Duration::zero()); double getZoom() const; void setLatLngZoom(LatLng latLng, double zoom, const Duration& = Duration::zero()); - void fitBounds(LatLngBounds bounds, EdgeInsets padding, const Duration& duration = Duration::zero()); - void fitBounds(AnnotationSegment segment, EdgeInsets padding, const Duration& duration = Duration::zero()); + CameraOptions cameraForLatLngBounds(LatLngBounds bounds, EdgeInsets padding); + CameraOptions cameraForLatLngs(std::vector latLngs, EdgeInsets padding); void resetZoom(); double getMinZoom() const; double getMaxZoom() const; @@ -121,7 +126,7 @@ public: void resetNorth(); // Pitch - void setPitch(double pitch); + void setPitch(double pitch, const Duration& = Duration::zero()); double getPitch() const; // Size diff --git a/src/mbgl/util/optional.hpp b/include/mbgl/util/optional.hpp similarity index 100% rename from src/mbgl/util/optional.hpp rename to include/mbgl/util/optional.hpp diff --git a/src/mbgl/util/unitbezier.hpp b/include/mbgl/util/unitbezier.hpp similarity index 100% rename from src/mbgl/util/unitbezier.hpp rename to include/mbgl/util/unitbezier.hpp diff --git a/ios/app/MBXViewController.mm b/ios/app/MBXViewController.mm index 99c207d24..d94e41b5f 100644 --- a/ios/app/MBXViewController.mm +++ b/ios/app/MBXViewController.mm @@ -2,8 +2,6 @@ #import -#import - #import static UIColor *const kTintColor = [UIColor colorWithRed:0.120 green:0.550 blue:0.670 alpha:1.000]; @@ -26,10 +24,20 @@ static NSUInteger const kStyleVersion = 8; @implementation MBXViewController -mbgl::Settings_NSUserDefaults *settings = nullptr; - #pragma mark - Setup ++ (void)initialize +{ + if (self == [MBXViewController class]) + { + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + @"userTrackingMode": @(MGLUserTrackingModeNone), + @"showsUserLocation": @NO, + @"debug": @NO, + }]; + } +} + - (id)init { self = [super init]; @@ -38,6 +46,7 @@ mbgl::Settings_NSUserDefaults *settings = nullptr; { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveState:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(restoreState:) name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveState:) name:UIApplicationWillTerminateNotification object:nil]; } return self; @@ -49,7 +58,6 @@ mbgl::Settings_NSUserDefaults *settings = nullptr; self.mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds]; self.mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - self.mapView.showsUserLocation = YES; self.mapView.delegate = self; [self.view addSubview:self.mapView]; @@ -74,36 +82,42 @@ mbgl::Settings_NSUserDefaults *settings = nullptr; target:self action:@selector(locateUser)]; - settings = new mbgl::Settings_NSUserDefaults(); [self restoreState:nil]; } - (void)saveState:(__unused NSNotification *)notification { - if (self.mapView && settings) + if (self.mapView) { - settings->longitude = self.mapView.centerCoordinate.longitude; - settings->latitude = self.mapView.centerCoordinate.latitude; - settings->zoom = self.mapView.zoomLevel; - settings->bearing = self.mapView.direction; - settings->pitch = self.mapView.pitch; - settings->debug = self.mapView.isDebugActive; - settings->userTrackingMode = self.mapView.userTrackingMode; - settings->showsUserLocation = self.mapView.showsUserLocation; - settings->save(); + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSData *archivedCamera = [NSKeyedArchiver archivedDataWithRootObject:self.mapView.camera]; + [defaults setObject:archivedCamera forKey:@"camera"]; + [defaults setInteger:self.mapView.userTrackingMode forKey:@"userTrackingMode"]; + [defaults setBool:self.mapView.showsUserLocation forKey:@"showsUserLocation"]; + [defaults setBool:self.mapView.debugActive forKey:@"debug"]; + [defaults synchronize]; } } - (void)restoreState:(__unused NSNotification *)notification { - if (self.mapView && settings) { - settings->load(); - [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(settings->latitude, settings->longitude) zoomLevel:settings->zoom animated:NO]; - self.mapView.direction = settings->bearing; - self.mapView.pitch = settings->pitch; - self.mapView.userTrackingMode = settings->userTrackingMode; - self.mapView.showsUserLocation = settings->showsUserLocation; - [self.mapView setDebugActive:settings->debug]; + if (self.mapView) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSData *archivedCamera = [defaults objectForKey:@"camera"]; + MGLMapCamera *camera = archivedCamera ? [NSKeyedUnarchiver unarchiveObjectWithData:archivedCamera] : nil; + if (camera) + { + self.mapView.camera = camera; + } + NSInteger uncheckedTrackingMode = [defaults integerForKey:@"userTrackingMode"]; + if (uncheckedTrackingMode >= 0 && + (NSUInteger)uncheckedTrackingMode >= MGLUserTrackingModeNone && + (NSUInteger)uncheckedTrackingMode <= MGLUserTrackingModeFollowWithCourse) + { + self.mapView.userTrackingMode = (MGLUserTrackingMode)uncheckedTrackingMode; + } + self.mapView.showsUserLocation = [defaults boolForKey:@"showsUserLocation"]; + self.mapView.debugActive = [defaults boolForKey:@"debug"]; } } @@ -333,12 +347,7 @@ mbgl::Settings_NSUserDefaults *settings = nullptr; { [[NSNotificationCenter defaultCenter] removeObserver:self]; - if (settings) - { - [self saveState:nil]; - delete settings; - settings = nullptr; - } + [self saveState:nil]; } #pragma mark - MGLMapViewDelegate diff --git a/ios/app/app-info.plist b/ios/app/app-info.plist index 978cc271e..665324693 100644 --- a/ios/app/app-info.plist +++ b/ios/app/app-info.plist @@ -40,7 +40,7 @@ NSHumanReadableCopyright (c) 2014 Mapbox NSLocationWhenInUseUsageDescription - The map will be centered on the user's location. + The map will display the user's location. UILaunchStoryboardName Default UIRequiredDeviceCapabilities diff --git a/ios/app/mapboxgl-app.gypi b/ios/app/mapboxgl-app.gypi index 34381ca15..8407ea7da 100644 --- a/ios/app/mapboxgl-app.gypi +++ b/ios/app/mapboxgl-app.gypi @@ -32,7 +32,6 @@ './MBXAppDelegate.m', './MBXViewController.h', './MBXViewController.mm', - '../../platform/darwin/settings_nsuserdefaults.mm', ], 'xcode_settings': { diff --git a/platform/ios/MGLMapCamera.mm b/platform/ios/MGLMapCamera.mm new file mode 100644 index 000000000..d04e46fa9 --- /dev/null +++ b/platform/ios/MGLMapCamera.mm @@ -0,0 +1,101 @@ +#import "MGLMapCamera.h" + +#include + +@implementation MGLMapCamera + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + ++ (instancetype)camera +{ + return [[self alloc] init]; +} + ++ (instancetype)cameraLookingAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + fromEyeCoordinate:(CLLocationCoordinate2D)eyeCoordinate + eyeAltitude:(CLLocationDistance)eyeAltitude +{ + mbgl::LatLng centerLatLng = mbgl::LatLng(centerCoordinate.latitude, centerCoordinate.longitude); + mbgl::LatLng eyeLatLng = mbgl::LatLng(eyeCoordinate.latitude, eyeCoordinate.longitude); + + mbgl::ProjectedMeters centerMeters = mbgl::Projection::projectedMetersForLatLng(centerLatLng); + mbgl::ProjectedMeters eyeMeters = mbgl::Projection::projectedMetersForLatLng(eyeLatLng); + CLLocationDirection heading = std::atan((centerMeters.northing - eyeMeters.northing) / + (centerMeters.easting - eyeMeters.easting)); + + double groundDistance = std::hypot(centerMeters.northing - eyeMeters.northing, + centerMeters.easting - eyeMeters.easting); + CGFloat pitch = std::atan(eyeAltitude / groundDistance); + + return [[self alloc] initWithCenterCoordinate:centerCoordinate + altitude:eyeAltitude + pitch:pitch + heading:heading]; +} + ++ (instancetype)cameraLookingAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + fromDistance:(CLLocationDistance)distance + pitch:(CGFloat)pitch + heading:(CLLocationDirection)heading +{ + return [[self alloc] initWithCenterCoordinate:centerCoordinate + altitude:distance + pitch:(CGFloat)pitch + heading:heading]; +} + +- (instancetype)initWithCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + altitude:(CLLocationDistance)altitude + pitch:(CGFloat)pitch + heading:(CLLocationDirection)heading +{ + if (self = [super init]) + { + _centerCoordinate = centerCoordinate; + _altitude = altitude; + _pitch = pitch; + _heading = heading; + } + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + if (self = [super init]) + { + _centerCoordinate = CLLocationCoordinate2DMake([coder decodeDoubleForKey:@"centerLatitude"], + [coder decodeDoubleForKey:@"centerLongitude"]); + _altitude = [coder decodeDoubleForKey:@"altitude"]; + _pitch = [coder decodeDoubleForKey:@"pitch"]; + _heading = [coder decodeDoubleForKey:@"heading"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeDouble:_centerCoordinate.latitude forKey:@"centerLatitude"]; + [coder encodeDouble:_centerCoordinate.longitude forKey:@"centerLongitude"]; + [coder encodeDouble:_altitude forKey:@"altitude"]; + [coder encodeDouble:_pitch forKey:@"pitch"]; + [coder encodeDouble:_heading forKey:@"heading"]; +} + +- (id)copyWithZone:(nullable NSZone *)zone +{ + return [[[self class] allocWithZone:zone] initWithCenterCoordinate:_centerCoordinate + altitude:_altitude + pitch:_pitch + heading:_heading]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", + self, _centerCoordinate.latitude, _centerCoordinate.longitude, _altitude, _heading, _pitch]; +} + +@end diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index 959f294f0..e509b5c8d 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -48,6 +48,9 @@ NSUInteger const MGLStyleVersion = 8; const NSTimeInterval MGLAnimationDuration = 0.3; const CGSize MGLAnnotationUpdateViewportOutset = {150, 150}; const CGFloat MGLMinimumZoom = 3; +const CGFloat MGLMinimumPitch = 0; +const CGFloat MGLMaximumPitch = 60; +const CLLocationDegrees MGLAngularFieldOfView = M_PI / 6.; NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey"; NSString *const MGLAnnotationSymbolKey = @"MGLAnnotationSymbolKey"; @@ -67,6 +70,18 @@ CLLocationDegrees MGLDegreesFromRadians(CGFloat radians) return radians * 180 / M_PI; } +mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction *function) +{ + if ( ! function) + { + function = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]; + } + float p1[2], p2[2]; + [function getControlPointAtIndex:0 values:p1]; + [function getControlPointAtIndex:1 values:p2]; + return { p1[0], p1[1], p2[0], p2[1] }; +} + #pragma mark - Private - @interface MGLMapView () @@ -368,7 +383,10 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) // set initial position // - _mbglMap->setLatLngZoom(mbgl::LatLng(0, 0), _mbglMap->getMinZoom()); + mbgl::CameraOptions options; + options.center = mbgl::LatLng(0, 0); + options.zoom = _mbglMap->getMinZoom(); + _mbglMap->jumpTo(options); _pendingLatitude = NAN; _pendingLongitude = NAN; @@ -1347,12 +1365,10 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) else if (twoFingerDrag.state == UIGestureRecognizerStateBegan || twoFingerDrag.state == UIGestureRecognizerStateChanged) { CGFloat gestureDistance = CGPoint([twoFingerDrag translationInView:twoFingerDrag.view]).y; - double currentPitch = _mbglMap->getPitch(); - double minPitch = 0; - double maxPitch = 60.0; - double slowdown = 20.0; + CGFloat currentPitch = _mbglMap->getPitch(); + CGFloat slowdown = 20.0; - double pitchNew = fmax(fmin(currentPitch - (gestureDistance / slowdown), maxPitch), minPitch); + CGFloat pitchNew = mbgl::util::clamp(currentPitch - (gestureDistance / slowdown), MGLMinimumPitch, MGLMaximumPitch); _mbglMap->setPitch(pitchNew); } @@ -1530,7 +1546,7 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate { - return [NSSet setWithObjects:@"latitude", @"longitude", nil]; + return [NSSet setWithObjects:@"latitude", @"longitude", @"camera", nil]; } - (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated preservingTracking:(BOOL)tracking @@ -1542,13 +1558,7 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) - (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated { - CGFloat duration = (animated ? MGLAnimationDuration : 0); - - _mbglMap->setLatLngZoom(MGLLatLngFromLocationCoordinate2D(coordinate), - fmaxf(_mbglMap->getZoom(), self.currentMinimumZoom), - secondsAsDuration(duration)); - - [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; + [self setCenterCoordinate:coordinate zoomLevel:self.zoomLevel animated:animated]; } - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate @@ -1562,43 +1572,58 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) } - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated +{ + [self setCenterCoordinate:centerCoordinate zoomLevel:zoomLevel direction:self.direction animated:animated]; +} + +- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction animated:(BOOL)animated { self.userTrackingMode = MGLUserTrackingModeNone; - CGFloat duration = (animated ? MGLAnimationDuration : 0); + [self _setCenterCoordinate:centerCoordinate zoomLevel:zoomLevel direction:direction animated:animated]; +} - _mbglMap->setLatLngZoom(MGLLatLngFromLocationCoordinate2D(centerCoordinate), zoomLevel, secondsAsDuration(duration)); +- (void)_setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction animated:(BOOL)animated +{ + mbgl::CameraOptions options; + options.center = MGLLatLngFromLocationCoordinate2D(centerCoordinate); + options.zoom = fmaxf(zoomLevel, self.currentMinimumZoom); + if (direction >= 0) + { + options.angle = MGLRadiansFromDegrees(-direction); + } + if (animated) + { + options.duration = secondsAsDuration(MGLAnimationDuration); + options.easing = MGLUnitBezierForMediaTimingFunction(nil); + } + _mbglMap->easeTo(options); [self unrotateIfNeededAnimated:animated]; [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; } ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomLevel +{ + return [NSSet setWithObject:@"camera"]; +} + - (double)zoomLevel { return _mbglMap->getZoom(); } -- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated -{ - self.userTrackingMode = MGLUserTrackingModeNone; - - CGFloat duration = (animated ? MGLAnimationDuration : 0); - - _mbglMap->setLatLngZoom(_mbglMap->getLatLng(), - fmaxf(zoomLevel, self.currentMinimumZoom), - secondsAsDuration(duration)); - - [self unrotateIfNeededAnimated:animated]; - - [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; -} - - (void)setZoomLevel:(double)zoomLevel { [self setZoomLevel:zoomLevel animated:NO]; } +- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated +{ + [self setCenterCoordinate:self.centerCoordinate zoomLevel:zoomLevel animated:animated]; +} + MGLCoordinateBounds MGLCoordinateBoundsFromLatLngBounds(mbgl::LatLngBounds latLngBounds) { return MGLCoordinateBoundsMake(MGLLocationCoordinate2DFromLatLng(latLngBounds.sw), @@ -1639,11 +1664,34 @@ mbgl::LatLngBounds MGLLatLngBoundsFromCoordinateBounds(MGLCoordinateBounds coord animated:animated]; } +- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction animated:(BOOL)animated +{ + CLLocationCoordinate2D coordinates[] = { + {bounds.ne.latitude, bounds.sw.longitude}, + bounds.sw, + {bounds.sw.latitude, bounds.ne.longitude}, + bounds.ne, + }; + [self setVisibleCoordinates:coordinates + count:sizeof(coordinates) / sizeof(coordinates[0]) + edgePadding:insets + direction:direction + animated:animated]; +} + - (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated +{ + [self setVisibleCoordinates:coordinates count:count edgePadding:insets direction:self.direction animated:animated]; +} + +- (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction animated:(BOOL)animated +{ + [self setVisibleCoordinates:coordinates count:count edgePadding:insets direction:direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil]; +} + +- (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(CAMediaTimingFunction *)function { // NOTE: does not disrupt tracking mode - CGFloat duration = animated ? MGLAnimationDuration : 0; - [self willChangeValueForKey:@"visibleCoordinateBounds"]; mbgl::EdgeInsets mbglInsets = {insets.top, insets.left, insets.bottom, insets.right}; mbgl::AnnotationSegment segment; @@ -1652,12 +1700,27 @@ mbgl::LatLngBounds MGLLatLngBoundsFromCoordinateBounds(MGLCoordinateBounds coord { segment.push_back({coordinates[i].latitude, coordinates[i].longitude}); } - _mbglMap->fitBounds(segment, mbglInsets, secondsAsDuration(duration)); + mbgl::CameraOptions options = _mbglMap->cameraForLatLngs(segment, mbglInsets); + if (direction >= 0) + { + options.angle = MGLRadiansFromDegrees(-direction); + } + if (duration > 0) + { + options.duration = secondsAsDuration(duration); + options.easing = MGLUnitBezierForMediaTimingFunction(function); + } + _mbglMap->easeTo(options); [self didChangeValueForKey:@"visibleCoordinateBounds"]; - [self unrotateIfNeededAnimated:animated]; + [self unrotateIfNeededAnimated:duration > 0]; - [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; + [self notifyMapChange:(duration > 0 ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingDirection +{ + return [NSSet setWithObject:@"camera"]; } - (CLLocationDirection)direction @@ -1683,23 +1746,139 @@ mbgl::LatLngBounds MGLLatLngBoundsFromCoordinateBounds(MGLCoordinateBounds coord [self setDirection:direction animated:NO]; } -- (double)pitch ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingPitch { - return _mbglMap->getPitch(); + return [NSSet setWithObject:@"camera"]; } -- (void)setPitch:(double)pitch ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCamera { - // constrain pitch to between 0º and 60º - // - _mbglMap->setPitch(fmax(fmin(pitch, 60), 0)); + return [NSSet setWithObjects:@"longitude", @"latitude", @"centerCoordinate", @"zoomLevel", @"direction", nil]; +} + +- (MGLMapCamera *)camera +{ + CGRect frame = self.frame; + CGPoint edgePoint; + // Constrain by the shorter of the two axes. + if (frame.size.width > frame.size.height) // landscape + { + edgePoint = CGPointMake(0, frame.size.height / 2.); + } + else // portrait + { + edgePoint = CGPointMake(frame.size.width / 2., 0); + } + CLLocationCoordinate2D edgeCoordinate = [self convertPoint:edgePoint toCoordinateFromView:self]; + mbgl::ProjectedMeters edgeMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(edgeCoordinate)); - //[self notifyMapChange:(mbgl::MapChangeRegionDidChange)]; + // Because we constrain the zoom level vertically in portrait orientation, + // the visible medial span is affected by pitch: the distance from the + // center point to the near edge is less than than distance from the center + // point to the far edge. Average the two distances. + mbgl::ProjectedMeters nearEdgeMeters; + if (frame.size.width > frame.size.height) + { + nearEdgeMeters = edgeMeters; + } + else + { + CGPoint nearEdgePoint = CGPointMake(frame.size.width / 2., frame.size.height); + CLLocationCoordinate2D nearEdgeCoordinate = [self convertPoint:nearEdgePoint toCoordinateFromView:self]; + nearEdgeMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(nearEdgeCoordinate)); + } + + // The opposite side is the distance between the center and one edge. + CLLocationCoordinate2D centerCoordinate = self.centerCoordinate; + mbgl::ProjectedMeters centerMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate)); + CLLocationDistance centerToEdge = std::hypot(centerMeters.easting - edgeMeters.easting, + centerMeters.northing - edgeMeters.northing); + CLLocationDistance centerToNearEdge = std::hypot(centerMeters.easting - nearEdgeMeters.easting, + centerMeters.northing - nearEdgeMeters.northing); + CLLocationDistance altitude = (centerToEdge + centerToNearEdge) / 2 / std::tan(MGLAngularFieldOfView / 2.); + + CGFloat pitch = _mbglMap->getPitch(); + + return [MGLMapCamera cameraLookingAtCenterCoordinate:centerCoordinate + fromDistance:altitude + pitch:pitch + heading:self.direction]; } -- (void)resetPitch +- (void)setCamera:(MGLMapCamera *)camera { - [self setPitch:0]; + [self setCamera:camera animated:NO]; +} + +- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated +{ + [self setCamera:camera withDuration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil]; +} + +- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(CAMediaTimingFunction *)function +{ + // The opposite side is the distance between the center and one edge. + mbgl::LatLng centerLatLng = MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate); + mbgl::ProjectedMeters centerMeters = _mbglMap->projectedMetersForLatLng(centerLatLng); + CLLocationDistance centerToEdge = camera.altitude * std::tan(MGLAngularFieldOfView / 2.); + + double angle = -1; + if (camera.heading >= 0) + { + angle = MGLRadiansFromDegrees(-camera.heading); + } + double pitch = -1; + if (camera.pitch >= 0) + { + pitch = MGLRadiansFromDegrees(mbgl::util::clamp(camera.pitch, MGLMinimumPitch, MGLMaximumPitch)); + } + + // Make a visible bounds that extends in the constrained direction (the + // shorter of the two axes). + CGRect frame = self.frame; + mbgl::LatLng sw, ne; + if (frame.size.width > frame.size.height) // landscape + { + sw = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing - centerToEdge * std::sin(angle), + centerMeters.easting - centerToEdge * std::cos(angle), + }); + ne = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing + centerToEdge * std::sin(angle), + centerMeters.easting + centerToEdge * std::cos(angle), + }); + } + else // portrait + { + sw = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing - centerToEdge * std::cos(-angle) + centerToEdge * std::cos(-angle) * std::sin(pitch) / 2, + centerMeters.easting - centerToEdge * std::sin(-angle) + centerToEdge * std::sin(-angle) * std::sin(pitch) / 2, + }); + ne = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing + centerToEdge * std::cos(-angle) - centerToEdge * std::cos(-angle) * std::sin(pitch) / 2, + centerMeters.easting + centerToEdge * std::sin(-angle) - centerToEdge * std::sin(-angle) * std::sin(pitch) / 2, + }); + } + + // Fit the viewport to the bounds. Correct the center in case pitch should + // cause the visual center to lie above the screen center. + mbgl::CameraOptions options = _mbglMap->cameraForLatLngs({ sw, ne }, {}); + options.center = centerLatLng; + + if (camera.heading >= 0) + { + options.angle = angle; + } + if (pitch >= 0) + { + options.pitch = pitch; + } + if (duration > 0) + { + options.duration = secondsAsDuration(duration); + options.easing = MGLUnitBezierForMediaTimingFunction(function); + } + _mbglMap->easeTo(options); } - (CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(nullable UIView *)view @@ -2482,6 +2661,12 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) [self.delegate mapView:self didUpdateUserLocation:self.userLocation]; } } + + CLLocationDirection course = self.userLocation.location.course; + if (course < 0 || self.userTrackingMode != MGLUserTrackingModeFollowWithCourse) + { + course = -1; + } if (self.userTrackingMode != MGLUserTrackingModeNone) { @@ -2496,7 +2681,7 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) { // at sufficient detail, just re-center the map; don't zoom // - [self setCenterCoordinate:self.userLocation.location.coordinate animated:YES preservingTracking:YES]; + [self _setCenterCoordinate:self.userLocation.location.coordinate zoomLevel:self.zoomLevel direction:course animated:YES]; } else { @@ -2526,17 +2711,11 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) desiredSouthWest.longitude != actualSouthWest.longitude) { // assumes we won't disrupt tracking mode - [self setVisibleCoordinateBounds:MGLCoordinateBoundsMake(desiredSouthWest, desiredNorthEast) animated:YES]; + [self setVisibleCoordinateBounds:MGLCoordinateBoundsMake(desiredSouthWest, desiredNorthEast) edgePadding:UIEdgeInsetsZero direction:course animated:YES]; } } } } - - CLLocationDirection course = self.userLocation.location.course; - if (course >= 0 && self.userTrackingMode == MGLUserTrackingModeFollowWithCourse) - { - _mbglMap->setBearing(course); - } self.userLocationAnnotationView.haloLayer.hidden = ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate) || newLocation.horizontalAccuracy > 10; @@ -3088,7 +3267,7 @@ class MBGLView : public mbgl::View + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLatitude { - return [NSSet setWithObject:@"centerCoordinate"]; + return [NSSet setWithObjects:@"centerCoordinate", @"camera", nil]; } - (double)latitude @@ -3114,7 +3293,7 @@ class MBGLView : public mbgl::View + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLongitude { - return [NSSet setWithObject:@"centerCoordinate"]; + return [NSSet setWithObjects:@"centerCoordinate", @"camera", nil]; } - (double)longitude diff --git a/src/mbgl/map/camera.cpp b/src/mbgl/map/camera.cpp new file mode 100644 index 000000000..4a45e904f --- /dev/null +++ b/src/mbgl/map/camera.cpp @@ -0,0 +1 @@ +#include diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index 82ab72db2..9195f6b58 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -9,6 +9,7 @@ #include #include +#include namespace mbgl { @@ -114,6 +115,18 @@ void Map::setGestureInProgress(bool inProgress) { update(Update::Repaint); } +#pragma mark - + +void Map::jumpTo(CameraOptions options) { + transform->jumpTo(options); + update(Update::Repaint); +} + +void Map::easeTo(CameraOptions options) { + transform->easeTo(options); + update(options.zoom ? Update::Zoom : Update::Repaint); +} + #pragma mark - Position void Map::moveBy(double dx, double dy, const Duration& duration) { @@ -131,9 +144,11 @@ LatLng Map::getLatLng() const { } void Map::resetPosition() { - transform->setAngle(0); - transform->setLatLng(LatLng(0, 0)); - transform->setZoom(0); + CameraOptions options; + options.angle = 0; + options.center = LatLng(0, 0); + options.zoom = 0; + transform->jumpTo(options); update(Update::Zoom); } @@ -168,25 +183,26 @@ void Map::setLatLngZoom(LatLng latLng, double zoom, const Duration& duration) { update(Update::Zoom); } -void Map::fitBounds(LatLngBounds bounds, EdgeInsets padding, const Duration& duration) { +CameraOptions Map::cameraForLatLngBounds(LatLngBounds bounds, EdgeInsets padding) { AnnotationSegment segment = { {bounds.ne.latitude, bounds.sw.longitude}, bounds.sw, {bounds.sw.latitude, bounds.ne.longitude}, bounds.ne, }; - fitBounds(segment, padding, duration); + return cameraForLatLngs(segment, padding); } -void Map::fitBounds(AnnotationSegment segment, EdgeInsets padding, const Duration& duration) { - if (segment.empty()) { - return; +CameraOptions Map::cameraForLatLngs(std::vector latLngs, EdgeInsets padding) { + CameraOptions options; + if (latLngs.empty()) { + return options; } // Calculate the bounds of the possibly rotated shape with respect to the viewport. vec2<> nePixel = {-INFINITY, -INFINITY}; vec2<> swPixel = {INFINITY, INFINITY}; - for (LatLng latLng : segment) { + for (LatLng latLng : latLngs) { vec2<> pixel = pixelForLatLng(latLng); swPixel.x = std::min(swPixel.x, pixel.x); nePixel.x = std::max(nePixel.x, pixel.x); @@ -214,7 +230,9 @@ void Map::fitBounds(AnnotationSegment segment, EdgeInsets padding, const Duratio vec2<> centerPixel = (paddedNEPixel + paddedSWPixel) * 0.5; LatLng centerLatLng = latLngForPixel(centerPixel); - setLatLngZoom(centerLatLng, zoom, duration); + options.center = centerLatLng; + options.zoom = zoom; + return options; } void Map::resetZoom() { @@ -270,8 +288,8 @@ void Map::resetNorth() { #pragma mark - Pitch -void Map::setPitch(double pitch) { - transform->setPitch(std::min(pitch, 60.0) * M_PI / 180); +void Map::setPitch(double pitch, const Duration& duration) { + transform->setPitch(util::clamp(pitch, 0., 60.) * M_PI / 180, duration); update(Update::Repaint); } diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 60c55c3f0..bb5590914 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -53,6 +54,38 @@ bool Transform::resize(const std::array size) { #pragma mark - Position +void Transform::jumpTo(const CameraOptions options) { + CameraOptions jumpOptions = options; + jumpOptions.duration.reset(); + easeTo(jumpOptions); +} + +void Transform::easeTo(CameraOptions options) { + LatLng latLng = options.center ? *options.center : getLatLng(); + double zoom = options.zoom ? *options.zoom : getZoom(); + double angle = options.angle ? *options.angle : getAngle(); + if (std::isnan(latLng.latitude) || std::isnan(latLng.longitude) || std::isnan(zoom)) { + return; + } + + double new_scale = std::pow(2.0, zoom); + + const double s = new_scale * util::tileSize; + state.Bc = s / 360; + state.Cc = s / util::M2PI; + + const double m = 1 - 1e-15; + const double f = std::fmin(std::fmax(std::sin(util::DEG2RAD * latLng.latitude), -m), m); + + double xn = -latLng.longitude * state.Bc; + double yn = 0.5 * state.Cc * std::log((1 + f) / (1 - f)); + + options.center.reset(); + options.zoom.reset(); + options.angle.reset(); + _easeTo(options, new_scale, angle, xn, yn); +} + void Transform::moveBy(const double dx, const double dy, const Duration& duration) { if (std::isnan(dx) || std::isnan(dy)) { return; @@ -62,71 +95,30 @@ void Transform::moveBy(const double dx, const double dy, const Duration& duratio } void Transform::_moveBy(const double dx, const double dy, const Duration& duration) { + double x = state.x + std::cos(state.angle) * dx + std::sin( state.angle) * dy; double y = state.y + std::cos(state.angle) * dy + std::sin(-state.angle) * dx; state.constrain(state.scale, y); - - if (duration == Duration::zero()) { - view.notifyMapChange(MapChangeRegionWillChange); - - state.x = x; - state.y = y; - - view.notifyMapChange(MapChangeRegionDidChange); - } else { - view.notifyMapChange(MapChangeRegionWillChangeAnimated); - - const double startX = state.x; - const double startY = state.y; - state.panning = true; - - startTransition( - [=](double t) { - state.x = util::interpolate(startX, x, t); - state.y = util::interpolate(startY, y, t); - view.notifyMapChange(MapChangeRegionIsChanging); - return Update::Repaint; - }, - [=] { - state.panning = false; - view.notifyMapChange(MapChangeRegionDidChangeAnimated); - }, duration); - } + + CameraOptions options; + options.duration = duration; + _easeTo(options, state.scale, state.angle, x, y); } void Transform::setLatLng(const LatLng latLng, const Duration& duration) { - if (std::isnan(latLng.latitude) || std::isnan(latLng.longitude)) { - return; - } - - const double m = 1 - 1e-15; - const double f = ::fmin(::fmax(std::sin(util::DEG2RAD * latLng.latitude), -m), m); - - double xn = -latLng.longitude * state.Bc; - double yn = 0.5 * state.Cc * std::log((1 + f) / (1 - f)); - - _setScaleXY(state.scale, xn, yn, duration); + CameraOptions options; + options.center = latLng; + options.duration = duration; + easeTo(options); } void Transform::setLatLngZoom(const LatLng latLng, const double zoom, const Duration& duration) { - if (std::isnan(latLng.latitude) || std::isnan(latLng.longitude) || std::isnan(zoom)) { - return; - } - - double new_scale = std::pow(2.0, zoom); - - const double s = new_scale * util::tileSize; - state.Bc = s / 360; - state.Cc = s / util::M2PI; - - const double m = 1 - 1e-15; - const double f = ::fmin(::fmax(std::sin(util::DEG2RAD * latLng.latitude), -m), m); - - double xn = -latLng.longitude * state.Bc; - double yn = 0.5 * state.Cc * std::log((1 + f) / (1 - f)); - - _setScaleXY(new_scale, xn, yn, duration); + CameraOptions options; + options.center = latLng; + options.zoom = zoom; + options.duration = duration; + easeTo(options); } @@ -206,13 +198,27 @@ void Transform::_setScale(double new_scale, double cx, double cy, const Duration void Transform::_setScaleXY(const double new_scale, const double xn, const double yn, const Duration& duration) { + CameraOptions options; + options.duration = duration; + _easeTo(options, new_scale, state.angle, xn, yn); +} + +void Transform::_easeTo(CameraOptions options, const double new_scale, const double new_angle, const double xn, const double yn) { + Update update = state.scale == new_scale ? Update::Repaint : Update::Zoom; double scale = new_scale; double x = xn; double y = yn; state.constrain(scale, y); + + double angle = _normalizeAngle(new_angle, state.angle); + state.angle = _normalizeAngle(state.angle, angle); + double pitch = options.pitch ? *options.pitch : state.pitch; - if (duration == Duration::zero()) { + if (!options.duration) { + options.duration = Duration::zero(); + } + if (!options.duration || *options.duration == Duration::zero()) { view.notifyMapChange(MapChangeRegionWillChange); state.scale = scale; @@ -221,18 +227,28 @@ void Transform::_setScaleXY(const double new_scale, const double xn, const doubl const double s = state.scale * util::tileSize; state.Bc = s / 360; state.Cc = s / util::M2PI; + + state.angle = angle; + state.pitch = pitch; view.notifyMapChange(MapChangeRegionDidChange); } else { view.notifyMapChange(MapChangeRegionWillChangeAnimated); const double startS = state.scale; + const double startA = state.angle; + const double startP = state.pitch; const double startX = state.x; const double startY = state.y; state.panning = true; state.scaling = true; + state.rotating = true; startTransition( + [=](double t) { + util::UnitBezier ease = options.easing ? *options.easing : util::UnitBezier(0, 0, 0.25, 1); + return ease.solve(t, 0.001); + }, [=](double t) { state.scale = util::interpolate(startS, scale, t); state.x = util::interpolate(startX, x, t); @@ -240,14 +256,17 @@ void Transform::_setScaleXY(const double new_scale, const double xn, const doubl const double s = state.scale * util::tileSize; state.Bc = s / 360; state.Cc = s / util::M2PI; + state.angle = util::wrap(util::interpolate(startA, angle, t), -M_PI, M_PI); + state.pitch = util::interpolate(startP, pitch, t); view.notifyMapChange(MapChangeRegionIsChanging); - return Update::Zoom; + return update; }, [=] { state.panning = false; state.scaling = false; + state.rotating = false; view.notifyMapChange(MapChangeRegionDidChangeAnimated); - }, duration); + }, *options.duration); } } @@ -307,7 +326,7 @@ void Transform::setAngle(const double new_angle, const double cx, const double c _moveBy(dx, dy, Duration::zero()); } - _setAngle(new_angle, Duration::zero()); + _setAngle(new_angle); if (cx >= 0 && cy >= 0) { _moveBy(-dx, -dy, Duration::zero()); @@ -315,32 +334,10 @@ void Transform::setAngle(const double new_angle, const double cx, const double c } void Transform::_setAngle(double new_angle, const Duration& duration) { - double angle = _normalizeAngle(new_angle, state.angle); - state.angle = _normalizeAngle(state.angle, angle); - - if (duration == Duration::zero()) { - view.notifyMapChange(MapChangeRegionWillChange); - - state.angle = angle; - - view.notifyMapChange(MapChangeRegionDidChange); - } else { - view.notifyMapChange(MapChangeRegionWillChangeAnimated); - - const double startA = state.angle; - state.rotating = true; - - startTransition( - [=](double t) { - state.angle = util::wrap(util::interpolate(startA, angle, t), -M_PI, M_PI); - view.notifyMapChange(MapChangeRegionIsChanging); - return Update::Repaint; - }, - [=] { - state.rotating = false; - view.notifyMapChange(MapChangeRegionDidChangeAnimated); - }, duration); - } + CameraOptions options; + options.angle = new_angle; + options.duration = duration; + easeTo(options); } double Transform::getAngle() const { @@ -349,8 +346,11 @@ double Transform::getAngle() const { #pragma mark - Pitch -void Transform::setPitch(double pitch) { - state.pitch = pitch; +void Transform::setPitch(double pitch, const Duration& duration) { + CameraOptions options; + options.pitch = pitch; + options.duration = duration; + easeTo(options); } double Transform::getPitch() const { @@ -359,7 +359,8 @@ double Transform::getPitch() const { #pragma mark - Transition -void Transform::startTransition(std::function frame, +void Transform::startTransition(std::function easing, + std::function frame, std::function finish, const Duration& duration) { if (transitionFinishFn) { @@ -369,7 +370,7 @@ void Transform::startTransition(std::function frame, transitionStart = Clock::now(); transitionDuration = duration; - transitionFrameFn = [frame, this](const TimePoint now) { + transitionFrameFn = [easing, frame, this](const TimePoint now) { float t = std::chrono::duration(now - transitionStart) / transitionDuration; if (t >= 1.0) { Update result = frame(1.0); @@ -378,8 +379,7 @@ void Transform::startTransition(std::function frame, transitionFinishFn = nullptr; return result; } else { - util::UnitBezier ease(0, 0, 0.25, 1); - return frame(ease.solve(t, 0.001)); + return frame(easing(t)); } }; diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 167198344..56001fad8 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -2,6 +2,7 @@ #define MBGL_MAP_TRANSFORM #include +#include #include #include #include @@ -22,6 +23,9 @@ public: // Map view bool resize(std::array size); + void jumpTo(const CameraOptions options); + void easeTo(const CameraOptions options); + // Position void moveBy(double dx, double dy, const Duration& = Duration::zero()); void setLatLng(LatLng latLng, const Duration& = Duration::zero()); @@ -42,7 +46,7 @@ public: double getAngle() const; // Pitch - void setPitch(double pitch); + void setPitch(double pitch, const Duration& = Duration::zero()); double getPitch() const; // Transitions @@ -60,13 +64,16 @@ private: void _moveBy(double dx, double dy, const Duration& = Duration::zero()); void _setScale(double scale, double cx, double cy, const Duration& = Duration::zero()); void _setScaleXY(double new_scale, double xn, double yn, const Duration& = Duration::zero()); + void _easeTo(CameraOptions options, const double new_scale, const double new_angle, + const double xn, const double yn); void _setAngle(double angle, const Duration& = Duration::zero()); View &view; TransformState state; - void startTransition(std::function frame, + void startTransition(std::function easing, + std::function frame, std::function finish, const Duration& duration);