diff options
author | John Firebaugh <john.firebaugh@gmail.com> | 2015-12-11 12:39:43 -0800 |
---|---|---|
committer | John Firebaugh <john.firebaugh@gmail.com> | 2015-12-16 14:52:29 -0800 |
commit | 13d5b18ebaeb330f05c4abfc04e0247222e038ba (patch) | |
tree | 90e24f1fb745581767b6979d1c562e60859cbcf6 /platform/android/MapboxGLAndroidSDK/src/main/java/com | |
parent | 0db9f229f2bb14e2c6ccd3a8c0d0a30eb25ea5c5 (diff) | |
download | qtlocation-mapboxgl-13d5b18ebaeb330f05c4abfc04e0247222e038ba.tar.gz |
[android] Move into platform subdirectory
Diffstat (limited to 'platform/android/MapboxGLAndroidSDK/src/main/java/com')
56 files changed, 9276 insertions, 0 deletions
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/BaseGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/BaseGestureDetector.java new file mode 100644 index 0000000000..622e8f759c --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/BaseGestureDetector.java @@ -0,0 +1,161 @@ +package com.almeros.android.multitouch.gesturedetectors; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) Copyright (c) 2013, Almer Thie + * (code.almeros.com) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +public abstract class BaseGestureDetector { + protected final Context mContext; + protected boolean mGestureInProgress; + + protected MotionEvent mPrevEvent; + protected MotionEvent mCurrEvent; + + protected float mCurrPressure; + protected float mPrevPressure; + protected long mTimeDelta; + + /** + * This value is the threshold ratio between the previous combined pressure + * and the current combined pressure. When pressure decreases rapidly + * between events the position values can often be imprecise, as it usually + * indicates that the user is in the process of lifting a pointer off of the + * device. This value was tuned experimentally. + */ + protected static final float PRESSURE_THRESHOLD = 0.67f; + + public BaseGestureDetector(Context context) { + mContext = context; + } + + /** + * All gesture detectors need to be called through this method to be able to + * detect gestures. This method delegates work to handler methods + * (handleStartProgressEvent, handleInProgressEvent) implemented in + * extending classes. + * + * @param event MotionEvent + * @return {@code true} as handled + */ + public boolean onTouchEvent(MotionEvent event) { + final int actionCode = event.getAction() & MotionEvent.ACTION_MASK; + if (!mGestureInProgress) { + handleStartProgressEvent(actionCode, event); + } else { + handleInProgressEvent(actionCode, event); + } + return true; + } + + /** + * Called when the current event occurred when NO gesture is in progress + * yet. The handling in this implementation may set the gesture in progress + * (via mGestureInProgress) or out of progress + * + * @param actionCode Action Code from MotionEvent + * @param event MotionEvent + */ + protected abstract void handleStartProgressEvent(int actionCode, + MotionEvent event); + + /** + * Called when the current event occurred when a gesture IS in progress. The + * handling in this implementation may set the gesture out of progress (via + * mGestureInProgress). + * + * + * @param actionCode Action Code from MotionEvent + * @param event MotionEvent + */ + protected abstract void handleInProgressEvent(int actionCode, + MotionEvent event); + + protected void updateStateByEvent(MotionEvent curr) { + final MotionEvent prev = mPrevEvent; + + // Reset mCurrEvent + if (mCurrEvent != null) { + mCurrEvent.recycle(); + mCurrEvent = null; + } + mCurrEvent = MotionEvent.obtain(curr); + + // Delta time + mTimeDelta = curr.getEventTime() - prev.getEventTime(); + + // Pressure + mCurrPressure = curr.getPressure(curr.getActionIndex()); + mPrevPressure = prev.getPressure(prev.getActionIndex()); + } + + protected void resetState() { + if (mPrevEvent != null) { + mPrevEvent.recycle(); + mPrevEvent = null; + } + if (mCurrEvent != null) { + mCurrEvent.recycle(); + mCurrEvent = null; + } + mGestureInProgress = false; + } + + /** + * Returns {@code true} if a gesture is currently in progress. + * + * @return {@code true} if a gesture is currently in progress, {@code false} + * otherwise. + */ + public boolean isInProgress() { + return mGestureInProgress; + } + + /** + * Return the time difference in milliseconds between the previous accepted + * GestureDetector event and the current GestureDetector event. + * + * @return Time difference since the last move event in milliseconds. + */ + public long getTimeDelta() { + return mTimeDelta; + } + + /** + * Return the event time of the current GestureDetector event being + * processed. + * + * @return Current GestureDetector event time in milliseconds. + */ + public long getEventTime() { + return mCurrEvent.getEventTime(); + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/MoveGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/MoveGestureDetector.java new file mode 100644 index 0000000000..2430f3f920 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/MoveGestureDetector.java @@ -0,0 +1,185 @@ +package com.almeros.android.multitouch.gesturedetectors; + +import android.content.Context; +import android.graphics.PointF; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) Copyright (c) 2013, Almer Thie + * (code.almeros.com) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +public class MoveGestureDetector extends BaseGestureDetector { + + /** + * Listener which must be implemented which is used by MoveGestureDetector + * to perform callbacks to any implementing class which is registered to a + * MoveGestureDetector via the constructor. + * + * @see MoveGestureDetector.SimpleOnMoveGestureListener + */ + public interface OnMoveGestureListener { + public boolean onMove(MoveGestureDetector detector); + + public boolean onMoveBegin(MoveGestureDetector detector); + + public void onMoveEnd(MoveGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods of + * OnMoveGestureListener. + */ + public static class SimpleOnMoveGestureListener implements + OnMoveGestureListener { + public boolean onMove(MoveGestureDetector detector) { + return false; + } + + public boolean onMoveBegin(MoveGestureDetector detector) { + return true; + } + + public void onMoveEnd(MoveGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } + + private static final PointF FOCUS_DELTA_ZERO = new PointF(); + + private final OnMoveGestureListener mListener; + + private PointF mFocusExternal = new PointF(); + private PointF mFocusDeltaExternal = new PointF(); + + public MoveGestureDetector(Context context, OnMoveGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_DOWN: + resetState(); // In case we missed an UP/CANCEL event + + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + break; + + case MotionEvent.ACTION_MOVE: + mGestureInProgress = mListener.onMoveBegin(this); + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mListener.onMoveEnd(this); + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { + final boolean updatePrevious = mListener.onMove(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + protected void updateStateByEvent(MotionEvent curr) { + super.updateStateByEvent(curr); + + final MotionEvent prev = mPrevEvent; + + // Focus intenal + PointF mCurrFocusInternal = determineFocalPoint(curr); + PointF mPrevFocusInternal = determineFocalPoint(prev); + + // Focus external + // - Prevent skipping of focus delta when a finger is added or removed + boolean mSkipNextMoveEvent = prev.getPointerCount() != curr + .getPointerCount(); + mFocusDeltaExternal = mSkipNextMoveEvent ? FOCUS_DELTA_ZERO + : new PointF(mCurrFocusInternal.x - mPrevFocusInternal.x, + mCurrFocusInternal.y - mPrevFocusInternal.y); + + // - Don't directly use mFocusInternal (or skipping will occur). Add + // unskipped delta values to mFocusExternal instead. + mFocusExternal.x += mFocusDeltaExternal.x; + mFocusExternal.y += mFocusDeltaExternal.y; + } + + /** + * Determine (multi)finger focal point (a.k.a. center point between all + * fingers) + * + * @param e + * @return PointF focal point + */ + private PointF determineFocalPoint(MotionEvent e) { + // Number of fingers on screen + final int pCount = e.getPointerCount(); + float x = 0.0f; + float y = 0.0f; + + for (int i = 0; i < pCount; i++) { + x += e.getX(i); + y += e.getY(i); + } + + return new PointF(x / pCount, y / pCount); + } + + public float getFocusX() { + return mFocusExternal.x; + } + + public float getFocusY() { + return mFocusExternal.y; + } + + public PointF getFocusDelta() { + return mFocusDeltaExternal; + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/RotateGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/RotateGestureDetector.java new file mode 100644 index 0000000000..124fe8509c --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/RotateGestureDetector.java @@ -0,0 +1,180 @@ +package com.almeros.android.multitouch.gesturedetectors; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) Copyright (c) 2013, Almer Thie + * (code.almeros.com) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +public class RotateGestureDetector extends TwoFingerGestureDetector { + + /** + * Listener which must be implemented which is used by RotateGestureDetector + * to perform callbacks to any implementing class which is registered to a + * RotateGestureDetector via the constructor. + * + * @see RotateGestureDetector.SimpleOnRotateGestureListener + */ + public interface OnRotateGestureListener { + public boolean onRotate(RotateGestureDetector detector); + + public boolean onRotateBegin(RotateGestureDetector detector); + + public void onRotateEnd(RotateGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods of + * OnRotateGestureListener. + */ + public static class SimpleOnRotateGestureListener implements + OnRotateGestureListener { + public boolean onRotate(RotateGestureDetector detector) { + return false; + } + + public boolean onRotateBegin(RotateGestureDetector detector) { + return true; + } + + public void onRotateEnd(RotateGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } + + private final OnRotateGestureListener mListener; + private boolean mSloppyGesture; + + public RotateGestureDetector(Context context, + OnRotateGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_DOWN: + // At least the second finger is on screen now + + resetState(); // In case we missed an UP/CANCEL event + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + + // See if we have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start gesture now + mGestureInProgress = mListener.onRotateBegin(this); + } + break; + + case MotionEvent.ACTION_MOVE: + if (!mSloppyGesture) { + break; + } + + // See if we still have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start normal gesture now + mGestureInProgress = mListener.onRotateBegin(this); + } + + break; + + case MotionEvent.ACTION_POINTER_UP: + if (!mSloppyGesture) { + break; + } + + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_UP: + // Gesture ended but + updateStateByEvent(event); + + if (!mSloppyGesture) { + mListener.onRotateEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_CANCEL: + if (!mSloppyGesture) { + mListener.onRotateEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { + final boolean updatePrevious = mListener.onRotate(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + @Override + protected void resetState() { + super.resetState(); + mSloppyGesture = false; + } + + /** + * Return the rotation difference from the previous rotate event to the + * current event. + * + * @return The current rotation //difference in degrees. + */ + public float getRotationDegreesDelta() { + double diffRadians = Math.atan2(mPrevFingerDiffY, mPrevFingerDiffX) + - Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX); + return (float) (diffRadians * 180.0 / Math.PI); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/ShoveGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/ShoveGestureDetector.java new file mode 100644 index 0000000000..254597105b --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/ShoveGestureDetector.java @@ -0,0 +1,213 @@ +package com.almeros.android.multitouch.gesturedetectors; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * @author Robert Nordan (robert.nordan@norkart.no) + * + * Copyright (c) 2013, Norkart AS + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +public class ShoveGestureDetector extends TwoFingerGestureDetector { + + /** + * Listener which must be implemented which is used by ShoveGestureDetector + * to perform callbacks to any implementing class which is registered to a + * ShoveGestureDetector via the constructor. + * + * @see ShoveGestureDetector.SimpleOnShoveGestureListener + */ + public interface OnShoveGestureListener { + public boolean onShove(ShoveGestureDetector detector); + + public boolean onShoveBegin(ShoveGestureDetector detector); + + public void onShoveEnd(ShoveGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods of + * OnShoveGestureListener. + */ + public static class SimpleOnShoveGestureListener implements + OnShoveGestureListener { + public boolean onShove(ShoveGestureDetector detector) { + return false; + } + + public boolean onShoveBegin(ShoveGestureDetector detector) { + return true; + } + + public void onShoveEnd(ShoveGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } + + private float mPrevAverageY; + private float mCurrAverageY; + + private final OnShoveGestureListener mListener; + private boolean mSloppyGesture; + + public ShoveGestureDetector(Context context, OnShoveGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_DOWN: + // At least the second finger is on screen now + + resetState(); // In case we missed an UP/CANCEL event + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + + // See if we have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start gesture now + mGestureInProgress = mListener.onShoveBegin(this); + } + break; + + case MotionEvent.ACTION_MOVE: + if (!mSloppyGesture) { + break; + } + + // See if we still have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start normal gesture now + mGestureInProgress = mListener.onShoveBegin(this); + } + + break; + + case MotionEvent.ACTION_POINTER_UP: + if (!mSloppyGesture) { + break; + } + + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_UP: + // Gesture ended but + updateStateByEvent(event); + + if (!mSloppyGesture) { + mListener.onShoveEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_CANCEL: + if (!mSloppyGesture) { + mListener.onShoveEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. Also check that shove is meaningful. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD + && Math.abs(getShovePixelsDelta()) > 0.5f) { + final boolean updatePrevious = mListener.onShove(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + @Override + protected void resetState() { + super.resetState(); + mSloppyGesture = false; + mPrevAverageY = 0.0f; + mCurrAverageY = 0.0f; + } + + @Override + protected void updateStateByEvent(MotionEvent curr) { + super.updateStateByEvent(curr); + + final MotionEvent prev = mPrevEvent; + float py0 = prev.getY(0); + float py1 = prev.getY(1); + mPrevAverageY = (py0 + py1) / 2.0f; + + float cy0 = curr.getY(0); + float cy1 = curr.getY(1); + mCurrAverageY = (cy0 + cy1) / 2.0f; + } + + @Override + protected boolean isSloppyGesture(MotionEvent event) { + boolean sloppy = super.isSloppyGesture(event); + if (sloppy) + return true; + + // If it's not traditionally sloppy, we check if the angle between + // fingers + // is acceptable. + double angle = Math.abs(Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX)); + // about 20 degrees, left or right + return !((0.0f < angle && angle < 0.35f) || 2.79f < angle + && angle < Math.PI); + } + + /** + * Return the distance in pixels from the previous shove event to the + * current event. + * + * @return The current distance in pixels. + */ + public float getShovePixelsDelta() { + return mCurrAverageY - mPrevAverageY; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java new file mode 100644 index 0000000000..91e5ef13c8 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java @@ -0,0 +1,223 @@ +package com.almeros.android.multitouch.gesturedetectors; + +import android.content.Context; +import android.graphics.PointF; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * @author Almer Thie (code.almeros.com) Copyright (c) 2013, Almer Thie + * (code.almeros.com) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +public abstract class TwoFingerGestureDetector extends BaseGestureDetector { + + private final float mEdgeSlop; + + protected float mPrevFingerDiffX; + protected float mPrevFingerDiffY; + protected float mCurrFingerDiffX; + protected float mCurrFingerDiffY; + + private float mCurrLen; + private float mPrevLen; + + private PointF mFocus; + + public TwoFingerGestureDetector(Context context) { + super(context); + + ViewConfiguration config = ViewConfiguration.get(context); + mEdgeSlop = config.getScaledEdgeSlop(); + } + + @Override + protected abstract void handleStartProgressEvent(int actionCode, + MotionEvent event); + + @Override + protected abstract void handleInProgressEvent(int actionCode, + MotionEvent event); + + protected void updateStateByEvent(MotionEvent curr) { + super.updateStateByEvent(curr); + + final MotionEvent prev = mPrevEvent; + + mCurrLen = -1; + mPrevLen = -1; + + // Previous + final float px0 = prev.getX(0); + final float py0 = prev.getY(0); + final float px1 = prev.getX(1); + final float py1 = prev.getY(1); + final float pvx = px1 - px0; + final float pvy = py1 - py0; + mPrevFingerDiffX = pvx; + mPrevFingerDiffY = pvy; + + // Current + final float cx0 = curr.getX(0); + final float cy0 = curr.getY(0); + final float cx1 = curr.getX(1); + final float cy1 = curr.getY(1); + final float cvx = cx1 - cx0; + final float cvy = cy1 - cy0; + mCurrFingerDiffX = cvx; + mCurrFingerDiffY = cvy; + mFocus = determineFocalPoint(curr); + } + + /** + * Return the current distance between the two pointers forming the gesture + * in progress. + * + * @return Distance between pointers in pixels. + */ + public float getCurrentSpan() { + if (mCurrLen == -1) { + final float cvx = mCurrFingerDiffX; + final float cvy = mCurrFingerDiffY; + mCurrLen = (float) Math.sqrt(cvx * cvx + cvy * cvy); + } + return mCurrLen; + } + + /** + * Return the previous distance between the two pointers forming the gesture + * in progress. + * + * @return Previous distance between pointers in pixels. + */ + public float getPreviousSpan() { + if (mPrevLen == -1) { + final float pvx = mPrevFingerDiffX; + final float pvy = mPrevFingerDiffY; + mPrevLen = (float) Math.sqrt(pvx * pvx + pvy * pvy); + } + return mPrevLen; + } + + /** + * MotionEvent has no getRawX(int) method; simulate it pending future API + * approval. + * + * @param event Motion Event + * @param pointerIndex Pointer Index + * @return Raw x value or 0 + */ + protected static float getRawX(MotionEvent event, int pointerIndex) { + float offset = event.getX() - event.getRawX(); + if (pointerIndex < event.getPointerCount()) { + return event.getX(pointerIndex) + offset; + } + return 0.0f; + } + + /** + * MotionEvent has no getRawY(int) method; simulate it pending future API + * approval. + * + * @param event Motion Event + * @param pointerIndex Pointer Index + * @return Raw y value or 0 + */ + protected static float getRawY(MotionEvent event, int pointerIndex) { + float offset = event.getY() - event.getRawY(); + if (pointerIndex < event.getPointerCount()) { + return event.getY(pointerIndex) + offset; + } + return 0.0f; + } + + /** + * Check if we have a sloppy gesture. Sloppy gestures can happen if the edge + * of the user's hand is touching the screen, for example. + * + * @param event Motion Event + * @return {@code true} if is sloppy gesture, {@code false} if not + */ + protected boolean isSloppyGesture(MotionEvent event) { + // As orientation can change, query the metrics in touch down + DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); + float mRightSlopEdge = metrics.widthPixels - mEdgeSlop; + float mBottomSlopEdge = metrics.heightPixels - mEdgeSlop; + + final float edgeSlop = mEdgeSlop; + + final float x0 = event.getRawX(); + final float y0 = event.getRawY(); + final float x1 = getRawX(event, 1); + final float y1 = getRawY(event, 1); + + boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop || x0 > mRightSlopEdge + || y0 > mBottomSlopEdge; + boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop || x1 > mRightSlopEdge + || y1 > mBottomSlopEdge; + + if (p0sloppy && p1sloppy) { + return true; + } else if (p0sloppy) { + return true; + } else if (p1sloppy) { + return true; + } + return false; + } + + /** + * Determine (multi)finger focal point (a.k.a. center point between all + * fingers) + * + * @param e Motion Event + * @return PointF focal point + */ + public static PointF determineFocalPoint(MotionEvent e) { + // Number of fingers on screen + final int pCount = e.getPointerCount(); + float x = 0.0f; + float y = 0.0f; + + for (int i = 0; i < pCount; i++) { + x += e.getX(i); + y += e.getY(i); + } + + return new PointF(x / pCount, y / pCount); + } + + public float getFocusX() { + return mFocus.x; + } + + public float getFocusY() { + return mFocus.y; + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/package-info.java new file mode 100644 index 0000000000..abc0a11892 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/package-info.java @@ -0,0 +1,4 @@ +/** + * Do not use this package. Used internally by the SDK. + */ +package com.almeros.android.multitouch.gesturedetectors; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapFragment.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapFragment.java new file mode 100644 index 0000000000..9a1861ec19 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapFragment.java @@ -0,0 +1,129 @@ +package com.mapbox.mapboxsdk; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.mapbox.mapboxsdk.utils.ApiAccess; +import com.mapbox.mapboxsdk.views.MapView; + +public class MapFragment extends Fragment { + + // + // Static members + // + + // Tag used for logging + private static final String TAG = "MapFragment"; + + // + // Instance members + // + + // The map + private MapView mMap; + + // + // Lifecycle events + // + + // Called when the fragment is created + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + Log.v(TAG, "onCreateView"); + + // Create the map + mMap = (MapView) inflater.inflate(R.layout.fragment_mapview, container, false); + + // Set accessToken + mMap.setAccessToken(ApiAccess.getToken(container.getContext())); + + // Need to pass on any saved state to the map + mMap.onCreate(savedInstanceState); + + // Return the map as the root view + return mMap; + } + + // Called when the fragment is destroyed + @Override + public void onDestroyView() { + super.onDestroyView(); + Log.v(TAG, "onDestroyView"); + + // Need to pass on to view + mMap.onDestroy(); + mMap = null; + } + + // Called when the fragment is visible + @Override + public void onStart() { + super.onStart(); + Log.v(TAG, "onStart"); + + // Need to pass on to view + mMap.onStart(); + } + + // Called when the fragment is invisible + @Override + public void onStop() { + super.onStop(); + Log.v(TAG, "onStop"); + + // Need to pass on to view + mMap.onStop(); + } + + // Called when the fragment is in the background + @Override + public void onPause() { + super.onPause(); + Log.v(TAG, "onPause"); + + // Need to pass on to view + mMap.onPause(); + } + + // Called when the fragment is no longer in the background + @Override + public void onResume() { + super.onResume(); + Log.v(TAG, "onResume"); + + // Need to pass on to view + mMap.onResume(); + } + + // Called before fragment is destroyed + @Override + public void onSaveInstanceState(Bundle outState) { + Log.v(TAG, "onSaveInstanceState"); + + // Need to retrieve any saved state from the map + mMap.onSaveInstanceState(outState); + super.onSaveInstanceState(outState); + } + + @Override + public void onLowMemory() { + Log.v(TAG, "OnLowMemory"); + + // Need to pass on to view + mMap.onLowMemory(); + super.onLowMemory(); + } + + // + // Property methods + // + + public MapView getMap() { + return mMap; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java new file mode 100644 index 0000000000..bf89998788 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java @@ -0,0 +1,92 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.views.MapView; + +/** + * Annotation is the most general kind of overlay on top of a map, + * from which {@link InfoWindow} and {@link Marker} are derived: it manages + * attachment to a map and identification, but does not require + * content to be placed at a geographical point. + */ +public abstract class Annotation implements Comparable<Annotation> { + + /** + * <p> + * The annotation id + * </p> + * Internal C++ id is stored as unsigned int. + */ + private long id = -1; // -1 unless added to a MapView + private MapView mapView; + + protected Annotation() { + } + + /** + * <p> + * Gets the annotation's unique ID. + * </p> + * This ID is unique for a MapView instance and is suitable for associating your own extra + * data with. + */ + public long getId() { + return id; + } + + public void remove() { + if (mapView == null) { + return; + } + mapView.removeAnnotation(this); + } + + /** + * Do not use this method. Used internally by the SDK. + */ + public void setId(long id) { + this.id = id; + } + + /** + * Do not use this method. Used internally by the SDK. + */ + public void setMapView(MapView mapView) { + this.mapView = mapView; + } + + protected MapView getMapView() { + if (mapView == null) { + return null; + } + return mapView; + } + + @Override + public int compareTo(@NonNull Annotation annotation) { + if (id < annotation.getId()) { + return 1; + } else if (id > annotation.getId()) { + return -1; + } + + // Equal + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Annotation that = (Annotation) o; + + return getId() == that.getId(); + } + + @Override + public int hashCode() { + return (int) (getId() ^ (getId() >>> 32)); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java new file mode 100644 index 0000000000..e0433f00d1 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java @@ -0,0 +1,251 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.content.Context; +import android.content.res.Resources; +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.mapboxsdk.R; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.views.MapView; + +import java.lang.ref.WeakReference; + +/** + * <p> + * A tooltip view. This is a UI element placed over a map at a specific geographic + * location. + * </p> + */ +public class InfoWindow { + + private WeakReference<Marker> mBoundMarker; + private WeakReference<MapView> mMapView; + private float mMarkerHeightOffset; + private float mViewWidthOffset; + private PointF mCoordinates; + private boolean mIsVisible; + protected View mView; + + static int mTitleId = 0; + static int mDescriptionId = 0; + static int mSubDescriptionId = 0; + static int mImageId = 0; + + InfoWindow(int layoutResId, MapView mapView) { + View view = LayoutInflater.from(mapView.getContext()).inflate(layoutResId, mapView, false); + + if (mTitleId == 0) { + setResIds(mapView.getContext()); + } + + initialize(view, mapView); + } + + InfoWindow(View view, MapView mapView) { + initialize(view, mapView); + } + + private void initialize(View view, MapView mapView) { + mMapView = new WeakReference<>(mapView); + mIsVisible = false; + mView = view; + + // default behavior: close it when clicking on the tooltip: + mView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_UP) { + boolean handledDefaultClick = false; + MapView.OnInfoWindowClickListener onInfoWindowClickListener = + mMapView.get().getOnInfoWindowClickListener(); + if (onInfoWindowClickListener != null) { + handledDefaultClick = onInfoWindowClickListener.onMarkerClick(getBoundMarker()); + } + + if (!handledDefaultClick) { + close(); + } + } + return true; + } + }); + } + + + /** + * open the window at the specified position. + * + * @param boundMarker the marker 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 + */ + InfoWindow open(Marker boundMarker, LatLng position, int offsetX, int offsetY) { + setBoundMarker(boundMarker); + + MapView.LayoutParams lp = new MapView.LayoutParams(MapView.LayoutParams.WRAP_CONTENT, MapView.LayoutParams.WRAP_CONTENT); + mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + + // Calculate y-offset for update method + mMarkerHeightOffset = -mView.getMeasuredHeight() + offsetY; + + // Calculate default Android x,y coordinate + mCoordinates = mMapView.get().toScreenLocation(position); + float x = mCoordinates.x - (mView.getMeasuredWidth() / 2) + offsetX; + float y = mCoordinates.y - mView.getMeasuredHeight() + offsetY; + + if (mView instanceof InfoWindowView) { + // only apply repositioning/margin for InfoWindowView + Resources resources = mMapView.get().getContext().getResources(); + + // get right/left popup window + float rightSideInfowWindow = x + mView.getMeasuredWidth(); + float leftSideInfoWindow = x; + + // get right/left map view + final float mapRight = mMapView.get().getRight(); + final float mapLeft = mMapView.get().getLeft(); + + float marginHorizontal = resources.getDimension(R.dimen.infowindow_margin); + float tipViewOffset = resources.getDimension(R.dimen.infowindow_tipview_width) / 2; + float tipViewMarginLeft = mView.getMeasuredWidth() / 2 - tipViewOffset; + + boolean outOfBoundsLeft = false; + boolean outOfBoundsRight = false; + + // if out of bounds right + if (rightSideInfowWindow > mapRight) { + outOfBoundsRight = true; + x -= rightSideInfowWindow - mapRight; + tipViewMarginLeft += rightSideInfowWindow - mapRight + tipViewOffset; + rightSideInfowWindow = x + mView.getMeasuredWidth(); + } + + // fit screen left + if (leftSideInfoWindow < mapLeft) { + outOfBoundsLeft = true; + x += mapLeft - leftSideInfoWindow; + tipViewMarginLeft -= mapLeft - leftSideInfoWindow + tipViewOffset; + leftSideInfoWindow = x; + } + + // Add margin right + if (outOfBoundsRight && mapRight - rightSideInfowWindow < marginHorizontal) { + x -= marginHorizontal - (mapRight - rightSideInfowWindow); + tipViewMarginLeft += marginHorizontal - (mapRight - rightSideInfowWindow) - tipViewOffset; + leftSideInfoWindow = x; + } + + // Add margin left + if (outOfBoundsLeft && leftSideInfoWindow - mapLeft < marginHorizontal) { + x += marginHorizontal - (leftSideInfoWindow - mapLeft); + tipViewMarginLeft -= (marginHorizontal - (leftSideInfoWindow - mapLeft)) - tipViewOffset; + } + + // Adjust tipView + InfoWindowView infoWindowView = (InfoWindowView) mView; + infoWindowView.setTipViewMarginLeft((int) tipViewMarginLeft); + } + + // set anchor popupwindowview + mView.setX(x); + mView.setY(y); + + // Calculate x-offset for update method + mViewWidthOffset = x - mCoordinates.x - offsetX; + + close(); //if it was already opened + mMapView.get().addView(mView, lp); + mIsVisible = true; + return this; + } + + /** + * Close this InfoWindow if it is visible, otherwise don't do anything. + * + * @return this info window + */ + InfoWindow close() { + if (mIsVisible) { + mIsVisible = false; + ((ViewGroup) mView.getParent()).removeView(mView); + setBoundMarker(null); + onClose(); + } + return this; + } + + /** + * 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 + */ + void adaptDefaultMarker(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); + } +*/ + } + + private void onClose() { + mMapView.get().deselectMarker(getBoundMarker()); + } + + InfoWindow setBoundMarker(Marker boundMarker) { + mBoundMarker = new WeakReference<>(boundMarker); + return this; + } + + Marker getBoundMarker() { + if (mBoundMarker == null) { + return null; + } + return mBoundMarker.get(); + } + + /** + * Given a context, set the resource ids for the layout + * of the InfoWindow. + * + * @param context the apps 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); + } + + public void update() { + MapView mapView = mMapView.get(); + Marker marker = mBoundMarker.get(); + if (mapView != null && marker != null) { + mCoordinates = mapView.toScreenLocation(marker.getPosition()); + mView.setX(mCoordinates.x + mViewWidthOffset); + mView.setY(mCoordinates.y + mMarkerHeightOffset); + } + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowTipView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowTipView.java new file mode 100644 index 0000000000..d2afafc59d --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowTipView.java @@ -0,0 +1,62 @@ +package com.mapbox.mapboxsdk.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; + +import com.mapbox.mapboxsdk.R; + +final class InfoWindowTipView extends View { + + private Paint mPaint; + private Path mPath; + private int mLineWidth; + + public InfoWindowTipView(Context context, AttributeSet attrs) { + super(context, attrs); + + mPath = new Path(); + mLineWidth = (int) context.getResources().getDimension(R.dimen.infowindow_line_width); + mPaint = new Paint(); + mPaint.setColor(Color.WHITE); + mPaint.setAntiAlias(true); + mPaint.setStrokeWidth(0.0f); + mPaint.setStyle(Paint.Style.FILL); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + int height = getMeasuredHeight(); + int width = getMeasuredWidth(); + + mPath.rewind(); + + this.mPaint.setColor(Color.WHITE); + this.mPaint.setAntiAlias(true); + this.mPaint.setStrokeWidth(0.0f); + this.mPaint.setStyle(Paint.Style.FILL); + + mPath.moveTo(0, 0); + mPath.lineTo(width, 0); + mPath.lineTo((width / 2), height); + mPath.lineTo(0, 0); + canvas.drawPath(mPath, this.mPaint); + + mPath.rewind(); + + this.mPaint.setColor(Color.parseColor("#C2C2C2")); + this.mPaint.setAntiAlias(true); + this.mPaint.setStrokeWidth(mLineWidth); + this.mPaint.setStyle(Paint.Style.STROKE); + + mPath.moveTo(0, 0); + mPath.lineTo(width / 2, height); + mPath.lineTo(width, 0); + canvas.drawPath(mPath, this.mPaint); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowView.java new file mode 100644 index 0000000000..80dc5931a7 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowView.java @@ -0,0 +1,38 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.RelativeLayout; + +import com.mapbox.mapboxsdk.R; + +class InfoWindowView extends RelativeLayout { + + private InfoWindowTipView mTipView; + + public InfoWindowView(Context context) { + this(context, null); + } + + public InfoWindowView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public InfoWindowView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(context); + } + + private void initialize(Context context) { + LayoutInflater.from(context).inflate(R.layout.infowindow_content, this); + mTipView = (InfoWindowTipView) findViewById(R.id.infowindow_tipview); + } + + void setTipViewMarginLeft(int marginLeft) { + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mTipView.getLayoutParams(); + layoutParams.leftMargin = marginLeft; + // This is a bit of a hack but prevents an occasional gap between the InfoWindow + layoutParams.topMargin = (int) getResources().getDimension(R.dimen.infowindow_offset); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java new file mode 100644 index 0000000000..0f626a2618 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java @@ -0,0 +1,158 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.support.annotation.Nullable; +import android.view.View; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.views.MapView; + +/** + * A marker is a map overlay that shows an icon image at a specific + * geographical location and can be associated with a {@link InfoWindow} + * that is shown when the marker is tapped. + */ +public final class Marker extends Annotation { + + private LatLng position; + private String snippet; + private Sprite icon; + private String title; + private InfoWindow infoWindow = null; + private boolean infoWindowShown = false; + private int topOffsetPixels; + + /** + * Constructor + */ + Marker() { + super(); + } + + public LatLng getPosition() { + return position; + } + + public String getSnippet() { + return snippet; + } + + public String getTitle() { + return title; + } + + /** + * Do not use this method. Used internally by the SDK. + */ + public void hideInfoWindow() { + if (infoWindow != null) { + infoWindow.close(); + } + infoWindowShown = false; + } + + /** + * Do not use this method. Used internally by the SDK. + */ + public boolean isInfoWindowShown() { + return infoWindowShown; + } + + void setPosition(LatLng position) { + this.position = position; + } + + void setSnippet(String snippet) { + this.snippet = snippet; + } + + /** + * Do not use this method. Used internally by the SDK. + */ + public void setIcon(@Nullable Sprite icon) { + this.icon = icon; + } + + public Sprite getIcon() { + return icon; + } + + void setTitle(String title) { + this.title = title; + } + + /** + * Do not use this method. Used internally by the SDK. + */ + public InfoWindow showInfoWindow() { + if (getMapView() == null) { + return null; + } + + MapView.InfoWindowAdapter infoWindowAdapter = getMapView().getInfoWindowAdapter(); + if (infoWindowAdapter != null) { + // end developer is using a custom InfoWindowAdapter + View content = infoWindowAdapter.getInfoWindow(this); + if (content != null) { + infoWindow = new InfoWindow(content, getMapView()); + showInfoWindow(infoWindow); + return infoWindow; + } + } + + getInfoWindow().adaptDefaultMarker(this); + return showInfoWindow(getInfoWindow()); + } + + private InfoWindow showInfoWindow(InfoWindow iw) { + iw.open(this, getPosition(), 0, topOffsetPixels); + infoWindowShown = true; + return iw; + } + + private InfoWindow getInfoWindow() { + if (infoWindow == null) { + infoWindow = new InfoWindow(R.layout.infowindow_view, getMapView()); + } + return infoWindow; + } + + /* + @Override + void setVisible(boolean visible) { + super.setVisible(visible); + if (!visible && infoWindowShown) { + hideInfoWindow(); + } + } + */ + + /** + * Do not use this method. Used internally by the SDK. + */ + public void setTopOffsetPixels(int topOffsetPixels) { + this.topOffsetPixels = topOffsetPixels; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + Marker marker = (Marker) o; + return !(getPosition() != null ? !getPosition().equals(marker.getPosition()) : marker.getPosition() != null); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (getPosition() != null ? getPosition().hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Marker [position[" + getPosition() + "]]"; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java new file mode 100644 index 0000000000..b5bb24a49d --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java @@ -0,0 +1,140 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; + +import com.mapbox.mapboxsdk.geometry.LatLng; + + +/** + * <p> + * A builder object for the options configuration {@link com.mapbox.mapboxsdk.annotations.Marker} and + * {@link com.mapbox.mapboxsdk.annotations.InfoWindow} + * instances on maps. + * </p> + * + * <h3>Example</h3> + * + * <pre> + * mMapView.addMarker(new MarkerOptions() + * .title("Intersection") + * .snippet("H St NW with 15th St NW") + * .position(new LatLng(38.9002073, -77.03364419))); + * </pre> + */ +public final class MarkerOptions implements Parcelable { + + public static final Parcelable.Creator<MarkerOptions> CREATOR + = new Parcelable.Creator<MarkerOptions>() { + public MarkerOptions createFromParcel(Parcel in) { + return new MarkerOptions(in); + } + + public MarkerOptions[] newArray(int size) { + return new MarkerOptions[size]; + } + }; + + private MarkerOptions(Parcel in) { + marker = new Marker(); + position((LatLng) in.readParcelable(LatLng.class.getClassLoader())); + snippet(in.readString()); + String spriteId = in.readString(); + Bitmap spriteBitmap = in.readParcelable(Bitmap.class.getClassLoader()); + Sprite icon = new Sprite(spriteId, spriteBitmap); + icon(icon); + title(in.readString()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeParcelable(getPosition(), flags); + out.writeString(getSnippet()); + out.writeString(getIcon().getId()); + out.writeParcelable(getIcon().getBitmap(), flags); + out.writeString(getTitle()); + } + + private Marker marker; + + public MarkerOptions() { + marker = new Marker(); + } + + /** + * Do not use this method. Used internally by the SDK. + */ + public Marker getMarker() { + return marker; + } + + public LatLng getPosition() { + return marker.getPosition(); + } + + public String getSnippet() { + return marker.getSnippet(); + } + + public String getTitle() { + return marker.getTitle(); + } + + public Sprite getIcon() { + return marker.getIcon(); + } + + public MarkerOptions position(LatLng position) { + marker.setPosition(position); + return this; + } + + public MarkerOptions snippet(String snippet) { + marker.setSnippet(snippet); + return this; + } + + public MarkerOptions icon(@Nullable Sprite icon) { + marker.setIcon(icon); + return this; + } + + public MarkerOptions title(String title) { + marker.setTitle(title); + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MarkerOptions marker = (MarkerOptions) o; + + if (getPosition() != null ? !getPosition().equals(marker.getPosition()) : marker.getPosition() != null) + return false; + if (getSnippet() != null ? !getSnippet().equals(marker.getSnippet()) : marker.getSnippet() != null) + return false; + if (getIcon() != null ? !getIcon().equals(marker.getIcon()) : marker.getIcon() != null) + return false; + return !(getTitle() != null ? !getTitle().equals(marker.getTitle()) : marker.getTitle() != null); + + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (getPosition() != null ? getPosition().hashCode() : 0); + result = 31 * result + (getSnippet() != null ? getSnippet().hashCode() : 0); + result = 31 * result + (getIcon() != null ? getIcon().hashCode() : 0); + result = 31 * result + (getTitle() != null ? getTitle().hashCode() : 0); + return result; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MultiPoint.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MultiPoint.java new file mode 100644 index 0000000000..5c1dfb119f --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MultiPoint.java @@ -0,0 +1,49 @@ +package com.mapbox.mapboxsdk.annotations; + +import com.mapbox.mapboxsdk.geometry.LatLng; + +import java.util.ArrayList; +import java.util.List; + +public abstract class MultiPoint extends Annotation { + + private List<LatLng> points; + private float alpha = 1.0f; + + protected MultiPoint() { + super(); + points = new ArrayList<>(); + } + + /** + * Returns a copy of the points. + * + * @return points - as a copy + */ + public List<LatLng> getPoints() { + return new ArrayList<>(points); + } + + /** + * Sets the points of this polyline. This method will take a copy + * of the points, so further mutations to points will have no effect + * on this polyline. + * + * @param points the points of the polyline + */ + void setPoints(List<LatLng> points) { + this.points = new ArrayList<>(points); + } + + void addPoint(LatLng point) { + points.add(point); + } + + public float getAlpha() { + return alpha; + } + + void setAlpha(float alpha) { + this.alpha = alpha; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java new file mode 100644 index 0000000000..4a07b16827 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java @@ -0,0 +1,37 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Color; + +import com.mapbox.mapboxsdk.geometry.LatLng; + +import java.util.ArrayList; +import java.util.List; + +/** + * Polygon is a geometry annotation that's a closed loop of coordinates. + */ +public final class Polygon extends MultiPoint { + + private int fillColor = Color.BLACK; // default fillColor is black + private int strokeColor = Color.BLACK; // default strokeColor is black + + Polygon() { + super(); + } + + public int getFillColor() { + return fillColor; + } + + public int getStrokeColor() { + return strokeColor; + } + + void setFillColor(int color) { + fillColor = color; + } + + void setStrokeColor(int color) { + strokeColor = color; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java new file mode 100644 index 0000000000..c716d10edf --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java @@ -0,0 +1,146 @@ +package com.mapbox.mapboxsdk.annotations; + + +import android.os.Parcel; +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.geometry.LatLng; + +import java.util.ArrayList; +import java.util.List; + +public final class PolygonOptions implements Parcelable { + + public static final Parcelable.Creator<PolygonOptions> CREATOR + = new Parcelable.Creator<PolygonOptions>() { + public PolygonOptions createFromParcel(Parcel in) { + return new PolygonOptions(in); + } + + public PolygonOptions[] newArray(int size) { + return new PolygonOptions[size]; + } + }; + + private PolygonOptions(Parcel in) { + polygon = new Polygon(); + ArrayList<LatLng> pointsList = new ArrayList<>(); + in.readList(pointsList, LatLng.class.getClassLoader()); + addAll(pointsList); + alpha(in.readFloat()); + fillColor(in.readInt()); + strokeColor(in.readInt()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeList(getPoints()); + out.writeFloat(getAlpha()); + out.writeInt(getFillColor()); + out.writeInt(getStrokeColor()); + } + + private Polygon polygon; + + public PolygonOptions() { + polygon = new Polygon(); + } + + public PolygonOptions add(LatLng point) { + polygon.addPoint(point); + return this; + } + + public PolygonOptions add(LatLng... points) { + for (LatLng point : points) { + add(point); + } + return this; + } + + public PolygonOptions addAll(Iterable<LatLng> points) { + for (LatLng point : points) { + add(point); + } + return this; + } + + public PolygonOptions alpha(float alpha) { + polygon.setAlpha(alpha); + return this; + } + + public float getAlpha() { + return polygon.getAlpha(); + } + + /** + * Sets the color of the polygon. + * + * @param color - the color in ARGB format + * @return PolygonOptions - the options object + */ + public PolygonOptions fillColor(int color) { + polygon.setFillColor(color); + return this; + } + + public int getFillColor() { + return polygon.getFillColor(); + } + + /** + * Do not use this method. Used internally by the SDK. + */ + public Polygon getPolygon() { + return polygon; + } + + /** + * Sets the color of the stroke of the polygon. + * + * @param color - the color in ARGB format + * @return PolygonOptions - the options object + */ + public PolygonOptions strokeColor(int color) { + polygon.setStrokeColor(color); + return this; + } + + public int getStrokeColor() { + return polygon.getStrokeColor(); + } + + public List<LatLng> getPoints() { + // the getter gives us a copy, which is the safe thing to do... + return polygon.getPoints(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PolygonOptions polygon = (PolygonOptions) o; + + if (Float.compare(polygon.getAlpha(), getAlpha()) != 0) return false; + if (getFillColor() != polygon.getFillColor()) return false; + if (getStrokeColor() != polygon.getStrokeColor()) return false; + return !(getPoints() != null ? !getPoints().equals(polygon.getPoints()) : polygon.getPoints() != null); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (getAlpha() != +0.0f ? Float.floatToIntBits(getAlpha()) : 0); + result = 31 * result + getFillColor(); + result = 31 * result + getStrokeColor(); + result = 31 * result + (getPoints() != null ? getPoints().hashCode() : 0); + return result; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java new file mode 100644 index 0000000000..cfaf0d21d9 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java @@ -0,0 +1,42 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Color; + +/** + * Polyline is a geometry feature with an unclosed list of coordinates drawn as a line + */ +public final class Polyline extends MultiPoint { + + private int color = Color.BLACK; // default color is black + private float width = 10; // As specified by Google API Docs (in pixels) + + Polyline() { + super(); + } + + public int getColor() { + return color; + } + + public float getWidth() { + return width; + } + + /** + * Sets the color of the polyline. + * + * @param color - the color in ARGB format + */ + void setColor(int color) { + this.color = color; + } + + /** + * Sets the width of the polyline. + * + * @param width in pixels + */ + void setWidth(float width) { + this.width = width; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java new file mode 100644 index 0000000000..d48858c7d9 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java @@ -0,0 +1,145 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.geometry.LatLng; + +import java.util.ArrayList; +import java.util.List; + +public final class PolylineOptions implements Parcelable { + + + public static final Parcelable.Creator<PolylineOptions> CREATOR + = new Parcelable.Creator<PolylineOptions>() { + public PolylineOptions createFromParcel(Parcel in) { + return new PolylineOptions(in); + } + + public PolylineOptions[] newArray(int size) { + return new PolylineOptions[size]; + } + }; + + private PolylineOptions(Parcel in) { + polyline = new Polyline(); + ArrayList<LatLng> pointsList = new ArrayList<>(); + in.readList(pointsList, LatLng.class.getClassLoader()); + addAll(pointsList); + alpha(in.readFloat()); + color(in.readInt()); + width(in.readFloat()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeList(getPoints()); + out.writeFloat(getAlpha()); + out.writeInt(getColor()); + out.writeFloat(getWidth()); + } + + private Polyline polyline; + + public PolylineOptions() { + polyline = new Polyline(); + } + + public PolylineOptions add(LatLng point) { + polyline.addPoint(point); + return this; + } + + public PolylineOptions add(LatLng... points) { + for (LatLng point : points) { + add(point); + } + return this; + } + + public PolylineOptions addAll(Iterable<LatLng> points) { + for (LatLng point : points) { + add(point); + } + return this; + } + + public PolylineOptions alpha(float alpha) { + polyline.setAlpha(alpha); + return this; + } + + public float getAlpha() { + return polyline.getAlpha(); + } + + /** + * Sets the color of the polyline. + * + * @param color - the color in ARGB format + */ + public PolylineOptions color(int color) { + polyline.setColor(color); + return this; + } + + public int getColor() { + return polyline.getColor(); + } + + /** + * Do not use this method. Used internally by the SDK. + */ + public Polyline getPolyline() { + return polyline; + } + + public float getWidth() { + return polyline.getWidth(); + } + + /** + * Sets the width of the polyline. + * + * @param width in pixels + * @return a new PolylineOptions + */ + public PolylineOptions width(float width) { + polyline.setWidth(width); + return this; + } + + public List<LatLng> getPoints() { + // the getter gives us a copy, which is the safe thing to do... + return polyline.getPoints(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PolylineOptions polyline = (PolylineOptions) o; + + if (Float.compare(polyline.getAlpha(), getAlpha()) != 0) return false; + if (getColor() != polyline.getColor()) return false; + if (Float.compare(polyline.getWidth(), getWidth()) != 0) return false; + return !(getPoints() != null ? !getPoints().equals(polyline.getPoints()) : polyline.getPoints() != null); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (getAlpha() != +0.0f ? Float.floatToIntBits(getAlpha()) : 0); + result = 31 * result + getColor(); + result = 31 * result + (getWidth() != +0.0f ? Float.floatToIntBits(getWidth()) : 0); + result = 31 * result + (getPoints() != null ? getPoints().hashCode() : 0); + return result; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Sprite.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Sprite.java new file mode 100644 index 0000000000..597c196d2a --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Sprite.java @@ -0,0 +1,40 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Bitmap; + +public final class Sprite { + private Bitmap mBitmap; + private String mId; + + Sprite(String id, Bitmap bitmap) { + mId = id; + mBitmap = bitmap; + } + + public String getId() { + return mId; + } + + public Bitmap getBitmap() { + return mBitmap; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Sprite sprite = (Sprite) o; + + if (!mBitmap.equals(sprite.mBitmap)) return false; + return mId.equals(sprite.mId); + + } + + @Override + public int hashCode() { + int result = mBitmap.hashCode(); + result = 31 * result + mId.hashCode(); + return result; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/SpriteFactory.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/SpriteFactory.java new file mode 100644 index 0000000000..3395c5039d --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/SpriteFactory.java @@ -0,0 +1,132 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.WindowManager; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.exceptions.TooManySpritesException; +import com.mapbox.mapboxsdk.views.MapView; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public final class SpriteFactory { + + private static final String SPRITE_ID_PREFIX = "com.mapbox.sprites.sprite_"; + + private MapView mMapView; + private Sprite mDefaultMarker; + private BitmapFactory.Options mOptions; + + private int mNextId = 0; + + public SpriteFactory(MapView mapView) { + mMapView = mapView; + DisplayMetrics realMetrics = null; + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) mMapView.getContext().getSystemService(Context.WINDOW_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + realMetrics = new DisplayMetrics(); + wm.getDefaultDisplay().getRealMetrics(realMetrics); + } + wm.getDefaultDisplay().getMetrics(metrics); + + mOptions = new BitmapFactory.Options(); + mOptions.inScaled = true; + mOptions.inDensity = DisplayMetrics.DENSITY_DEFAULT; + mOptions.inTargetDensity = metrics.densityDpi; + if (realMetrics != null) { + mOptions.inScreenDensity = realMetrics.densityDpi; + } + + } + + public Sprite fromBitmap(Bitmap bitmap) { + if (bitmap == null) { + return null; + } + + if (mNextId < 0) { + throw new TooManySpritesException(); + } + String id = SPRITE_ID_PREFIX + ++mNextId; + + return new Sprite(id, bitmap); + } + + public Sprite fromDrawable(Drawable drawable) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + + return fromDrawable(drawable, width, height); + } + + + public Sprite fromDrawable(Drawable drawable, int width, int height) { + if ((width < 0) || (height < 0)) { + return null; + } + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Rect temp = drawable.getBounds(); + Rect bounds = new Rect(0, 0, width, height); + drawable.setBounds(bounds); + drawable.draw(canvas); + drawable.setBounds(temp); + + return fromBitmap(bitmap); + } + + public Sprite fromResource(int resourceId) { + Bitmap bitmap = BitmapFactory.decodeResource(mMapView.getResources(), resourceId); + return fromBitmap(bitmap); + } + + public Sprite defaultMarker() { + if (mDefaultMarker == null) { + mDefaultMarker = fromResource(R.drawable.default_marker); + } + return mDefaultMarker; + } + + private Sprite fromInputStream(InputStream is) { + Bitmap bitmap = BitmapFactory.decodeStream(is, null, mOptions); + return fromBitmap(bitmap); + } + + public Sprite fromAsset(String assetName) { + InputStream is; + try { + is = mMapView.getContext().getAssets().open(assetName); + } catch (IOException e) { + return null; + } + return fromInputStream(is); + } + + public Sprite fromPath(String absolutePath) { + Bitmap bitmap = BitmapFactory.decodeFile(absolutePath, mOptions); + return fromBitmap(bitmap); + } + + public Sprite fromFile(String fileName) { + FileInputStream is; + try { + is = mMapView.getContext().openFileInput(fileName); + } catch (FileNotFoundException e) { + return null; + } + return fromInputStream(is); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java new file mode 100644 index 0000000000..575d10c564 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains classes to add and manage annotations and markers in your map. + */ +package com.mapbox.mapboxsdk.annotations; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeoConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeoConstants.java new file mode 100644 index 0000000000..ed6f77f419 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeoConstants.java @@ -0,0 +1,38 @@ +package com.mapbox.mapboxsdk.constants; + +/** + * GeoConstants exposes constants for doing locational calculations on Earth + */ +public class GeoConstants { + + /** + * The <a href='http://en.wikipedia.org/wiki/Earth_radius#Equatorial_radius'>equatorial radius</a> + * value in meters + */ + public static final int RADIUS_EARTH_METERS = 6378137; + + /** + * The minimum latitude on Earth. This is the minimum latitude representable + * by Mapbox GL's Mercator projection, because the projection distorts latitude + * near the poles towards infinity. + */ + public static final double MIN_LATITUDE = -85.05112878; + + /** + * The maximum latitude on Earth. This is the maximum latitude representable + * by Mapbox GL's Mercator projection, because the projection distorts latitude + * near the poles towards infinity. + */ + public static final double MAX_LATITUDE = 85.05112878; + + /** + * The minimum longitude on Earth + */ + public static final double MIN_LONGITUDE = -180; + + /** + * The maximum longitude on Earth + */ + public static final double MAX_LONGITUDE = 180; + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java new file mode 100644 index 0000000000..33b3d46a63 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java @@ -0,0 +1,20 @@ +package com.mapbox.mapboxsdk.constants; + +import java.util.Locale; + +/** + * MapboxConstants exposes Mapbox related constants + */ +public class MapboxConstants { + + /** + * Default Locale for data processing (ex: String.toLowerCase(MAPBOX_LOCALE, "foo")) + */ + public static final Locale MAPBOX_LOCALE = Locale.US; + + /** + * Key used to store access token in AndroidManifest.xml + */ + public static final String KEY_META_DATA_MANIFEST = "com.mapbox.AccessToken"; + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MathConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MathConstants.java new file mode 100644 index 0000000000..6e320cc9f6 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MathConstants.java @@ -0,0 +1,22 @@ +package com.mapbox.mapboxsdk.constants; + +/** + * MathConstants exposes math related constant values + */ +public class MathConstants { + + /** + * Constant used to convert degrees to radials + */ + public static final double DEG2RAD = (Math.PI / 180.0); + + /** + * Constant used to convert radials to degrees + */ + public static final double RAD2DEG = (180.0 / Math.PI); + + /** + * The number PI + */ + public static final double PI = Math.PI; +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MyBearingTracking.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MyBearingTracking.java new file mode 100644 index 0000000000..a70d37dedc --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MyBearingTracking.java @@ -0,0 +1,43 @@ +package com.mapbox.mapboxsdk.constants; + +import android.support.annotation.IntDef; + +import com.mapbox.mapboxsdk.views.MapView; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * MyBearingTracking exposes different types bearing tracking modes. + * + * @see MapView#setMyBearingTrackingMode(int) + * @see com.mapbox.mapboxsdk.views.UserLocationView#setMyBearingTrackingMode(int) + */ +public class MyBearingTracking { + + /** + * Indicates the parameter accepts one of the values from {@link MyBearingTracking}. + */ + @IntDef({NONE, COMPASS, GPS, /**COMBINED**/}) + @Retention(RetentionPolicy.SOURCE) + public @interface Mode { + } + + /** + * Bearing tracking is disabled + */ + public static final int NONE = 0x00000000; + + /** + * Tracking the bearing of the user based on sensor data + */ + public static final int COMPASS = 0x00000004; + + /** + * Tracking the bearing of the user based on GPS data + */ + public static final int GPS = 0x00000008; + + //public static final int COMBINED = 0x00000012; + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MyLocationTracking.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MyLocationTracking.java new file mode 100644 index 0000000000..130ca36f76 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MyLocationTracking.java @@ -0,0 +1,36 @@ +package com.mapbox.mapboxsdk.constants; + +import android.support.annotation.IntDef; + +import com.mapbox.mapboxsdk.views.MapView; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * MyLocationTracking exposes different types of locational tracking modes. + * + * @see MapView#setMyLocationTrackingMode(int) + * @see com.mapbox.mapboxsdk.views.UserLocationView#setMyLocationTrackingMode(int) + */ +public class MyLocationTracking { + + /** + * Indicates the parameter accepts one of the values from {@link MyLocationTracking}. + */ + @IntDef({TRACKING_NONE, TRACKING_FOLLOW}) + @Retention(RetentionPolicy.SOURCE) + public @interface Mode { + } + + /** + * Location tracking is disabled. + */ + public static final int TRACKING_NONE = 0x00000000; + + /** + * Tracking the location of the user, {@link MapView} will reposition to center of {@link com.mapbox.mapboxsdk.views.UserLocationView} + */ + public static final int TRACKING_FOLLOW = 0x00000004; + +}
\ No newline at end of file diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/Style.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/Style.java new file mode 100644 index 0000000000..11ee9c3155 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/Style.java @@ -0,0 +1,55 @@ +package com.mapbox.mapboxsdk.constants; + +import android.support.annotation.StringDef; + +import com.mapbox.mapboxsdk.views.MapView; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +/** + * <p> + * Style provides URLs to several professional styles designed by Mapbox. + * </p> + * These styles are all ready to go in your app. To load one, pass it into {@link MapView#setStyleUrl(String)} + * + * @see MapView#setStyleUrl(String) + */ +public class Style { + + /** + * Indicates the parameter accepts one of the values from {@link Style}. + */ + @StringDef({MAPBOX_STREETS, EMERALD, LIGHT, DARK, SATELLITE, SATELLITE_STREETS}) + @Retention(RetentionPolicy.SOURCE) + public @interface StyleUrl { + } + + // IMPORTANT: If you change any of these you also need to edit them in strings.xml + + /** + * Mapbox Streets: A complete basemap, perfect for incorporating your own data. + */ + public static final String MAPBOX_STREETS = "mapbox://styles/mapbox/streets-v8"; + /** + * Emerald: A versatile style, with emphasis on road networks and public transit. + */ + public static final String EMERALD = "mapbox://styles/mapbox/emerald-v8"; + /** + * Light: Subtle light backdrop for data visualizations. + */ + public static final String LIGHT = "mapbox://styles/mapbox/light-v8"; + /** + * Dark: Subtle dark backdrop for data visualizations. + */ + public static final String DARK = "mapbox://styles/mapbox/dark-v8"; + /** + * Satellite: A beautiful global satellite and aerial imagery layer. + */ + public static final String SATELLITE = "mapbox://styles/mapbox/satellite-v8"; + + /** + * Satellite Streets: Global satellite and aerial imagery with unobtrusive labels. + */ + public static final String SATELLITE_STREETS = "mapbox://styles/mapbox/satellite-hybrid-v8"; + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java new file mode 100644 index 0000000000..b99119db0a --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java @@ -0,0 +1,4 @@ +/** + * This package provides access to several map related constants and bundled styles. + */ +package com.mapbox.mapboxsdk.constants; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidAccessTokenException.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidAccessTokenException.java new file mode 100644 index 0000000000..889df322aa --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidAccessTokenException.java @@ -0,0 +1,19 @@ +package com.mapbox.mapboxsdk.exceptions; + +import android.os.Bundle; +import com.mapbox.mapboxsdk.views.MapView; + +/** + * A {@code InvalidAccessTokenException} is thrown by {@link MapView} when there is either no access + * token set before {@link MapView#onCreate(Bundle)} or an invalid access token is set in {@link MapView#setAccessToken(String)} + * + * @see MapView#onCreate(Bundle) + * @see MapView#setAccessToken(String) + */ +public class InvalidAccessTokenException extends RuntimeException { + + public InvalidAccessTokenException() { + super("Using MapView requires setting a valid access token. See the INSTALL.md"); + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/SpriteBitmapChangedException.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/SpriteBitmapChangedException.java new file mode 100644 index 0000000000..03c5f914b1 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/SpriteBitmapChangedException.java @@ -0,0 +1,27 @@ +package com.mapbox.mapboxsdk.exceptions; + +import android.graphics.Bitmap; + +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.Sprite; +import com.mapbox.mapboxsdk.views.MapView; + +/** + * <p> + * A {@code SpriteBitmapChangedException} is thrown by {@link MapView} when a {@link Marker} is added + * that has a {@link Sprite} with a {@link Bitmap} that has been modified. + * </p> + * You cannot modify a {@code Sprite} after it has been added to the map in a {@code Marker} + * + * @see MapView + * @see Sprite + * @see Marker + */ +public class SpriteBitmapChangedException extends RuntimeException { + + public SpriteBitmapChangedException() { + super("The added Marker has a Sprite with a Bitmap that has been modified. You cannot modufy" + + "a Sprite after it has been added in a Marker."); + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManySpritesException.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManySpritesException.java new file mode 100644 index 0000000000..d36c99edd2 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManySpritesException.java @@ -0,0 +1,21 @@ +package com.mapbox.mapboxsdk.exceptions; + +import com.mapbox.mapboxsdk.annotations.Sprite; +import com.mapbox.mapboxsdk.annotations.SpriteFactory; + +/** + * <p> + * A {@code TooManySpritesException} is thrown by {@link SpriteFactory} when it + * cannot create a {@link Sprite} because there are already too many. + * </p> + * You should try to reuse Sprite objects whenever possible. + * + * @see SpriteFactory + */ +public class TooManySpritesException extends RuntimeException { + + public TooManySpritesException() { + super("Cannot create a Sprite because there are already too many. Try reusing Sprites."); + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java new file mode 100644 index 0000000000..d593884ee3 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains exceptions thrown in this SDK. + */ +package com.mapbox.mapboxsdk.exceptions; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/BoundingBox.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/BoundingBox.java new file mode 100644 index 0000000000..e778e30aad --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/BoundingBox.java @@ -0,0 +1,304 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.Serializable; +import java.util.List; + +/** + * A rectangular geographical area defined in latitude and longitude units. + */ +public final class BoundingBox implements Parcelable, Serializable { + + private final double mLatNorth; + private final double mLatSouth; + private final double mLonEast; + private final double mLonWest; + + private final boolean mIsValid; + + /** + * Construct a new bounding box based on its corners, given in NESW + * order. + * + * @param northLatitude Northern Latitude + * @param eastLongitude Eastern Longitude + * @param southLatitude Southern Latitude + * @param westLongitude Western Longitude + */ + public BoundingBox(final double northLatitude, final double eastLongitude, final double southLatitude, final double westLongitude) { + this.mLatNorth = northLatitude; + this.mLonEast = eastLongitude; + this.mLatSouth = southLatitude; + this.mLonWest = westLongitude; + this.mIsValid = ((this.mLonWest < this.mLonEast) && (this.mLatNorth > this.mLatSouth)); + } + + /** + * Construct a new bounding box based on its corners, given in NESW order. + * + * @param northEast Coordinate + * @param southWest Coordinate + */ + public BoundingBox(final LatLng northEast, final LatLng southWest) { + this(northEast.getLatitude(), northEast.getLongitude(), southWest.getLatitude(), southWest.getLongitude()); + } + + /** + * Create a bounding box from another bounding box + * + * @param other the other bounding box + */ + public BoundingBox(final BoundingBox other) { + this.mLatNorth = other.getLatNorth(); + this.mLonEast = other.getLonEast(); + this.mLatSouth = other.getLatSouth(); + this.mLonWest = other.getLonWest(); + this.mIsValid = other.isValid(); + } + + /** + * Create a new BoundingBox with no size centered at 0, 0, also known as null island + */ + public BoundingBox() { + this(0, 0, 0, 0); + } + + /** + * Calculates the centerpoint of this bounding box by simple interpolation and returns + * it as a point. This is a non-geodesic calculation which is not the geographic center. + * + * @return LatLng center of this BoundingBox + */ + public LatLng getCenter() { + return new LatLng((this.mLatNorth + this.mLatSouth) / 2, + (this.mLonEast + this.mLonWest) / 2); + } + + public double getLatNorth() { + return this.mLatNorth; + } + + public double getLatSouth() { + return this.mLatSouth; + } + + public double getLonEast() { + return this.mLonEast; + } + + public double getLonWest() { + return this.mLonWest; + } + + public boolean isValid() { + return this.mIsValid; + } + + /** + * Get the area spanned by this bounding box + * + * @return CoordinateSpan area + */ + public CoordinateSpan getSpan() { + return new CoordinateSpan(getLatitudeSpan(), getLongitudeSpan()); + } + + /** + * Get the absolute distance, in degrees, between the north and + * south boundaries of this bounding box + * + * @return Span distance + */ + public double getLatitudeSpan() { + return Math.abs(this.mLatNorth - this.mLatSouth); + } + + /** + * Get the absolute distance, in degrees, between the west and + * east boundaries of this bounding box + * + * @return Span distance + */ + public double getLongitudeSpan() { + return Math.abs(this.mLonEast - this.mLonWest); + } + + + /** + * Validate if bounding box is empty, determined if absolute distance is + * + * @return boolean indicating if span is empty + */ + public boolean isEmpty() { + return getLongitudeSpan() == 0.0 || getLatitudeSpan() == 0.0; + } + + @Override + public String toString() { + return "N:" + this.mLatNorth + "; E:" + this.mLonEast + "; S:" + this.mLatSouth + "; W:" + this.mLonWest; + } + + /** + * Constructs a bounding box that contains all of a list of LatLng + * objects. Empty lists will yield invalid bounding boxes. + * + * @param latLngs List of LatLng objects + * @return BoundingBox + */ + public static BoundingBox fromLatLngs(final List<? extends ILatLng> latLngs) { + double minLat = 90, + minLon = 180, + maxLat = -90, + maxLon = -180; + + for (final ILatLng gp : latLngs) { + final double latitude = gp.getLatitude(); + final double longitude = gp.getLongitude(); + + minLat = Math.min(minLat, latitude); + minLon = Math.min(minLon, longitude); + maxLat = Math.max(maxLat, latitude); + maxLon = Math.max(maxLon, longitude); + } + + return new BoundingBox(maxLat, maxLon, minLat, minLon); + } + + /** + * Determines whether this bounding box matches another one via coordinates. + * + * @param o another object + * @return a boolean indicating whether the bounding boxes are equal + */ + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o instanceof BoundingBox) { + BoundingBox other = (BoundingBox) o; + return mLatNorth == other.getLatNorth() + && mLatSouth == other.getLatSouth() + && mLonEast == other.getLonEast() + && mLonWest == other.getLonWest(); + } + return false; + } + + /** + * Determines whether this bounding box contains a point and the point + * does not touch its boundary. + * + * @param pGeoPoint the point which may be contained + * @return true, if the point is contained within the box. + */ + public boolean contains(final ILatLng pGeoPoint) { + final double latitude = pGeoPoint.getLatitude(); + final double longitude = pGeoPoint.getLongitude(); + return ((latitude < this.mLatNorth) + && (latitude > this.mLatSouth)) + && ((longitude < this.mLonEast) + && (longitude > this.mLonWest)); + } + + /** + * Returns a new BoundingBox that stretches to contain both this and another BoundingBox. + * + * @param box BoundingBox to add + * @return BoundingBox + */ + public BoundingBox union(BoundingBox box) { + return union(box.getLatNorth(), box.getLonEast(), box.getLatSouth(), box.getLonWest()); + } + + /** + * Returns a new BoundingBox that stretches to include another bounding box, + * given by corner points. + * + * @param lonNorth Northern Longitude + * @param latEast Eastern Latitude + * @param lonSouth Southern Longitude + * @param latWest Western Longitude + * @return BoundingBox + */ + public BoundingBox union(final double lonNorth, final double latEast, final double lonSouth, final double latWest) { + return new BoundingBox((this.mLatNorth < lonNorth) ? lonNorth : this.mLatNorth, + (this.mLonEast < latEast) ? latEast : this.mLonEast, + (this.mLatSouth > lonSouth) ? lonSouth : this.mLatSouth, + (this.mLonWest > latWest) ? latWest : this.mLonWest); + } + + /** + * Returns a new BoundingBox that is the intersection of this with another box + * + * @param box BoundingBox to intersect with + * @return BoundingBox + */ + public BoundingBox intersect(BoundingBox box) { + double minLatWest = Math.max(getLonWest(), box.getLonWest()); + double maxLatEast = Math.min(getLonEast(), box.getLonEast()); + if (maxLatEast > minLatWest) { + double minLonSouth = Math.max(getLatSouth(), box.getLatSouth()); + double maxLonNorth = Math.min(getLatNorth(), box.getLatNorth()); + if (maxLonNorth > minLonSouth) { + return new BoundingBox(maxLonNorth, maxLatEast, minLonSouth, minLatWest); + } + } + return null; + } + + /** + * Returns a new BoundingBox that is the intersection of this with another box + * + * @param northLongitude Northern Longitude + * @param eastLatitude Eastern Latitude + * @param southLongitude Southern Longitude + * @param westLatitude Western Latitude + * @return BoundingBox + */ + public BoundingBox intersect(double northLongitude, double eastLatitude, double southLongitude, double westLatitude) { + return intersect(new BoundingBox(northLongitude, eastLatitude, southLongitude, westLatitude)); + } + + public static final Parcelable.Creator<BoundingBox> CREATOR = + new Parcelable.Creator<BoundingBox>() { + @Override + public BoundingBox createFromParcel(final Parcel in) { + return readFromParcel(in); + } + + @Override + public BoundingBox[] newArray(final int size) { + return new BoundingBox[size]; + } + }; + + @Override + public int hashCode() { + return (int) ((mLatNorth + 90) + + ((mLatSouth + 90) * 1000) + + ((mLonEast + 180) * 1000000) + + ((mLonEast + 180) * 1000000000)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(final Parcel out, final int arg1) { + out.writeDouble(this.mLatNorth); + out.writeDouble(this.mLonEast); + out.writeDouble(this.mLatSouth); + out.writeDouble(this.mLonWest); + } + + private static BoundingBox readFromParcel(final Parcel in) { + final double lonNorth = in.readDouble(); + final double latEast = in.readDouble(); + final double lonSouth = in.readDouble(); + final double latWest = in.readDouble(); + return new BoundingBox(lonNorth, latEast, lonSouth, latWest); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateBounds.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateBounds.java new file mode 100644 index 0000000000..1ad584f8c8 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateBounds.java @@ -0,0 +1,58 @@ +package com.mapbox.mapboxsdk.geometry; + +/** + * Implementation of iOS MGLCoordinateBounds + */ +public class CoordinateBounds { + + private LatLng southWest; + private LatLng northEast; + + public CoordinateBounds(LatLng southWest, LatLng northEast) { + this.southWest = southWest; + this.northEast = northEast; + } + + public LatLng getSouthWest() { + return southWest; + } + + public void setSouthWest(LatLng southWest) { + this.southWest = southWest; + } + + public LatLng getNorthEast() { + return northEast; + } + + public void setNorthEast(LatLng northEast) { + this.northEast = northEast; + } + + @Override + public int hashCode() { + int result; + long temp; + temp = southWest.hashCode(); + result = (int) (temp ^ (temp >>> 32)); + temp = northEast.hashCode(); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof CoordinateBounds) { + CoordinateBounds other = (CoordinateBounds) o; + return getNorthEast().equals(other.getNorthEast()) + && getSouthWest().equals(other.getSouthWest()); + } + return false; + } + + @Override + public String toString() { + return "CoordinateBounds [northEast[" + getNorthEast() + "], southWest[]" + getSouthWest() + "]"; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateRegion.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateRegion.java new file mode 100644 index 0000000000..a70bb05a41 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateRegion.java @@ -0,0 +1,30 @@ +package com.mapbox.mapboxsdk.geometry; + +/** + * Implementation of iOS MKCoordinateRegion + */ +public class CoordinateRegion { + private LatLng center; + private CoordinateSpan span; + + public CoordinateRegion(final LatLng center, final CoordinateSpan span) { + this.center = center; + this.span = span; + } + + public LatLng getCenter() { + return center; + } + + public void setCenter(final LatLng center) { + this.center = center; + } + + public CoordinateSpan getSpan() { + return span; + } + + public void setSpan(final CoordinateSpan span) { + this.span = span; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateSpan.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateSpan.java new file mode 100644 index 0000000000..79361f725a --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateSpan.java @@ -0,0 +1,43 @@ +package com.mapbox.mapboxsdk.geometry; + +/** + * Implementation of iOS MKCoordinateSpan + */ +public class CoordinateSpan { + + private double latitudeSpan; + private double longitudeSpan; + + public CoordinateSpan(final double latitudeSpan, final double longitudeSpan) { + this.latitudeSpan = latitudeSpan; + this.longitudeSpan = longitudeSpan; + } + + public double getLatitudeSpan() { + return latitudeSpan; + } + + public void setLatitudeSpan(final double latitudeSpan) { + this.latitudeSpan = latitudeSpan; + } + + public double getLongitudeSpan() { + return longitudeSpan; + } + + public void setLongitudeSpan(final double longitudeSpan) { + this.longitudeSpan = longitudeSpan; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof CoordinateSpan) { + CoordinateSpan other = (CoordinateSpan) o; + return longitudeSpan == other.getLongitudeSpan() + && latitudeSpan == other.getLatitudeSpan(); + } + return false; + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ILatLng.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ILatLng.java new file mode 100644 index 0000000000..892d0ad4ae --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ILatLng.java @@ -0,0 +1,12 @@ +package com.mapbox.mapboxsdk.geometry; + +/** + * A Latitude, Longitude point. + */ +public interface ILatLng { + double getLatitude(); + + double getLongitude(); + + double getAltitude(); +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/IProjectedMeters.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/IProjectedMeters.java new file mode 100644 index 0000000000..5aa5f607eb --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/IProjectedMeters.java @@ -0,0 +1,7 @@ +package com.mapbox.mapboxsdk.geometry; + +public interface IProjectedMeters { + double getNorthing(); + + double getEasting(); +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java new file mode 100644 index 0000000000..c340b09e05 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java @@ -0,0 +1,187 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.location.Location; +import android.os.Parcel; +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.constants.GeoConstants; +import com.mapbox.mapboxsdk.constants.MathConstants; + +import java.io.Serializable; + +/** + * <p> + * This class is container for a single latitude, longitude pair, with + * optional altitude. Latitude and longitude are expressed as decimal degrees + * in the WGS84 datum. By default, altitude is 0.0, or sea level. + * </p> + * <p> + * Mapbox GL displays maps in the Mercator Projection and projects geographical + * data automatically, so all data enters in the WGS84 datum. + * </p> + */ +public class LatLng implements ILatLng, Parcelable, Serializable { + + public static final Parcelable.Creator<LatLng> CREATOR = new Parcelable.Creator<LatLng>() { + public LatLng createFromParcel(Parcel in) { + return new LatLng(in); + } + + public LatLng[] newArray(int size) { + return new LatLng[size]; + } + }; + + private double latitude; + private double longitude; + private double altitude = 0.0; + + /** + * Construct a new latitude, longitude point at (0, 0) + */ + public LatLng() { + this.latitude = 0.0; + this.longitude = 0.0; + } + + /** + * Construct a new latitude, longitude point given float arguments + * @param latitude Latitude in degrees + * @param longitude Longitude in degrees + */ + public LatLng(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + + /** + * Construct a new latitude, longitude, altitude point given float arguments + * @param latitude Latitude in degrees + * @param longitude Longitude in degress + * @param altitude Altitude in meters + */ + public LatLng(double latitude, double longitude, double altitude) { + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + } + + /** + * Transform a Location into a LatLng point + * @param location Android Location + */ + public LatLng(Location location) { + this(location.getLatitude(), location.getLongitude(), location.getAltitude()); + } + + /** + * Clone an existing latitude longitude point + * @param aLatLng LatLng + */ + public LatLng(LatLng aLatLng) { + this.latitude = aLatLng.latitude; + this.longitude = aLatLng.longitude; + this.altitude = aLatLng.altitude; + } + + protected LatLng(Parcel in) { + latitude = in.readDouble(); + longitude = in.readDouble(); + altitude = in.readDouble(); + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + @Override + public double getLatitude() { + return latitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + @Override + public double getLongitude() { + return longitude; + } + + public void setAltitude(double altitude) { + this.altitude = altitude; + } + + @Override + public double getAltitude() { + return altitude; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LatLng latLng = (LatLng) o; + + return Double.compare(latLng.altitude, altitude) == 0 && Double.compare(latLng.latitude, latitude) == 0 && Double.compare(latLng.longitude, longitude) == 0; + + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(latitude); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(longitude); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(altitude); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "LatLng [longitude=" + longitude + ", latitude=" + latitude + ", altitude=" + altitude + "]"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeDouble(latitude); + out.writeDouble(longitude); + out.writeDouble(altitude); + } + + /** + * Calculate distance between two points + * @param other Other LatLng to compare to + * @return distance in meters + */ + public double distanceTo(LatLng other) { + + final double a1 = MathConstants.DEG2RAD * this.latitude; + final double a2 = MathConstants.DEG2RAD * this.longitude; + final double b1 = MathConstants.DEG2RAD * other.getLatitude(); + final double b2 = MathConstants.DEG2RAD * other.getLongitude(); + + final double cosa1 = Math.cos(a1); + final double cosb1 = Math.cos(b1); + + final double t1 = cosa1 * Math.cos(a2) * cosb1 * Math.cos(b2); + final double t2 = cosa1 * Math.sin(a2) * cosb1 * Math.sin(b2); + final double t3 = Math.sin(a1) * Math.sin(b1); + final double tt = Math.acos(t1 + t2 + t3); + + return GeoConstants.RADIUS_EARTH_METERS * tt; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngZoom.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngZoom.java new file mode 100644 index 0000000000..9e453a391c --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngZoom.java @@ -0,0 +1,93 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.Serializable; + +public class LatLngZoom extends LatLng implements Parcelable, Serializable { + + public static final Parcelable.Creator<LatLngZoom> CREATOR = new Parcelable.Creator<LatLngZoom>() { + public LatLngZoom createFromParcel(Parcel in) { + return new LatLngZoom(in); + } + + public LatLngZoom[] newArray(int size) { + return new LatLngZoom[size]; + } + }; + + private double zoom; + + public LatLngZoom() { + super(); + this.zoom = 0.0; + } + + public LatLngZoom(double latitude, double longitude, double zoom) { + super(latitude, longitude); + this.zoom = zoom; + } + + public LatLngZoom(LatLng latLng, double zoom) { + super(latLng.getLatitude(), latLng.getLongitude()); + this.zoom = zoom; + } + + private LatLngZoom(Parcel in) { + super(in); + zoom = in.readDouble(); + } + + public double getZoom() { + return zoom; + } + + public void setZoom(double zoom) { + this.zoom = zoom; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + LatLngZoom that = (LatLngZoom) o; + + return Double.compare(that.zoom, zoom) == 0; + + } + + @Override + public int hashCode() { + int result = super.hashCode(); + long temp; + temp = Double.doubleToLongBits(zoom); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "LatLngZoom [latitude=" + super.getLatitude() + ", longitude=" + super.getLongitude() + ", altitude=" + super.getAltitude() + ", zoom=" + zoom + "]"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeDouble(zoom); + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java new file mode 100644 index 0000000000..fc821e887c --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java @@ -0,0 +1,95 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.Serializable; + +/** + * ProjectedMeters is an internal representation of longitude, latitude points that + * have been projected into Mapbox GL's Mercator projection. Instead of decimal + * degrees, it uses Mercator meters (which are notably not equivalent to SI meters) + * except at the equator. + */ +public class ProjectedMeters implements IProjectedMeters, Parcelable, Serializable { + + public static final Creator<ProjectedMeters> CREATOR = new Creator<ProjectedMeters>() { + public ProjectedMeters createFromParcel(Parcel in) { + return new ProjectedMeters(in); + } + + public ProjectedMeters[] newArray(int size) { + return new ProjectedMeters[size]; + } + }; + + private double northing; + private double easting; + + public ProjectedMeters(double northing, double easting) { + this.northing = northing; + this.easting = easting; + } + + public ProjectedMeters(ProjectedMeters aProjectedMeters) { + this.northing = aProjectedMeters.northing; + this.easting = aProjectedMeters.easting; + } + + protected ProjectedMeters(Parcel in) { + northing = in.readDouble(); + easting = in.readDouble(); + } + + @Override + public double getNorthing() { + return northing; + } + + @Override + public double getEasting() { + return easting; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ProjectedMeters projectedMeters = (ProjectedMeters) o; + + return Double.compare(projectedMeters.easting, easting) == 0 && Double.compare(projectedMeters.northing, northing) == 0; + + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(easting); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(northing); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "ProjectedMeters [northing=" + northing + ", easting=" + easting + "]"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeDouble(northing); + out.writeDouble(easting); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java new file mode 100644 index 0000000000..bf7dae12a8 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java @@ -0,0 +1,5 @@ +/** + * This package contains classes that deal with geometry and map coordinates. Many SDK functions + * accept or return these classes. + */ +package com.mapbox.mapboxsdk.geometry; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPContext.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPContext.java new file mode 100644 index 0000000000..886c7b9af8 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPContext.java @@ -0,0 +1,141 @@ +package com.mapbox.mapboxsdk.http; + +import android.util.Log; + +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.squareup.okhttp.Call; +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.ProtocolException; +import java.net.SocketException; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLException; + +class HTTPContext { + + private static final int CONNECTION_ERROR = 0; + private static final int TEMPORARY_ERROR = 1; + private static final int PERMANENT_ERROR = 2; + private static final int CANCELED_ERROR = 3; + + private static HTTPContext mInstance = null; + + private OkHttpClient mClient; + + private HTTPContext() { + super(); + mClient = new OkHttpClient(); + //mClient.interceptors().add(new LoggingInterceptor()); + } + + public static HTTPContext getInstance() { + if (mInstance == null) { + mInstance = new HTTPContext(); + } + + return mInstance; + } + + public HTTPRequest createRequest(long nativePtr, String resourceUrl, String userAgent, String etag, String modified) { + return new HTTPRequest(nativePtr, resourceUrl, userAgent, etag, modified); + } + + public class HTTPRequest implements Callback { + private final String LOG_TAG = HTTPRequest.class.getName(); + + private long mNativePtr = 0; + + private Call mCall; + private Request mRequest; + + private native void nativeOnFailure(long nativePtr, int type, String message); + private native void nativeOnResponse(long nativePtr, int code, String message, String etag, String modified, String cacheControl, String expires, byte[] body); + + private HTTPRequest(long nativePtr, String resourceUrl, String userAgent, String etag, String modified) { + mNativePtr = nativePtr; + Request.Builder builder = new Request.Builder().url(resourceUrl).tag(resourceUrl.toLowerCase(MapboxConstants.MAPBOX_LOCALE)).addHeader("User-Agent", userAgent); + if (etag.length() > 0) { + builder = builder.addHeader("If-None-Match", etag); + } else if (modified.length() > 0) { + builder = builder.addHeader("If-Modified-Since", modified); + } + mRequest = builder.build(); + } + + public void start() { + mCall = HTTPContext.getInstance().mClient.newCall(mRequest); + mCall.enqueue(this); + } + + public void cancel() { + mCall.cancel(); + } + + @Override + public void onFailure(Request request, IOException e) { + Log.d(LOG_TAG, "onFailure: " + e.getMessage()); + + int type = PERMANENT_ERROR; + if ((e instanceof UnknownHostException) || (e instanceof SocketException) || (e instanceof ProtocolException) || (e instanceof SSLException)) { + type = CONNECTION_ERROR; + } else if ((e instanceof InterruptedIOException)) { + type = TEMPORARY_ERROR; + } else if (mCall.isCanceled()) { + type = CANCELED_ERROR; + } + + nativeOnFailure(mNativePtr, type, e.getMessage()); + } + + @Override + public void onResponse(Response response) throws IOException { + Log.d(LOG_TAG, "onResponse"); + + byte[] body; + try { + body = response.body().bytes(); + } catch (IOException e) { + onFailure(null, e); + //throw e; + return; + } finally { + response.body().close(); + } + + nativeOnResponse(mNativePtr, response.code(), response.message(), response.header("ETag"), response.header("Last-Modified"), response.header("Cache-Control"), response.header("Expires"), body); + } + } + + /* + * Application interceptor that logs the outgoing request and the incoming response. + * Based on https://github.com/square/okhttp/wiki/Interceptors + */ + + class LoggingInterceptor implements Interceptor { + + private final static String LOG_TAG = "LoggingInterceptor"; + + @Override public Response intercept(Interceptor.Chain chain) throws IOException { + Request request = chain.request(); + + long t1 = System.nanoTime(); + Log.i(LOG_TAG, String.format("Sending request %s on %s%n%s", + request.url(), chain.connection(), request.headers())); + + Response response = chain.proceed(request); + + long t2 = System.nanoTime(); + Log.i(LOG_TAG, String.format("Received response for %s in %.1fms%n%s", + response.request().url(), (t2 - t1) / 1e6d, response.headers())); + + return response; + } + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java new file mode 100644 index 0000000000..a5dbf30463 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java @@ -0,0 +1,4 @@ +/** + * Do not use this package. Used internally by the SDK. + */ +package com.mapbox.mapboxsdk.http; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/layers/CustomLayer.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/layers/CustomLayer.java new file mode 100644 index 0000000000..f6fb297dc6 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/layers/CustomLayer.java @@ -0,0 +1,21 @@ +package com.mapbox.mapboxsdk.layers; + +public class CustomLayer { + public CustomLayer(String id, + long context, + long initializeFunction, + long renderFunction, + long deinitializeFunction) { + this.mID = id; + this.mContext = context; + this.mInitializeFunction = initializeFunction; + this.mRenderFunction = renderFunction; + this.mDeinitializeFunction = deinitializeFunction; + } + + public String mID; + public long mContext; + public long mInitializeFunction; + public long mRenderFunction; + public long mDeinitializeFunction; +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/layers/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/layers/package-info.java new file mode 100644 index 0000000000..fda1a493e2 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/layers/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains classes to add and manage layers in your map. + */ +package com.mapbox.mapboxsdk.layers; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationListener.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationListener.java new file mode 100644 index 0000000000..c385820423 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationListener.java @@ -0,0 +1,13 @@ +package com.mapbox.mapboxsdk.location; + +import android.location.Location; + +public interface LocationListener { + + /** + * Callback method for receiving location updates from LocationServices. + * @param location The new Location data + */ + public void onLocationChanged(Location location); + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationServices.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationServices.java new file mode 100644 index 0000000000..74a68c3722 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationServices.java @@ -0,0 +1,131 @@ +package com.mapbox.mapboxsdk.location; + +import android.content.Context; +import android.location.Location; +import android.support.annotation.NonNull; +import com.mapzen.android.lost.api.LocationRequest; +import com.mapzen.android.lost.api.LostApiClient; +import java.util.ArrayList; +import java.util.List; + +public class LocationServices implements com.mapzen.android.lost.api.LocationListener { + + private static LocationServices instance = null; + + private LostApiClient mLocationClient; + private LocationRequest mLocationRequest; + + private Location lastLocation = null; + + private List<LocationListener> locationListeners = null; + + private boolean isGPSEnabled = false; + + /** + * Private constructor for singleton LocationServices + */ + private LocationServices(Context context) { + super(); + // Setup location services + mLocationClient = new LostApiClient.Builder(context).build(); + locationListeners = new ArrayList<>(); + } + + /** + * Primary (singleton) access method for LocationServices + * @param context Context + * @return LocationServices + */ + public static LocationServices getLocationServices(@NonNull final Context context) { + if (instance == null) { + if (context == null) { + throw new NullPointerException("Context required for accessing LocationServices"); + } + instance = new LocationServices(context.getApplicationContext()); + } + return instance; + } + + /** + * Enabled / Disable GPS focused location tracking + * + * @param enableGPS true if GPS is to be enabled, false if GPS is to be disabled + */ + public void toggleGPS(boolean enableGPS) { + + if (enableGPS) { + + if (mLocationClient.isConnected()) { + // Disconnect first to ensure that the new requests are GPS + com.mapzen.android.lost.api.LocationServices.FusedLocationApi.removeLocationUpdates(this); + mLocationClient.disconnect(); + } + + // Setup Fresh + mLocationClient.connect(); + Location lastLocation = com.mapzen.android.lost.api.LocationServices.FusedLocationApi.getLastLocation(); + if (lastLocation != null) { + this.lastLocation = lastLocation; + } + + // LocationRequest Tuned for GPS + mLocationRequest = LocationRequest.create() + .setFastestInterval(1000) + .setSmallestDisplacement(3.0f) + .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); + + com.mapzen.android.lost.api.LocationServices.FusedLocationApi.requestLocationUpdates(mLocationRequest, this); + + } else { + + // Disconnect + if (mLocationClient.isConnected()) { + // Disconnect first to ensure that the new requests are GPS + com.mapzen.android.lost.api.LocationServices.FusedLocationApi.removeLocationUpdates(this); + mLocationClient.disconnect(); + } + + } + + isGPSEnabled = enableGPS; + } + + public boolean isGPSEnabled() { + return isGPSEnabled; + } + + @Override + public void onLocationChanged(Location location) { + this.lastLocation = location; + + // Update Listeners + for (LocationListener listener : this.locationListeners) { + listener.onLocationChanged(location); + } + } + + /** + * Last known location + * @return Last known location data + */ + public Location getLastLocation() { + return lastLocation; + } + + /** + * Registers a LocationListener to receive location updates + * @param locationListener LocationListener + */ + public void addLocationListener(@NonNull LocationListener locationListener) { + this.locationListeners.add(locationListener); + } + + /** + * Unregister a LocationListener to stop receiving location updates + * @param locationListener LocationListener to remove + * @return True if LocationListener was found and removed, False if it was not + */ + public boolean removeLocationListener(@NonNull LocationListener locationListener) { + return this.locationListeners.remove(locationListener); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/package-info.java new file mode 100644 index 0000000000..474a554463 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/package-info.java @@ -0,0 +1,5 @@ +/** + * This package contains the {@link com.mapbox.mapboxsdk.MapFragment} class. {@code MapFragment} + * provides a quick and easy way to add a map to your app. + */ +package com.mapbox.mapboxsdk; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ApiAccess.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ApiAccess.java new file mode 100644 index 0000000000..c92031c548 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ApiAccess.java @@ -0,0 +1,45 @@ +package com.mapbox.mapboxsdk.utils; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.mapboxsdk.constants.MapboxConstants; + +/** + * {@code ApiAccess} provides a method to load the Mapbox access token. + */ +public final class ApiAccess { + + /** + * <p> + * Returns the Mapbox access token set in the app resources. + * </p> + * It will first search the application manifest for a {@link MapboxConstants#KEY_META_DATA_MANIFEST} + * meta-data value. If not found it will then attempt to load the access token from the + * {@code res/raw/token.txt} development file. + * + * @param context The {@link Context} of the {@link android.app.Activity} or {@link android.app.Fragment}. + * @return The Mapbox access token or null if not found. + * @see MapboxConstants#KEY_META_DATA_MANIFEST + */ + @Nullable + public static String getToken(@NonNull Context context) { + try { + // read out AndroidManifest + PackageManager packageManager = context.getPackageManager(); + ApplicationInfo appInfo = packageManager.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + String token = appInfo.metaData.getString(MapboxConstants.KEY_META_DATA_MANIFEST); + if (token == null || token.isEmpty()) { + throw new IllegalArgumentException(); + } + return token; + } catch (Exception e) { + // use fallback on string resource, used for development + int tokenResId = context.getResources().getIdentifier("access_token", "string", context.getPackageName()); + return tokenResId != 0 ? context.getString(tokenResId) : null; + } + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/package-info.java new file mode 100644 index 0000000000..9d9c0bf4bb --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/package-info.java @@ -0,0 +1,5 @@ +/** + * This package contains the {@link com.mapbox.mapboxsdk.utils.ApiAccess} class. {@code ApiAccess} + * provides a methods to load a Mapbox access token. + */ +package com.mapbox.mapboxsdk.utils; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/CompassView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/CompassView.java new file mode 100644 index 0000000000..7bd07d0934 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/CompassView.java @@ -0,0 +1,156 @@ +package com.mapbox.mapboxsdk.views; + +import android.content.Context; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorCompat; +import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.mapbox.mapboxsdk.R; + +import java.lang.ref.WeakReference; +import java.util.Timer; +import java.util.TimerTask; + +/** + * CompassView is a UI element overlaid on a map that shows the map's bearing + * when it isn't true north (0.0). Tapping the compass resets the bearing to true + * north and hides the compass. + */ +final class CompassView extends ImageView { + + private Timer mNorthTimer; + private double mDirection = 0.0f; + private ViewPropertyAnimatorCompat mFadeAnimator; + + public CompassView(Context context) { + super(context); + initialize(context); + } + + public CompassView(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(context); + } + + public CompassView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(context); + } + + private void initialize(Context context) { + + // View configuration + setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.compass)); + setContentDescription(getResources().getString(R.string.compassContentDescription)); + setEnabled(false); + + // Layout params + float mScreenDensity = context.getResources().getDisplayMetrics().density; + ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams((int) (48 * mScreenDensity), (int) (48 * mScreenDensity)); + setLayoutParams(lp); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (enabled) { + if (mDirection != 0.0) { + if (mNorthTimer != null){ + mNorthTimer.cancel(); + mNorthTimer = null; + } + if (mFadeAnimator != null) { + mFadeAnimator.cancel(); + } + mFadeAnimator = null; + setAlpha(1.0f); + setVisibility(View.VISIBLE); + } + } else { + if (mNorthTimer != null){ + mNorthTimer.cancel(); + mNorthTimer = null; + } + if (mFadeAnimator != null) { + mFadeAnimator.cancel(); + } + mFadeAnimator = null; + setVisibility(View.INVISIBLE); + } + } + + public void update(double direction) { + mDirection = direction; + setRotation((float) direction); + + if (!isEnabled()) { + return; + } + + if (direction == 0.0) { + if (getVisibility() == View.INVISIBLE) { + return; + } + + if (mNorthTimer == null) { + if (mFadeAnimator != null) { + mFadeAnimator.cancel(); + } + mFadeAnimator = null; + + mNorthTimer = new Timer("CompassView North timer"); + mNorthTimer.schedule(new TimerTask() { + @Override + public void run() { + post(new Runnable() { + @Override + public void run() { + setAlpha(1.0f); + mFadeAnimator = ViewCompat.animate(CompassView.this).alpha(0.0f).setDuration(1000).withLayer(); + mFadeAnimator.setListener(new ViewPropertyAnimatorListenerAdapter() { + @Override + public void onAnimationEnd(View view) { + setVisibility(View.INVISIBLE); + mNorthTimer = null; + } + }); + } + }); + } + }, 1000); + } + } else { + if (mNorthTimer != null){ + mNorthTimer.cancel(); + mNorthTimer = null; + } + if (mFadeAnimator != null) { + mFadeAnimator.cancel(); + } + setAlpha(1.0f); + setVisibility(View.VISIBLE); + } + } + + public static class CompassClickListener implements View.OnClickListener { + + private WeakReference<MapView> mMapView; + + public CompassClickListener(final MapView mapView) { + mMapView = new WeakReference<>(mapView); + } + + @Override + public void onClick(View v) { + final MapView mapView = mMapView.get(); + if (mapView != null) { + mapView.resetNorth(); + } + } + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/MapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/MapView.java new file mode 100644 index 0000000000..f169e98b86 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/MapView.java @@ -0,0 +1,3846 @@ +package com.mapbox.mapboxsdk.views; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.Dialog; +import android.app.Fragment; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; +import android.location.Location; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.CallSuper; +import android.support.annotation.FloatRange; +import android.support.annotation.IntDef; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.view.GestureDetectorCompat; +import android.support.v4.view.ScaleGestureDetectorCompat; +import android.support.v7.app.AlertDialog; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.ArrayAdapter; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ZoomButtonsController; +import com.almeros.android.multitouch.gesturedetectors.RotateGestureDetector; +import com.almeros.android.multitouch.gesturedetectors.ShoveGestureDetector; +import com.almeros.android.multitouch.gesturedetectors.TwoFingerGestureDetector; +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.InfoWindow; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.MarkerOptions; +import com.mapbox.mapboxsdk.annotations.Polygon; +import com.mapbox.mapboxsdk.annotations.PolygonOptions; +import com.mapbox.mapboxsdk.annotations.Polyline; +import com.mapbox.mapboxsdk.annotations.PolylineOptions; +import com.mapbox.mapboxsdk.annotations.Sprite; +import com.mapbox.mapboxsdk.annotations.SpriteFactory; +import com.mapbox.mapboxsdk.constants.MyBearingTracking; +import com.mapbox.mapboxsdk.constants.MyLocationTracking; +import com.mapbox.mapboxsdk.constants.Style; +import com.mapbox.mapboxsdk.exceptions.InvalidAccessTokenException; +import com.mapbox.mapboxsdk.exceptions.SpriteBitmapChangedException; +import com.mapbox.mapboxsdk.geometry.BoundingBox; +import com.mapbox.mapboxsdk.geometry.CoordinateBounds; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngZoom; +import com.mapbox.mapboxsdk.layers.CustomLayer; +import com.mapbox.mapboxsdk.utils.ApiAccess; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * <p> + * A {@code MapView} provides an embeddable map interface. + * You use this class to display map information and to manipulate the map contents from your application. + * You can center the map on a given coordinate, specify the size of the area you want to display, + * and style the features of the map to fit your application's use case. + * </p> + * <p> + * Use of {@code MapView} requires a Mapbox API access token. + * Obtain an access token on the <a href="https://www.mapbox.com/studio/account/tokens/">Mapbox account page</a>. + * </p> + * <strong>Warning:</strong> Please note that you are responsible for getting permission to use the map data, + * and for ensuring your use adheres to the relevant terms of use. + * + * @see MapView#setAccessToken(String) + */ +public final class MapView extends FrameLayout { + + // + // Static members + // + + // Used for logging + private static final String TAG = "MapView"; + + // Used for animation + private static final long ANIMATION_DURATION = 300; + + // Used for saving instance state + private static final String STATE_CENTER_COORDINATE = "centerCoordinate"; + private static final String STATE_CENTER_DIRECTION = "centerDirection"; + private static final String STATE_ZOOM_LEVEL = "zoomLevel"; + private static final String STATE_TILT = "tilt"; + private static final String STATE_ZOOM_ENABLED = "zoomEnabled"; + private static final String STATE_SCROLL_ENABLED = "scrollEnabled"; + private static final String STATE_ROTATE_ENABLED = "rotateEnabled"; + private static final String STATE_TILT_ENABLED = "tiltEnabled"; + private static final String STATE_ZOOM_CONTROLS_ENABLED = "zoomControlsEnabled"; + private static final String STATE_DEBUG_ACTIVE = "debugActive"; + private static final String STATE_STYLE_URL = "styleUrl"; + private static final String STATE_ACCESS_TOKEN = "accessToken"; + private static final String STATE_STYLE_CLASSES = "styleClasses"; + private static final String STATE_DEFAULT_TRANSITION_DURATION = "defaultTransitionDuration"; + private static final String STATE_MY_LOCATION_ENABLED = "myLocationEnabled"; + private static final String STATE_MY_LOCATION_TRACKING_MODE = "myLocationTracking"; + private static final String STATE_COMPASS_ENABLED = "compassEnabled"; + private static final String STATE_COMPASS_GRAVITY = "compassGravity"; + private static final String STATE_COMPASS_MARGIN_LEFT = "compassMarginLeft"; + private static final String STATE_COMPASS_MARGIN_TOP = "compassMarginTop"; + private static final String STATE_COMPASS_MARGIN_RIGHT = "compassMarginRight"; + private static final String STATE_COMPASS_MARGIN_BOTTOM = "compassMarginBottom"; + private static final String STATE_LOGO_GRAVITY = "logoGravity"; + private static final String STATE_LOGO_MARGIN_LEFT = "logoMarginLeft"; + private static final String STATE_LOGO_MARGIN_TOP = "logoMarginTop"; + private static final String STATE_LOGO_MARGIN_RIGHT = "logoMarginRight"; + private static final String STATE_LOGO_MARGIN_BOTTOM = "logoMarginBottom"; + private static final String STATE_LOGO_VISIBILITY = "logoVisibility"; + private static final String STATE_ATTRIBUTION_GRAVITY = "attrGravity"; + private static final String STATE_ATTRIBUTION_MARGIN_LEFT = "attrMarginLeft"; + private static final String STATE_ATTRIBUTION_MARGIN_TOP = "attrMarginTop"; + private static final String STATE_ATTRIBUTION_MARGIN_RIGHT = "attrMarginRight"; + private static final String STATE_ATTRIBUTION_MARGIN_BOTTOM = "atrrMarginBottom"; + private static final String STATE_ATTRIBUTION_VISIBILITY = "atrrVisibility"; + + // Used for positioning views + private static final float DIMENSION_SEVEN_DP = 7f; + private static final float DIMENSION_TEN_DP = 10f; + private static final float DIMENSION_SIXTEEN_DP = 16f; + private static final float DIMENSION_SEVENTYSIX_DP = 76f; + + // Used to select "Improve this map" link in the attribution dialog + // Index into R.arrays.attribution_links + private static final int ATTRIBUTION_INDEX_IMPROVE_THIS_MAP = 2; + + /** + * The currently supported maximum zoom level. + * + * @see MapView#setZoomLevel(double) + */ + public static final double MAXIMUM_ZOOM_LEVEL = 18.0; + + /** + * The currently supported maximum and minimum tilt values. + * + * @see MapView#setTilt(double) + */ + private static final double MINIMUM_TILT = 0; + private static final double MAXIMUM_TILT = 60; + + // + // Instance members + // + + // Used to call JNI NativeMapView + private NativeMapView mNativeMapView; + + // Used to track rendering + private TextureView mTextureView; + + // Used to handle DPI scaling + private float mScreenDensity = 1.0f; + + // Touch gesture detectors + private GestureDetectorCompat mGestureDetector; + private ScaleGestureDetector mScaleGestureDetector; + private RotateGestureDetector mRotateGestureDetector; + private ShoveGestureDetector mShoveGestureDetector; + private boolean mTwoTap = false; + private boolean mZoomStarted = false; + private boolean mQuickZoom = false; + + // Shows zoom buttons + private ZoomButtonsController mZoomButtonsController; + private boolean mZoomControlsEnabled = false; + + // Used to track trackball long presses + private TrackballLongPressTimeOut mCurrentTrackballLongPressTimeOut; + + // Receives changes to network connectivity + private ConnectivityReceiver mConnectivityReceiver; + + // Used for user location + private UserLocationView mUserLocationView; + + // Used for the compass + private CompassView mCompassView; + + // Used for displaying annotations + // Every annotation that has been added to the map + private final List<Annotation> mAnnotations = new ArrayList<>(); + private List<Marker> mMarkersNearLastTap = new ArrayList<>(); + private List<Marker> mSelectedMarkers = new ArrayList<>(); + private List<InfoWindow> mInfoWindows = new ArrayList<>(); + private InfoWindowAdapter mInfoWindowAdapter; + private SpriteFactory mSpriteFactory; + private ArrayList<Sprite> mSprites = new ArrayList<>(); + + // Used for the Mapbox Logo + private ImageView mLogoView; + + // Used for attributions control + private ImageView mAttributionsView; + + // Used to manage MapChange event listeners + private ArrayList<OnMapChangedListener> mOnMapChangedListener = new ArrayList<>(); + + // Used to manage map click event listeners + private OnMapClickListener mOnMapClickListener; + private OnMapLongClickListener mOnMapLongClickListener; + + // Used to manage fling and scroll event listeners + private OnFlingListener mOnFlingListener; + private OnScrollListener mOnScrollListener; + + // Used to manage marker click event listeners + private OnMarkerClickListener mOnMarkerClickListener; + private OnInfoWindowClickListener mOnInfoWindowClickListener; + + // Used to manage FPS change event listeners + private OnFpsChangedListener mOnFpsChangedListener; + + // + // Properties + // + + // These are properties with setters/getters, saved in onSaveInstanceState and XML attributes + private boolean mZoomEnabled = true; + private boolean mScrollEnabled = true; + private boolean mRotateEnabled = true; + private boolean mTiltEnabled = true; + private boolean mAllowConcurrentMultipleOpenInfoWindows = false; + private String mStyleUrl; + + // + // Inner classes + // + + // + // Enums + // + + /** + * Map change event types. + * + * @see MapView.OnMapChangedListener#onMapChanged(int) + */ + @IntDef({REGION_WILL_CHANGE, + REGION_WILL_CHANGE_ANIMATED, + REGION_IS_CHANGING, + REGION_DID_CHANGE, + REGION_DID_CHANGE_ANIMATED, + WILL_START_LOADING_MAP, + DID_FINISH_LOADING_MAP, + DID_FAIL_LOADING_MAP, + WILL_START_RENDERING_FRAME, + DID_FINISH_RENDERING_FRAME, + DID_FINISH_RENDERING_FRAME_FULLY_RENDERED, + WILL_START_RENDERING_MAP, + DID_FINISH_RENDERING_MAP, + DID_FINISH_RENDERING_MAP_FULLY_RENDERED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface MapChange { + } + + /** + * <p> + * This {@link MapChange} is triggered whenever the currently displayed map region is about to changing + * without an animation. + * </p> + * <p> + * This event is followed by a series of {@link MapView#REGION_IS_CHANGING} and ends + * with {@link MapView#REGION_DID_CHANGE}. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int REGION_WILL_CHANGE = 0; + + /** + * <p> + * This {@link MapChange} is triggered whenever the currently displayed map region is about to changing + * with an animation. + * </p> + * <p> + * This event is followed by a series of {@link MapView#REGION_IS_CHANGING} and ends + * with {@link MapView#REGION_DID_CHANGE_ANIMATED}. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int REGION_WILL_CHANGE_ANIMATED = 1; + + /** + * <p> + * This {@link MapChange} is triggered whenever the currently displayed map region is changing. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int REGION_IS_CHANGING = 2; + + /** + * <p> + * This {@link MapChange} is triggered whenever the currently displayed map region finished changing + * without an animation. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int REGION_DID_CHANGE = 3; + + /** + * <p> + * This {@link MapChange} is triggered whenever the currently displayed map region finished changing + * with an animation. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int REGION_DID_CHANGE_ANIMATED = 4; + + /** + * <p> + * This {@link MapChange} is triggered when the map is about to start loading a new map style. + * </p> + * <p> + * This event is followed by {@link MapView#DID_FINISH_LOADING_MAP} or + * {@link MapView#DID_FAIL_LOADING_MAP}. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int WILL_START_LOADING_MAP = 5; + + /** + * <p> + * This {@link MapChange} is triggered when the map has successfully loaded a new map style. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int DID_FINISH_LOADING_MAP = 6; + + /** + * <p> + * This {@link MapChange} is currently not implemented. + * </p> + * <p> + * This event is triggered when the map has failed to load a new map style. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int DID_FAIL_LOADING_MAP = 7; + + /** + * <p> + * This {@link MapChange} is currently not implemented. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int WILL_START_RENDERING_FRAME = 8; + + /** + * <p> + * This {@link MapChange} is currently not implemented. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int DID_FINISH_RENDERING_FRAME = 9; + + /** + * <p> + * This {@link MapChange} is currently not implemented. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int DID_FINISH_RENDERING_FRAME_FULLY_RENDERED = 10; + + /** + * <p> + * This {@link MapChange} is currently not implemented. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int WILL_START_RENDERING_MAP = 11; + + /** + * <p> + * This {@link MapChange} is currently not implemented. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int DID_FINISH_RENDERING_MAP = 12; + + /** + * <p> + * This {@link MapChange} is currently not implemented. + * </p> + * @see com.mapbox.mapboxsdk.views.MapView.OnMapChangedListener + */ + public static final int DID_FINISH_RENDERING_MAP_FULLY_RENDERED = 13; + + // + // Interfaces + // + + /** + * Interface definition for a callback to be invoked when the map is flinged. + * + * @see MapView#setOnFlingListener(OnFlingListener) + */ + public interface OnFlingListener { + /** + * Called when the map is flinged. + */ + void onFling(); + } + + /** + * Interface definition for a callback to be invoked when the map is scrolled. + * + * @see MapView#setOnScrollListener(OnScrollListener) + */ + public interface OnScrollListener { + /** + * Called when the map is scrolled. + */ + void onScroll(); + } + + /** + * Interface definition for a callback to be invoked on every frame rendered to the map view. + * + * @see MapView#setOnFpsChangedListener(OnFpsChangedListener) + */ + public interface OnFpsChangedListener { + /** + * Called for every frame rendered to the map view. + * + * @param fps The average number of frames rendered over the last second. + */ + void onFpsChanged(double fps); + } + + /** + * Interface definition for a callback to be invoked when the user clicks on the map view. + * + * @see MapView#setOnMapClickListener(OnMapClickListener) + */ + public interface OnMapClickListener { + /** + * Called when the user clicks on the map view. + * + * @param point The projected map coordinate the user clicked on. + */ + void onMapClick(@NonNull LatLng point); + } + + /** + * Interface definition for a callback to be invoked when the user long clicks on the map view. + * + * @see MapView#setOnMapLongClickListener(OnMapLongClickListener) + */ + public interface OnMapLongClickListener { + /** + * Called when the user long clicks on the map view. + * + * @param point The projected map coordinate the user long clicked on. + */ + void onMapLongClick(@NonNull LatLng point); + } + + /** + * Interface definition for a callback to be invoked when the user clicks on a marker. + * + * @see MapView#setOnMarkerClickListener(OnMarkerClickListener) + */ + public interface OnMarkerClickListener { + /** + * Called when the user clicks on a marker. + * + * @param marker The marker the user clicked on. + * @return If true the listener has consumed the event and the info window will not be shown. + */ + boolean onMarkerClick(@NonNull Marker marker); + } + + /** + * Interface definition for a callback to be invoked when the user clicks on an info window. + * + * @see MapView#setOnInfoWindowClickListener(OnInfoWindowClickListener) + */ + public interface OnInfoWindowClickListener { + /** + * Called when the user clicks on an info window. + * + * @param marker The marker of the info window the user clicked on. + * @return If true the listener has consumed the event and the info window will not be closed. + */ + boolean onMarkerClick(@NonNull Marker marker); + } + + /** + * Interface definition for a callback to be invoked when the displayed map view changes. + * + * @see MapView#addOnMapChangedListener(OnMapChangedListener) + * @see MapView.MapChange + */ + public interface OnMapChangedListener { + /** + * Called when the displayed map view changes. + * + * @param change Type of map change event, one of {@link #REGION_WILL_CHANGE}, + * {@link #REGION_WILL_CHANGE_ANIMATED}, + * {@link #REGION_IS_CHANGING}, + * {@link #REGION_DID_CHANGE}, + * {@link #REGION_DID_CHANGE_ANIMATED}, + * {@link #WILL_START_LOADING_MAP}, + * {@link #DID_FAIL_LOADING_MAP}, + * {@link #DID_FINISH_LOADING_MAP}, + * {@link #WILL_START_RENDERING_FRAME}, + * {@link #DID_FINISH_RENDERING_FRAME}, + * {@link #DID_FINISH_RENDERING_FRAME_FULLY_RENDERED}, + * {@link #WILL_START_RENDERING_MAP}, + * {@link #DID_FINISH_RENDERING_MAP}, + * {@link #DID_FINISH_RENDERING_MAP_FULLY_RENDERED}. + */ + void onMapChanged(@MapChange int change); + } + + /** + * Interface definition for a callback to be invoked when an info window will be shown. + * + * @see MapView#setInfoWindowAdapter(InfoWindowAdapter) + */ + public interface InfoWindowAdapter { + /** + * Called when an info window will be shown as a result of a marker click. + * + * @param marker The marker the user clicked on. + * @return View to be shown as a info window. If null is returned the default + * info window will be shown. + */ + @Nullable + View getInfoWindow(@NonNull Marker marker); + } + + /** + * Interface definition for a callback to be invoked when the the My Location dot + * (which signifies the user's location) changes location. + * + * @see MapView#setOnMyLocationChangeListener(OnMyLocationChangeListener) + */ + public interface OnMyLocationChangeListener { + /** + * Called when the location of the My Location dot has changed + * (be it latitude/longitude, bearing or accuracy). + * + * @param location The current location of the My Location dot The type of map change event. + */ + void onMyLocationChange(@Nullable Location location); + } + + // + // Constructors + // + + /** + * Simple constructor to use when creating a {@link MapView} from code using the default map style. + * + * @param context The {@link Context} of the {@link android.app.Activity} + * or {@link android.app.Fragment} the {@link MapView} is running in. + * @param accessToken Your public Mapbox access token. Used to load map styles and tiles. + */ + @UiThread + public MapView(@NonNull Context context, @NonNull String accessToken) { + super(context); + if (accessToken == null) { + Log.w(TAG, "accessToken was null, so just returning"); + return; + } + initialize(context, null); + setAccessToken(accessToken); + setStyleUrl(null); + } + + /** + * Simple constructor to use when creating a {@link MapView} from code using the provided map style URL. + * + * @param context The {@link Context} of the {@link android.app.Activity} + * or {@link android.app.Fragment} the {@link MapView} is running in. + * @param accessToken Your public Mapbox access token. Used to load map styles and tiles. + * @param styleUrl A URL to the map style initially displayed. See {@link MapView#setStyleUrl(String)} for possible values. + * @see MapView#setStyleUrl(String) + */ + @UiThread + public MapView(@NonNull Context context, @NonNull String accessToken, @NonNull String styleUrl) { + super(context); + if (accessToken == null) { + Log.w(TAG, "accessToken was null, so just returning"); + return; + } + if (styleUrl == null) { + Log.w(TAG, "styleUrl was null, so just returning"); + return; + } + initialize(context, null); + setAccessToken(accessToken); + setStyleUrl(styleUrl); + } + + // Constructor that is called when inflating a view from XML. + + /** + * Do not call from code. + */ + @UiThread + public MapView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initialize(context, attrs); + } + + // Constructor that is called when inflating a view from XML. + + /** + * Do not call from code. + */ + @UiThread + public MapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(context, attrs); + } + + // + // Initialization + // + + // Common initialization code goes here + private void initialize(Context context, AttributeSet attrs) { + if (context == null) { + Log.w(TAG, "context was null, so just returning"); + return; + } + + // Inflate content + View view = LayoutInflater.from(context).inflate(R.layout.mapview_internal, this); + + if (!isInEditMode()) { + setWillNotDraw(false); + } + + // Reference the TextureView + mTextureView = (TextureView) view.findViewById(R.id.textureView); + mTextureView.setSurfaceTextureListener(new SurfaceTextureListener()); + + // Check if we are in Android Studio UI editor to avoid error in layout preview + if (isInEditMode()) { + return; + } + + // Get the screen's density + mScreenDensity = context.getResources().getDisplayMetrics().density; + + // Get the cache path + String cachePath = context.getCacheDir().getAbsolutePath(); + String dataPath = context.getFilesDir().getAbsolutePath(); + String apkPath = context.getPackageCodePath(); + + // Create the NativeMapView + int availableProcessors = Runtime.getRuntime().availableProcessors(); + ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); + ActivityManager activityManager = (ActivityManager) context + .getSystemService(Context.ACTIVITY_SERVICE); + activityManager.getMemoryInfo(memoryInfo); + long maxMemory = memoryInfo.availMem; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + maxMemory = memoryInfo.totalMem; + } + mNativeMapView = new + NativeMapView(this, cachePath, dataPath, apkPath, mScreenDensity, availableProcessors, maxMemory); + + // Ensure this view is interactable + setClickable(true); + setLongClickable(true); + setFocusable(true); + setFocusableInTouchMode(true); + requestFocus(); + + // Touch gesture detectors + mGestureDetector = new GestureDetectorCompat(context, new GestureListener()); + mGestureDetector.setIsLongpressEnabled(true); + mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureListener()); + ScaleGestureDetectorCompat.setQuickScaleEnabled(mScaleGestureDetector, true); + mRotateGestureDetector = new RotateGestureDetector(context, new RotateGestureListener()); + mShoveGestureDetector = new ShoveGestureDetector(context, new ShoveGestureListener()); + + // Shows the zoom controls + if (!context.getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)) { + mZoomControlsEnabled = true; + } + mZoomButtonsController = new ZoomButtonsController(this); + mZoomButtonsController.setZoomSpeed(ANIMATION_DURATION); + mZoomButtonsController.setOnZoomListener(new OnZoomListener()); + + // Check current connection status + ConnectivityManager connectivityManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); + boolean isConnected = (activeNetwork != null) && activeNetwork.isConnectedOrConnecting(); + onConnectivityChanged(isConnected); + + // Setup user location UI + mUserLocationView = (UserLocationView) view.findViewById(R.id.userLocationView); + mUserLocationView.setMapView(this); + + // Setup compass + mCompassView = (CompassView) view.findViewById(R.id.compassView); + mCompassView.setOnClickListener(new CompassView.CompassClickListener(this)); + + // Setup Mapbox logo + mLogoView = (ImageView) view.findViewById(R.id.logoView); + + // Setup Attributions control + mAttributionsView = (ImageView) view.findViewById(R.id.attributionView); + mAttributionsView.setOnClickListener(new AttributionOnClickListener(this)); + + // Load the attributes + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MapView, 0, 0); + try { + double centerLatitude = typedArray.getFloat(R.styleable.MapView_center_latitude, 0.0f); + double centerLongitude = typedArray.getFloat(R.styleable.MapView_center_longitude, 0.0f); + LatLng centerCoordinate = new LatLng(centerLatitude, centerLongitude); + setCenterCoordinate(centerCoordinate); + // need to set zoom level first because of limitation on rotating when zoomed out + setZoomLevel(typedArray.getFloat(R.styleable.MapView_zoom_level, 0.0f)); + setDirection(typedArray.getFloat(R.styleable.MapView_direction, 0.0f)); + setZoomEnabled(typedArray.getBoolean(R.styleable.MapView_zoom_enabled, true)); + setScrollEnabled(typedArray.getBoolean(R.styleable.MapView_scroll_enabled, true)); + setRotateEnabled(typedArray.getBoolean(R.styleable.MapView_rotate_enabled, true)); + setTiltEnabled(typedArray.getBoolean(R.styleable.MapView_tilt_enabled, true)); + setZoomControlsEnabled(typedArray.getBoolean(R.styleable.MapView_zoom_controls_enabled, isZoomControlsEnabled())); + setDebugActive(typedArray.getBoolean(R.styleable.MapView_debug_active, false)); + if (typedArray.getString(R.styleable.MapView_style_url) != null) { + setStyleUrl(typedArray.getString(R.styleable.MapView_style_url)); + } + if (typedArray.getString(R.styleable.MapView_access_token) != null) { + setAccessToken(typedArray.getString(R.styleable.MapView_access_token)); + } + if (typedArray.getString(R.styleable.MapView_style_classes) != null) { + List<String> styleClasses = Arrays.asList(typedArray + .getString(R.styleable.MapView_style_classes).split("\\s*,\\s*")); + for (String styleClass : styleClasses) { + if (styleClass.length() == 0) { + styleClasses.remove(styleClass); + } + } + setStyleClasses(styleClasses); + } + + // Compass + setCompassEnabled(typedArray.getBoolean(R.styleable.MapView_compass_enabled, true)); + setCompassGravity(typedArray.getInt(R.styleable.MapView_compass_gravity, Gravity.TOP | Gravity.END)); + setWidgetMargins(mCompassView, typedArray.getDimension(R.styleable.MapView_compass_margin_left, DIMENSION_TEN_DP) + , typedArray.getDimension(R.styleable.MapView_compass_margin_top, DIMENSION_TEN_DP) + , typedArray.getDimension(R.styleable.MapView_compass_margin_right, DIMENSION_TEN_DP) + , typedArray.getDimension(R.styleable.MapView_compass_margin_bottom, DIMENSION_TEN_DP)); + + // Logo + setLogoVisibility(typedArray.getInt(R.styleable.MapView_logo_visibility, View.VISIBLE)); + setLogoGravity(typedArray.getInt(R.styleable.MapView_logo_gravity, Gravity.BOTTOM | Gravity.START)); + setWidgetMargins(mLogoView, typedArray.getDimension(R.styleable.MapView_logo_margin_left, DIMENSION_SIXTEEN_DP) + , typedArray.getDimension(R.styleable.MapView_logo_margin_top, DIMENSION_SIXTEEN_DP) + , typedArray.getDimension(R.styleable.MapView_logo_margin_right, DIMENSION_SIXTEEN_DP) + , typedArray.getDimension(R.styleable.MapView_logo_margin_bottom, DIMENSION_SIXTEEN_DP)); + + // Attribution + setAttributionVisibility(typedArray.getInt(R.styleable.MapView_attribution_visibility, View.VISIBLE)); + setAttributionGravity(typedArray.getInt(R.styleable.MapView_attribution_gravity, Gravity.BOTTOM)); + setWidgetMargins(mAttributionsView, typedArray.getDimension(R.styleable.MapView_attribution_margin_left, DIMENSION_SEVENTYSIX_DP) + , typedArray.getDimension(R.styleable.MapView_attribution_margin_top, DIMENSION_SEVEN_DP) + , typedArray.getDimension(R.styleable.MapView_attribution_margin_right, DIMENSION_SEVEN_DP) + , typedArray.getDimension(R.styleable.MapView_attribution_margin_bottom, DIMENSION_SEVEN_DP)); + + // User location + setMyLocationEnabled(typedArray.getBoolean(R.styleable.MapView_my_location_enabled, false)); + } finally { + typedArray.recycle(); + } + } + + // + // Lifecycle events + // + + /** + * <p> + * You must call this method from the parent's {@link android.app.Activity#onCreate(Bundle)} or + * {@link android.app.Fragment#onCreate(Bundle)}. + * </p> + * You must set a valid access token with {@link MapView#setAccessToken(String)} before you this method + * or an exception will be thrown. + * + * @param savedInstanceState Pass in the parent's savedInstanceState. + * @see MapView#setAccessToken(String) + */ + @UiThread + public void onCreate(@Nullable Bundle savedInstanceState) { + if (savedInstanceState != null) { + setCenterCoordinate((LatLng) savedInstanceState.getParcelable(STATE_CENTER_COORDINATE)); + // need to set zoom level first because of limitation on rotating when zoomed out + setZoomLevel(savedInstanceState.getDouble(STATE_ZOOM_LEVEL)); + setDirection(savedInstanceState.getDouble(STATE_CENTER_DIRECTION)); + setTilt(savedInstanceState.getDouble(STATE_TILT), null); + setZoomEnabled(savedInstanceState.getBoolean(STATE_ZOOM_ENABLED)); + setScrollEnabled(savedInstanceState.getBoolean(STATE_SCROLL_ENABLED)); + setRotateEnabled(savedInstanceState.getBoolean(STATE_ROTATE_ENABLED)); + setTiltEnabled(savedInstanceState.getBoolean(STATE_TILT_ENABLED)); + setZoomControlsEnabled(savedInstanceState.getBoolean(STATE_ZOOM_CONTROLS_ENABLED)); + setDebugActive(savedInstanceState.getBoolean(STATE_DEBUG_ACTIVE)); + setStyleUrl(savedInstanceState.getString(STATE_STYLE_URL)); + setAccessToken(savedInstanceState.getString(STATE_ACCESS_TOKEN)); + List<String> appliedStyleClasses = savedInstanceState.getStringArrayList(STATE_STYLE_CLASSES); + if (!appliedStyleClasses.isEmpty()) { + setStyleClasses(appliedStyleClasses); + } + mNativeMapView.setDefaultTransitionDuration( + savedInstanceState.getLong(STATE_DEFAULT_TRANSITION_DURATION)); + setMyLocationEnabled(savedInstanceState.getBoolean(STATE_MY_LOCATION_ENABLED)); + + // Compass + setCompassEnabled(savedInstanceState.getBoolean(STATE_COMPASS_ENABLED)); + setCompassGravity(savedInstanceState.getInt(STATE_COMPASS_GRAVITY)); + setCompassMargins(savedInstanceState.getInt(STATE_COMPASS_MARGIN_LEFT) + , savedInstanceState.getInt(STATE_COMPASS_MARGIN_TOP) + , savedInstanceState.getInt(STATE_COMPASS_MARGIN_RIGHT) + , savedInstanceState.getInt(STATE_COMPASS_MARGIN_BOTTOM)); + + // Logo + setLogoVisibility(savedInstanceState.getInt(STATE_LOGO_VISIBILITY)); + setLogoGravity(savedInstanceState.getInt(STATE_LOGO_GRAVITY)); + setLogoMargins(savedInstanceState.getInt(STATE_LOGO_MARGIN_LEFT) + , savedInstanceState.getInt(STATE_LOGO_MARGIN_TOP) + , savedInstanceState.getInt(STATE_LOGO_MARGIN_RIGHT) + , savedInstanceState.getInt(STATE_LOGO_MARGIN_BOTTOM)); + + // Attribution + setAttributionVisibility(savedInstanceState.getInt(STATE_ATTRIBUTION_VISIBILITY)); + setAttributionGravity(savedInstanceState.getInt(STATE_ATTRIBUTION_GRAVITY)); + setAttributionMargins(savedInstanceState.getInt(STATE_ATTRIBUTION_MARGIN_LEFT) + , savedInstanceState.getInt(STATE_ATTRIBUTION_MARGIN_TOP) + , savedInstanceState.getInt(STATE_ATTRIBUTION_MARGIN_RIGHT) + , savedInstanceState.getInt(STATE_ATTRIBUTION_MARGIN_BOTTOM)); + + //noinspection ResourceType + setMyLocationTrackingMode(savedInstanceState.getInt(STATE_MY_LOCATION_TRACKING_MODE, MyLocationTracking.TRACKING_NONE)); + } + + // Force a check for an access token + validateAccessToken(getAccessToken()); + + // Initialize EGL + mNativeMapView.initializeDisplay(); + mNativeMapView.initializeContext(); + + // Add annotation deselection listener + addOnMapChangedListener(new OnMapChangedListener() { + @Override + public void onMapChanged(@MapChange int change) { + if (change == DID_FINISH_LOADING_MAP) { + reloadSprites(); + reloadMarkers(); + adjustTopOffsetPixels(); + } + } + }); + } + + /** + * You must call this method from the parent's {@link android.app.Activity#onSaveInstanceState(Bundle)} + * or {@link android.app.Fragment#onSaveInstanceState(Bundle)}. + * + * @param outState Pass in the parent's outState. + */ + @UiThread + public void onSaveInstanceState(@NonNull Bundle outState) { + if (outState == null) { + Log.w(TAG, "outState was null, so just returning"); + return; + } + + outState.putParcelable(STATE_CENTER_COORDINATE, getCenterCoordinate()); + // need to set zoom level first because of limitation on rotating when zoomed out + outState.putDouble(STATE_ZOOM_LEVEL, getZoomLevel()); + outState.putDouble(STATE_CENTER_DIRECTION, getDirection()); + outState.putDouble(STATE_TILT, getTilt()); + outState.putBoolean(STATE_ZOOM_ENABLED, isZoomEnabled()); + outState.putBoolean(STATE_SCROLL_ENABLED, isScrollEnabled()); + outState.putBoolean(STATE_ROTATE_ENABLED, isRotateEnabled()); + outState.putBoolean(STATE_TILT_ENABLED, isTiltEnabled()); + outState.putBoolean(STATE_ZOOM_CONTROLS_ENABLED, isZoomControlsEnabled()); + outState.putBoolean(STATE_DEBUG_ACTIVE, isDebugActive()); + outState.putString(STATE_STYLE_URL, getStyleUrl()); + outState.putString(STATE_ACCESS_TOKEN, getAccessToken()); + outState.putStringArrayList(STATE_STYLE_CLASSES, new ArrayList<>(getStyleClasses())); + outState.putLong(STATE_DEFAULT_TRANSITION_DURATION, mNativeMapView.getDefaultTransitionDuration()); + outState.putBoolean(STATE_MY_LOCATION_ENABLED, isMyLocationEnabled()); + outState.putInt(STATE_MY_LOCATION_TRACKING_MODE, mUserLocationView.getMyLocationTrackingMode()); + + // Compass + LayoutParams compassParams = (LayoutParams) mCompassView.getLayoutParams(); + outState.putBoolean(STATE_COMPASS_ENABLED, isCompassEnabled()); + outState.putInt(STATE_COMPASS_GRAVITY, compassParams.gravity); + outState.putInt(STATE_COMPASS_MARGIN_LEFT, compassParams.leftMargin); + outState.putInt(STATE_COMPASS_MARGIN_TOP, compassParams.topMargin); + outState.putInt(STATE_COMPASS_MARGIN_BOTTOM, compassParams.bottomMargin); + outState.putInt(STATE_COMPASS_MARGIN_RIGHT, compassParams.rightMargin); + + // Logo + LayoutParams logoParams = (LayoutParams) mLogoView.getLayoutParams(); + outState.putInt(STATE_LOGO_GRAVITY, logoParams.gravity); + outState.putInt(STATE_LOGO_MARGIN_LEFT, logoParams.leftMargin); + outState.putInt(STATE_LOGO_MARGIN_TOP, logoParams.topMargin); + outState.putInt(STATE_LOGO_MARGIN_RIGHT, logoParams.rightMargin); + outState.putInt(STATE_LOGO_MARGIN_BOTTOM, logoParams.bottomMargin); + outState.putInt(STATE_LOGO_VISIBILITY, mLogoView.getVisibility()); + + // Attribution + LayoutParams attrParams = (LayoutParams) mAttributionsView.getLayoutParams(); + outState.putInt(STATE_ATTRIBUTION_GRAVITY, attrParams.gravity); + outState.putInt(STATE_ATTRIBUTION_MARGIN_LEFT, attrParams.leftMargin); + outState.putInt(STATE_ATTRIBUTION_MARGIN_TOP, attrParams.topMargin); + outState.putInt(STATE_ATTRIBUTION_MARGIN_RIGHT, attrParams.rightMargin); + outState.putInt(STATE_ATTRIBUTION_MARGIN_BOTTOM, attrParams.bottomMargin); + outState.putInt(STATE_ATTRIBUTION_VISIBILITY, mAttributionsView.getVisibility()); + } + + /** + * You must call this method from the parent's {@link Activity#onDestroy()} or {@link Fragment#onDestroy()}. + */ + @UiThread + public void onDestroy() { + mNativeMapView.terminateContext(); + mNativeMapView.terminateDisplay(); + mNativeMapView.destroySurface(); + mNativeMapView.destroy(); + mNativeMapView = null; + } + + /** + * You must call this method from the parent's {@link Activity#onStart()} or {@link Fragment#onStart()}. + */ + @UiThread + public void onStart() { + mUserLocationView.onStart(); + } + + /** + * You must call this method from the parent's {@link Activity#onStop()} or {@link Fragment#onStop()} + */ + @UiThread + public void onStop() { + mUserLocationView.onStop(); + } + + /** + * You must call this method from the parent's {@link Activity#onPause()} or {@link Fragment#onPause()}. + */ + @UiThread + public void onPause() { + // Register for connectivity changes + getContext().unregisterReceiver(mConnectivityReceiver); + mConnectivityReceiver = null; + + mUserLocationView.pause(); + mNativeMapView.pause(); + } + + /** + * You must call this method from the parent's {@link Activity#onResume()} or {@link Fragment#onResume()}. + */ + @UiThread + public void onResume() { + // Register for connectivity changes + mConnectivityReceiver = new ConnectivityReceiver(); + getContext().registerReceiver(mConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + + mUserLocationView.resume(); + mNativeMapView.resume(); + mNativeMapView.update(); + } + + /** + * You must call this method from the parent's {@link Activity#onLowMemory()} or {@link Fragment#onLowMemory()}. + */ + @UiThread + public void onLowMemory() { + mNativeMapView.onLowMemory(); + } + + // + // Position + // + + /** + * Returns the current coordinate at the center of the map view. + * + * @return The current coordinate. + */ + @UiThread + @NonNull + public LatLng getCenterCoordinate() { + return mNativeMapView.getLatLng(); + } + + /** + * <p> + * Centers the map on a new coordinate immediately without changing the zoom level. + * </p> + * <p> + * The initial coordinate is (0, 0). + * </p> + * If you want to animate the change, use {@link MapView#setCenterCoordinate(LatLng, boolean)}. + * + * @param centerCoordinate The new coordinate. + * @see MapView#setCenterCoordinate(LatLng, boolean) + */ + @UiThread + public void setCenterCoordinate(@NonNull LatLng centerCoordinate) { + setCenterCoordinate(centerCoordinate, false); + } + + /** + * <p> + * Centers the map on a new coordinate without changing the zoom level and optionally animates the change. + * </p> + * The initial coordinate is (0, 0). + * + * @param centerCoordinate The new coordinate. + * @param animated If true, animates the change. If false, immediately changes the map. + */ + @UiThread + public void setCenterCoordinate(@NonNull LatLng centerCoordinate, boolean animated) { + if (centerCoordinate == null) { + Log.w(TAG, "centerCoordinate was null, so just returning"); + return; + } + long duration = animated ? ANIMATION_DURATION : 0; + mNativeMapView.cancelTransitions(); + mNativeMapView.setLatLng(centerCoordinate, duration); + } + + + /** + * <p> + * Centers the map on a new coordinate immediately while changing the current zoom level. + * </p> + * <p> + * The initial value is a center coordinate of (0, 0) and a zoom level of 0. + * </p> + * If you want to animate the change, use {@link MapView#setCenterCoordinate(LatLngZoom, boolean)}. + * + * @param centerCoordinate The new coordinate and zoom level. + * @see MapView#setCenterCoordinate(LatLngZoom, boolean) + */ + @UiThread + public void setCenterCoordinate(@NonNull LatLngZoom centerCoordinate) { + setCenterCoordinate(centerCoordinate, false); + } + + /** + * Resets the map to the minimum zoom level, a center coordinate of (0, 0), a true north heading, + * and animates the change. + */ + @UiThread + public void resetPosition() { + mNativeMapView.cancelTransitions(); + mNativeMapView.resetPosition(); + } + + /** + * <p> + * Centers the map on a new coordinate while changing the zoom level and optionally animates the change. + * </p> + * The initial value is a center coordinate of (0, 0) and a zoom level of 0. + * + * @param centerCoordinate The new coordinate and zoom level. + * @param animated If true, animates the change. If false, immediately changes the map. + */ + @UiThread + public void setCenterCoordinate(@NonNull LatLngZoom centerCoordinate, + boolean animated) { + if (centerCoordinate == null) { + Log.w(TAG, "centerCoordinate was null, so just returning"); + return; + } + long duration = animated ? ANIMATION_DURATION : 0; + mNativeMapView.cancelTransitions(); + mNativeMapView.setLatLngZoom(centerCoordinate, duration); + } + + /** + * Returns whether the user may scroll around the map. + * + * @return If true, scrolling is enabled. + */ + @UiThread + public boolean isScrollEnabled() { + return mScrollEnabled; + } + + /** + * <p> + * Changes whether the user may scroll around the map. + * </p> + * <p> + * This setting controls only user interactions with the map. If you set the value to false, + * you may still change the map location programmatically. + * </p> + * The default value is true. + * + * @param scrollEnabled If true, scrolling is enabled. + */ + @UiThread + public void setScrollEnabled(boolean scrollEnabled) { + this.mScrollEnabled = scrollEnabled; + } + + // + // Pitch / Tilt + // + + /** + * Gets the current Tilt in degrees of the MapView + * @return tilt in degrees + */ + public double getTilt() { + return mNativeMapView.getPitch(); + } + + /** + * Sets the Tilt in degrees of the MapView. + * @param pitch New tilt in degrees + * @param duration Animation time in milliseconds. If null then 0 is used, making the animation immediate. + */ + @FloatRange(from = MINIMUM_TILT, to = MAXIMUM_TILT) + public void setTilt(Double pitch, @Nullable Long duration) { + long actualDuration = 0; + if (duration != null) { + actualDuration = duration; + } + mNativeMapView.setPitch(pitch, actualDuration); + } + + // + // Rotation + // + + /** + * Returns the current heading of the map relative to true north. + * + * @return The current heading measured in degrees. + */ + @UiThread + @FloatRange(from = 0, to = 360) + public double getDirection() { + double direction = -mNativeMapView.getBearing(); + + while (direction > 360) { + direction -= 360; + } + while (direction < 0) { + direction += 360; + } + + return direction; + } + + /** + * <p> + * Rotates the map to a new heading relative to true north immediately. + * </p> + * <ul> + * <li>The value 0 means that the top edge of the map view will correspond to true north.</li> + * <li>The value 90 means the top of the map will point due east.</li> + * <li>The value 180 means the top of the map will point due south.</li> + * <li>The value 270 means the top of the map will point due west.</li> + * </ul> + * <p> + * The initial heading is 0. + * </p> + * If you want to animate the change, use {@link MapView#setDirection(double, boolean)}. + * + * @param direction The new heading measured in degrees. + * @see MapView#setDirection(double, boolean) + */ + @UiThread + public void setDirection(@FloatRange(from = 0, to = 360) double direction) { + setDirection(direction, false); + } + + /** + * <p> + * Rotates the map to a new heading relative to true north and optionally animates the change. + * </p> + * <ul> + * <li>The value 0 means that the top edge of the map view will correspond to true north.</li> + * <li>The value 90 means the top of the map will point due east.</li> + * <li>The value 180 means the top of the map will point due south.</li> + * <li>The value 270 means the top of the map will point due west.</li> + * </ul> + * The initial heading is 0. + * + * @param direction The new heading measured in degrees from true north. + * @param animated If true, animates the change. If false, immediately changes the map. + */ + @UiThread + public void setDirection(@FloatRange(from = 0, to = 360) double direction, boolean animated) { + long duration = animated ? ANIMATION_DURATION : 0; + mNativeMapView.cancelTransitions(); + // Out of range direactions are normallised in setBearing + mNativeMapView.setBearing(-direction, duration); + } + + /** + * Resets the map heading to true north and animates the change. + */ + @UiThread + public void resetNorth() { + mNativeMapView.cancelTransitions(); + mNativeMapView.resetNorth(); + } + + /** + * Returns whether the user may rotate the map. + * + * @return If true, rotating is enabled. + */ + @UiThread + public boolean isRotateEnabled() { + return mRotateEnabled; + } + + /** + * <p> + * Changes whether the user may rotate the map. + * </p> + * <p> + * This setting controls only user interactions with the map. If you set the value to false, + * you may still change the map location programmatically. + * </p> + * The default value is true. + * + * @param rotateEnabled If true, rotating is enabled. + */ + @UiThread + public void setRotateEnabled(boolean rotateEnabled) { + this.mRotateEnabled = rotateEnabled; + } + + // + // Scale + // + + /** + * Returns the current zoom level of the map view. + * + * @return The current zoom level. + */ + @UiThread + @FloatRange(from = 0.0, to = MAXIMUM_ZOOM_LEVEL) + public double getZoomLevel() { + return mNativeMapView.getZoom(); + } + + /** + * <p> + * Zooms the map to a new zoom level immediately without changing the center coordinate. + * </p> + * <p> + * At zoom level 0, tiles cover the entire world map; + * at zoom level 1, tiles cover 1/14 of the world; + * at zoom level 2, tiles cover 1/16 of the world, and so on. + * </p> + * <p> + * The initial zoom level is 0. The maximum zoom level is {@link MapView#MAXIMUM_ZOOM_LEVEL}. + * </p> + * If you want to animate the change, use {@link MapView#setZoomLevel(double, boolean)}. + * + * @param zoomLevel The new coordinate. + * @see MapView#setZoomLevel(double, boolean) + * @see MapView#MAXIMUM_ZOOM_LEVEL + */ + @UiThread + public void setZoomLevel(@FloatRange(from = 0.0, to = MAXIMUM_ZOOM_LEVEL) double zoomLevel) { + setZoomLevel(zoomLevel, false); + } + + /** + * <p> + * Zooms the map to a new zoom level and optionally animates the change without changing the center coordinate. + * </p> + * <p> + * At zoom level 0, tiles cover the entire world map; + * at zoom level 1, tiles cover 1/14 of the world; + * at zoom level 2, tiles cover 1/16 of the world, and so on. + * </p> + * The initial zoom level is 0. The maximum zoom level is {@link MapView#MAXIMUM_ZOOM_LEVEL}. + * + * @param zoomLevel The new coordinate. + * @param animated If true, animates the change. If false, immediately changes the map. + * @see MapView#MAXIMUM_ZOOM_LEVEL + */ + @UiThread + public void setZoomLevel(@FloatRange(from = 0.0, to = MAXIMUM_ZOOM_LEVEL) double zoomLevel, boolean animated) { + if ((zoomLevel < 0.0) || (zoomLevel > MAXIMUM_ZOOM_LEVEL)) { + throw new IllegalArgumentException("zoomLevel is < 0 or > MapView.MAXIMUM_ZOOM_LEVEL"); + } + long duration = animated ? ANIMATION_DURATION : 0; + mNativeMapView.cancelTransitions(); + mNativeMapView.setZoom(zoomLevel, duration); + } + + /** + * Returns whether the user may zoom the map. + * + * @return If true, zooming is enabled. + */ + @UiThread + public boolean isZoomEnabled() { + return mZoomEnabled; + } + + /** + * <p> + * Changes whether the user may zoom the map. + * </p> + * <p> + * This setting controls only user interactions with the map. If you set the value to false, + * you may still change the map location programmatically. + * </p> + * The default value is true. + * + * @param zoomEnabled If true, zooming is enabled. + */ + @UiThread + public void setZoomEnabled(boolean zoomEnabled) { + this.mZoomEnabled = zoomEnabled; + + if (mZoomControlsEnabled && (getVisibility() == View.VISIBLE) && mZoomEnabled) { + mZoomButtonsController.setVisible(true); + } else { + mZoomButtonsController.setVisible(false); + } + } + + /** + * Gets whether the zoom controls are enabled. + * + * @return If true, the zoom controls are enabled. + */ + public boolean isZoomControlsEnabled() { + return mZoomControlsEnabled; + } + + /** + * <p> + * Sets whether the zoom controls are enabled. + * If enabled, the zoom controls are a pair of buttons + * (one for zooming in, one for zooming out) that appear on the screen. + * When pressed, they cause the camera to zoom in (or out) by one zoom level. + * If disabled, the zoom controls are not shown. + * </p> + * By default the zoom controls are enabled if the device is only single touch capable; + * + * @param enabled If true, the zoom controls are enabled. + */ + public void setZoomControlsEnabled(boolean enabled) { + mZoomControlsEnabled = enabled; + + if (mZoomControlsEnabled && (getVisibility() == View.VISIBLE) && mZoomEnabled) { + mZoomButtonsController.setVisible(true); + } else { + mZoomButtonsController.setVisible(false); + } + } + + // Zoom in or out + private void zoom(boolean zoomIn) { + zoom(zoomIn, -1.0f, -1.0f); + } + + private void zoom(boolean zoomIn, float x, float y) { + // Cancel any animation + mNativeMapView.cancelTransitions(); + + if (zoomIn) { + mNativeMapView.scaleBy(2.0, x / mScreenDensity, y / mScreenDensity, ANIMATION_DURATION); + } else { + mNativeMapView.scaleBy(0.5, x / mScreenDensity, y / mScreenDensity, ANIMATION_DURATION); + } + } + + // + // Tilt + // + + /** + * Returns whether the user may tilt the map. + * + * @return If true, tilting is enabled. + */ + @UiThread + public boolean isTiltEnabled() { + return mTiltEnabled; + } + + /** + * <p> + * Changes whether the user may tilt the map. + * </p> + * <p> + * This setting controls only user interactions with the map. If you set the value to false, + * you may still change the map location programmatically. + * </p> + * The default value is true. + * + * @param tiltEnabled If true, tilting is enabled. + */ + @UiThread + public void setTiltEnabled(boolean tiltEnabled) { + this.mTiltEnabled = tiltEnabled; + } + + // + // InfoWindows + // + + /** + * Changes whether the map allows concurrent multiple infowindows to be shown. + * + * @param allow If true, map allows concurrent multiple infowindows to be shown. + */ + @UiThread + public void setAllowConcurrentMultipleOpenInfoWindows(boolean allow) { + this.mAllowConcurrentMultipleOpenInfoWindows = allow; + } + + /** + * Returns whether the map allows concurrent multiple infowindows to be shown. + * + * @return If true, map allows concurrent multiple infowindows to be shown. + */ + @UiThread + public boolean isAllowConcurrentMultipleOpenInfoWindows() { + return this.mAllowConcurrentMultipleOpenInfoWindows; + } + + // + // Debug + // + + /** + * Returns whether the map debug information is currently shown. + * + * @return If true, map debug information is currently shown. + */ + @UiThread + public boolean isDebugActive() { + return mNativeMapView.getDebug(); + } + + /** + * <p> + * Changes whether the map debug information is shown. + * </p> + * The default value is false. + * + * @param debugActive If true, map debug information is shown. + */ + @UiThread + public void setDebugActive(boolean debugActive) { + mNativeMapView.setDebug(debugActive); + } + + /** + * <p> + * Cycles through the map debug options. + * </p> + * The value of {@link MapView#isDebugActive()} reflects whether there are + * any map debug options enabled or disabled. + * + * @see MapView#isDebugActive() + */ + @UiThread + public void cycleDebugOptions() { + mNativeMapView.cycleDebugOptions(); + } + + // True if map has finished loading the view + private boolean isFullyLoaded() { + return mNativeMapView.isFullyLoaded(); + } + + // + // Styling + // + + /** + * <p> + * Loads a new map style from the specified URL. + * </p> + * {@code url} can take the following forms: + * <ul> + * <li>{@code Style.*}: load one of the bundled styles in {@link Style}.</li> + * <li>{@code mapbox://styles/<user>/<style>}: + * retrieves the style from a <a href="https://www.mapbox.com/account/">Mapbox account.</a> + * {@code user} is your username. {@code style} is the ID of your custom + * style created in <a href="https://www.mapbox.com/studio">Mapbox Studio</a>.</li> + * <li>{@code http://...} or {@code https://...}: + * retrieves the style over the Internet from any web server.</li> + * <li>{@code asset://...}: + * reads the style from the APK {@code assets/} directory. + * This is used to load a style bundled with your app.</li> + * <li>{@code null}: loads the default {@link Style#MAPBOX_STREETS} style.</li> + * </ul> + * <p> + * This method is asynchronous and will return immediately before the style finishes loading. + * If you wish to wait for the map to finish loading listen for the {@link MapView#DID_FINISH_LOADING_MAP} event. + * </p> + * If the style fails to load or an invalid style URL is set, the map view will become blank. + * An error message will be logged in the Android logcat and {@link MapView#DID_FAIL_LOADING_MAP} event will be sent. + * + * @param url The URL of the map style + * @see Style + */ + @UiThread + public void setStyleUrl(@Nullable String url) { + if (url == null) { + url = Style.MAPBOX_STREETS; + } + mStyleUrl = url; + mNativeMapView.setStyleUrl(url); + } + + /** + * <p> + * Loads a new map style from the specified bundled style. + * </p> + * <p> + * This method is asynchronous and will return immediately before the style finishes loading. + * If you wish to wait for the map to finish loading listen for the {@link MapView#DID_FINISH_LOADING_MAP} event. + * </p> + * If the style fails to load or an invalid style URL is set, the map view will become blank. + * An error message will be logged in the Android logcat and {@link MapView#DID_FAIL_LOADING_MAP} event will be sent. + * + * @param style The bundled style. Accepts one of the values from {@link Style}. + * @see Style + */ + @UiThread + public void setStyle(@Style.StyleUrl String style) { + setStyleUrl(style); + } + + /** + * <p> + * Returns the map style currently displayed in the map view. + * </p> + * If the default style is currently displayed, a URL will be returned instead of null. + * + * @return The URL of the map style. + */ + @UiThread + @NonNull + public String getStyleUrl() { + return mStyleUrl; + } + + /** + * Returns the set of currently active map style classes. + * + * @return A list of class identifiers. + */ + @UiThread + @NonNull + public List<String> getStyleClasses() { + return Collections.unmodifiableList(mNativeMapView.getClasses()); + } + + /** + * <p> + * Changes the set of currently active map style classes immediately. + * </p> + * <p> + * The list of valid class identifiers is defined by the currently loaded map style. + * </p> + * If you want to animate the change, use {@link MapView#setStyleClasses(List, long)}. + * + * @param styleClasses A list of class identifiers. + * @see MapView#setStyleClasses(List, long) + * @see MapView#setStyleUrl(String) + */ + @UiThread + public void setStyleClasses(@NonNull List<String> styleClasses) { + setStyleClasses(styleClasses, 0); + } + + /** + * <p> + * Changes the set of currently active map style classes with an animated transition. + * </p> + * The list of valid class identifiers is defined by the currently loaded map style. + * + * @param styleClasses A list of class identifiers. + * @param transitionDuration The duration of the transition animation in milliseconds. + * @see MapView#setStyleClasses(List, long) + * @see MapView#setStyleUrl(String) + */ + @UiThread + public void setStyleClasses(@NonNull List<String> styleClasses, @IntRange(from = 0) long transitionDuration) { + if (styleClasses == null) { + Log.w(TAG, "styleClasses was null, so just returning"); + return; + } + if (transitionDuration < 0) { + throw new IllegalArgumentException("transitionDuration is < 0"); + } + // TODO non negative check and annotation (go back and check other functions too) + mNativeMapView.setDefaultTransitionDuration(transitionDuration); + mNativeMapView.setClasses(styleClasses); + } + + /** + * <p> + * Activates the specified map style class. + * </p> + * If you want to animate the change, use {@link MapView#setStyleClasses(List, long)}. + * + * @param styleClass The class identifier. + * @see MapView#setStyleClasses(List, long) + */ + @UiThread + public void addStyleClass(@NonNull String styleClass) { + if (styleClass == null) { + Log.w(TAG, "styleClass was null, so just returning"); + return; + } + mNativeMapView.addClass(styleClass); + } + + /** + * <p> + * Deactivates the specified map style class. + * </p> + * If you want to animate the change, use {@link MapView#setStyleClasses(List, long)}. + * + * @param styleClass The class identifier. + * @see MapView#setStyleClasses(List, long) + */ + @UiThread + public void removeStyleClass(@NonNull String styleClass) { + if (styleClass == null) { + Log.w(TAG, "styleClass was null, so just returning"); + return; + } + mNativeMapView.removeClass(styleClass); + } + + /** + * Returns whether the specified map style class is currently active. + * + * @param styleClass The class identifier. + * @return If true, the class is currently active. + */ + @UiThread + public boolean hasStyleClass(@NonNull String styleClass) { + if (styleClass == null) { + Log.w(TAG, "centerCoordinate was null, so just returning false"); + return false; + } + return mNativeMapView.hasClass(styleClass); + } + + /** + * <p> + * Deactivates all the currently active map style classes immediately. + * </p> + * If you want to animate the change, use {@link MapView#removeAllStyleClasses(long)}. + * + * @see MapView#removeAllStyleClasses(long) + */ + @UiThread + public void removeAllStyleClasses() { + removeAllStyleClasses(0); + } + + /** + * Deactivates all the currently active map style classes with an animated transition. + * + * @param transitionDuration The duration of the transition animation in milliseconds. + */ + @UiThread + public void removeAllStyleClasses(@IntRange(from = 0) long transitionDuration) { + if (transitionDuration < 0) { + throw new IllegalArgumentException("transitionDuration is < 0"); + } + mNativeMapView.setDefaultTransitionDuration(transitionDuration); + ArrayList<String> styleClasses = new ArrayList<>(0); + setStyleClasses(styleClasses); + } + + // + // Access token + // + + // Checks if the given token is valid + private void validateAccessToken(String accessToken) { + if (TextUtils.isEmpty(accessToken) || (!accessToken.startsWith("pk.") && !accessToken.startsWith("sk."))) { + throw new InvalidAccessTokenException(); + } + } + + /** + * <p> + * Sets the current Mapbox access token used to load map styles and tiles. + * </p> + * <p> + * You must set a valid access token before you call {@link MapView#onCreate(Bundle)} + * or an exception will be thrown. + * </p> + * You can use {@link ApiAccess#getToken(Context)} to load an access token from your + * application's manifest. + * + * @param accessToken Your public Mapbox access token. + * @see MapView#onCreate(Bundle) + * @see ApiAccess#getToken(Context) + */ + @UiThread + public void setAccessToken(@NonNull String accessToken) { + // validateAccessToken does the null check + if (!TextUtils.isEmpty(accessToken)) { + accessToken = accessToken.trim(); + } + validateAccessToken(accessToken); + mNativeMapView.setAccessToken(accessToken); + } + + /** + * Returns the current Mapbox access token used to load map styles and tiles. + * + * @return The current Mapbox access token. + */ + @UiThread + @Nullable + public String getAccessToken() { + return mNativeMapView.getAccessToken(); + } + + // + // Projection + // + + /** + * Converts a point in this view's coordinate system to a map coordinate. + * + * @param point A point in this view's coordinate system. + * @return The converted map coordinate. + */ + @UiThread + @NonNull + public LatLng fromScreenLocation(@NonNull PointF point) { + if (point == null) { + Log.w(TAG, "point was null, so just returning (0, 0)"); + return new LatLng(); + } + + float x = point.x; + float y = point.y; + + // flip y direction vertically to match core GL + y = getHeight() - y; + + return mNativeMapView.latLngForPixel(new PointF(x / mScreenDensity, y / mScreenDensity)); + } + + /** + * Converts a map coordinate to a point in this view's coordinate system. + * + * @param location A map coordinate. + * @return The converted point in this view's coordinate system. + */ + @UiThread + @NonNull + public PointF toScreenLocation(@NonNull LatLng location) { + if (location == null) { + Log.w(TAG, "location was null, so just returning (0, 0)"); + return new PointF(); + } + + PointF point = mNativeMapView.pixelForLatLng(location); + + float x = point.x * mScreenDensity; + float y = point.y * mScreenDensity; + + // flip y direction vertically to match core GL + y = getHeight() - y; + + return new PointF(x, y); + } + + // + // Annotations + // + + public SpriteFactory getSpriteFactory() { + if (mSpriteFactory == null) { + mSpriteFactory = new SpriteFactory(this); + } + return mSpriteFactory; + } + + private void loadSprite(Sprite sprite) { + Bitmap bitmap = sprite.getBitmap(); + String id = sprite.getId(); + if (bitmap.getConfig() != Bitmap.Config.ARGB_8888) { + bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false); + } + ByteBuffer buffer = ByteBuffer.allocate(bitmap.getRowBytes() * bitmap.getHeight()); + bitmap.copyPixelsToBuffer(buffer); + + float density = bitmap.getDensity(); + if (density == Bitmap.DENSITY_NONE) { + density = DisplayMetrics.DENSITY_DEFAULT; + } + float scale = density / DisplayMetrics.DENSITY_DEFAULT; + + mNativeMapView.addAnnotationIcon( + id, + (int) (bitmap.getWidth() / scale), + (int) (bitmap.getHeight() / scale), + scale, buffer.array()); + } + + private void reloadSprites() { + int count = mSprites.size(); + for (int i = 0; i < count; i++) { + Sprite sprite = mSprites.get(i); + loadSprite(sprite); + } + } + + private Marker prepareMarker(MarkerOptions markerOptions) { + Marker marker = markerOptions.getMarker(); + Sprite icon = marker.getIcon(); + if (icon == null) { + icon = getSpriteFactory().defaultMarker(); + marker.setIcon(icon); + } + if (!mSprites.contains(icon)) { + mSprites.add(icon); + loadSprite(icon); + } else { + Sprite oldSprite = mSprites.get(mSprites.indexOf(icon)); + if (!oldSprite.getBitmap().sameAs(icon.getBitmap())) { + throw new SpriteBitmapChangedException(); + } + } + marker.setTopOffsetPixels(getTopOffsetPixelsForSprite(icon)); + return marker; + } + + /** + * <p> + * Adds a marker to this map. + * </p> + * The marker's icon is rendered on the map at the location {@code Marker.position}. + * If {@code Marker.title} is defined, the map shows an info box with the marker's title and snippet. + * + * @param markerOptions A marker options object that defines how to render the marker. + * @return The {@code Marker} that was added to the map. + */ + @UiThread + @NonNull + public Marker addMarker(@NonNull MarkerOptions markerOptions) { + if (markerOptions == null) { + Log.w(TAG, "markerOptions was null, so just returning null"); + return null; + } + + Marker marker = prepareMarker(markerOptions); + long id = mNativeMapView.addMarker(marker); + marker.setId(id); // the annotation needs to know its id + marker.setMapView(this); // the annotation needs to know which map view it is in + mAnnotations.add(marker); + return marker; + } + + /** + * <p> + * Adds multiple markers to this map. + * </p> + * The marker's icon is rendered on the map at the location {@code Marker.position}. + * If {@code Marker.title} is defined, the map shows an info box with the marker's title and snippet. + * + * @param markerOptionsList A list of marker options objects that defines how to render the markers. + * @return A list of the {@code Marker}s that were added to the map. + */ + @UiThread + @NonNull + public List<Marker> addMarkers(@NonNull List<MarkerOptions> markerOptionsList) { + if (markerOptionsList == null) { + Log.w(TAG, "markerOptionsList was null, so just returning null"); + return null; + } + + int count = markerOptionsList.size(); + List<Marker> markers = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + MarkerOptions markerOptions = markerOptionsList.get(i); + Marker marker = prepareMarker(markerOptions); + markers.add(marker); + } + + long[] ids = mNativeMapView.addMarkers(markers); + + Marker m; + for (int i = 0; i < count; i++) { + m = markers.get(i); + m.setId(ids[i]); + m.setMapView(this); + mAnnotations.add(m); + } + + return new ArrayList<>(markers); + } + + /** + * Adds a polyline to this map. + * + * @param polylineOptions A polyline options object that defines how to render the polyline. + * @return The {@code Polyine} that was added to the map. + */ + @UiThread + @NonNull + public Polyline addPolyline(@NonNull PolylineOptions polylineOptions) { + if (polylineOptions == null) { + Log.w(TAG, "polylineOptions was null, so just returning null"); + return null; + } + + Polyline polyline = polylineOptions.getPolyline(); + long id = mNativeMapView.addPolyline(polyline); + polyline.setId(id); + polyline.setMapView(this); + mAnnotations.add(polyline); + return polyline; + } + + /** + * Adds multiple polylines to this map. + * + * @param polylineOptionsList A list of polyline options objects that defines how to render the polylines. + * @return A list of the {@code Polyline}s that were added to the map. + */ + @UiThread + @NonNull + public List<Polyline> addPolylines(@NonNull List<PolylineOptions> polylineOptionsList) { + if (polylineOptionsList == null) { + Log.w(TAG, "polylineOptionsList was null, so just returning null"); + return null; + } + + int count = polylineOptionsList.size(); + List<Polyline> polylines = new ArrayList<>(count); + for (PolylineOptions options : polylineOptionsList) { + polylines.add(options.getPolyline()); + } + + long[] ids = mNativeMapView.addPolylines(polylines); + + Polyline p; + for (int i = 0; i < count; i++) { + p = polylines.get(i); + p.setId(ids[i]); + p.setMapView(this); + mAnnotations.add(p); + } + + return new ArrayList<>(polylines); + } + + /** + * Adds a polygon to this map. + * + * @param polygonOptions A polygon options object that defines how to render the polygon. + * @return The {@code Polygon} that was added to the map. + */ + @UiThread + @NonNull + public Polygon addPolygon(@NonNull PolygonOptions polygonOptions) { + if (polygonOptions == null) { + Log.w(TAG, "polygonOptions was null, so just returning null"); + return null; + } + + Polygon polygon = polygonOptions.getPolygon(); + long id = mNativeMapView.addPolygon(polygon); + polygon.setId(id); + polygon.setMapView(this); + mAnnotations.add(polygon); + return polygon; + } + + + /** + * Adds multiple polygons to this map. + * + * @param polygonOptionsList A list of polygon options objects that defines how to render the polygons. + * @return A list of the {@code Polygon}s that were added to the map. + */ + @UiThread + @NonNull + public List<Polygon> addPolygons(@NonNull List<PolygonOptions> polygonOptionsList) { + if (polygonOptionsList == null) { + Log.w(TAG, "polygonOptionsList was null, so just returning null"); + return null; + } + + int count = polygonOptionsList.size(); + List<Polygon> polygons = new ArrayList<>(count); + for (PolygonOptions polygonOptions : polygonOptionsList) { + polygons.add(polygonOptions.getPolygon()); + } + + long[] ids = mNativeMapView.addPolygons(polygons); + + Polygon p; + for (int i = 0; i < count; i++) { + p = polygons.get(i); + p.setId(ids[i]); + p.setMapView(this); + mAnnotations.add(p); + } + + return new ArrayList<>(polygons); + } + + + /** + * <p> + * Convenience method for removing a Marker from the map. + * </p> + * Calls removeAnnotation() internally + * + * @param marker Marker to remove + */ + @UiThread + public void removeMarker(@NonNull Marker marker) { + removeAnnotation(marker); + } + + /** + * Removes an annotation from the map. + * + * @param annotation The annotation object to remove. + */ + @UiThread + public void removeAnnotation(@NonNull Annotation annotation) { + if (annotation == null) { + Log.w(TAG, "annotation was null, so just returning"); + return; + } + + if (annotation instanceof Marker) { + ((Marker) annotation).hideInfoWindow(); + } + long id = annotation.getId(); + mNativeMapView.removeAnnotation(id); + mAnnotations.remove(annotation); + } + + /** + * Removes multiple annotations from the map. + * + * @param annotationList A list of annotation objects to remove. + */ + @UiThread + public void removeAnnotations(@NonNull List<? extends Annotation> annotationList) { + if (annotationList == null) { + Log.w(TAG, "annotationList was null, so just returning"); + return; + } + + int count = annotationList.size(); + long[] ids = new long[count]; + for (int i = 0; i < count; i++) { + ids[i] = annotationList.get(i).getId(); + } + mNativeMapView.removeAnnotations(ids); + } + + /** + * Removes all annotations from the map. + */ + @UiThread + public void removeAllAnnotations() { + int count = mAnnotations.size(); + long[] ids = new long[mAnnotations.size()]; + + for (int i = 0; i < count; i++) { + Annotation annotation = mAnnotations.get(i); + long id = annotation.getId(); + ids[i] = id; + if (annotation instanceof Marker) { + ((Marker) annotation).hideInfoWindow(); + } + } + + mNativeMapView.removeAnnotations(ids); + mAnnotations.clear(); + } + + /** + * Returns a list of all the annotations on the map. + * + * @return A list of all the annotation objects. The returned object is a copy so modifying this + * list will not update the map. + */ + @NonNull + public List<Annotation> getAllAnnotations() { + return new ArrayList<>(mAnnotations); + } + + private List<Marker> getMarkersInBounds(@NonNull BoundingBox bbox) { + if (bbox == null) { + Log.w(TAG, "bbox was null, so just returning null"); + return null; + } + + // TODO: filter in JNI using C++ parameter to getAnnotationsInBounds + long[] ids = mNativeMapView.getAnnotationsInBounds(bbox); + + List<Long> idsList = new ArrayList<>(ids.length); + for (int i = 0; i < ids.length; i++) { + idsList.add(ids[i]); + } + + List<Marker> annotations = new ArrayList<>(ids.length); + int count = mAnnotations.size(); + for (int i = 0; i < count; i++) { + Annotation annotation = mAnnotations.get(i); + if (annotation instanceof Marker && idsList.contains(annotation.getId())) { + annotations.add((Marker) annotation); + } + } + + return new ArrayList<>(annotations); + } + + private int getTopOffsetPixelsForSprite(Sprite sprite) { + // This method will dead lock if map paused. Causes a freeze if you add a marker in an + // activity's onCreate() + if (mNativeMapView.isPaused()) { + return 0; + } + + return (int) (mNativeMapView.getTopOffsetPixelsForAnnotationSymbol(sprite.getId()) + * mScreenDensity); + } + + /** + * <p> + * Returns the distance spanned by one pixel at the specified latitude and current zoom level. + * </p> + * The distance between pixels decreases as the latitude approaches the poles. + * This relationship parallels the relationship between longitudinal coordinates at different latitudes. + * + * @param latitude The latitude for which to return the value. + * @return The distance measured in meters. + */ + @UiThread + public double getMetersPerPixelAtLatitude(@FloatRange(from = -180, to = 180) double latitude) { + return mNativeMapView.getMetersPerPixelAtLatitude(latitude, getZoomLevel()) / mScreenDensity; + } + + /** + * <p> + * Selects a marker. The selected marker will have it's info window opened. + * Any other open info windows will be closed unless isAllowConcurrentMultipleOpenInfoWindows() + * is true. + * </p> + * Selecting an already selected marker will have no effect. + * + * @param marker The marker to select. + */ + @UiThread + public void selectMarker(@NonNull Marker marker) { + if (marker == null) { + Log.w(TAG, "marker was null, so just returning"); + return; + } + + if (mSelectedMarkers.contains(marker)) { + return; + } + + // Need to deselect any currently selected annotation first + if (!isAllowConcurrentMultipleOpenInfoWindows()) { + deselectMarkers(); + } + + boolean handledDefaultClick = false; + if (mOnMarkerClickListener != null) { + // end developer has provided a custom click listener + handledDefaultClick = mOnMarkerClickListener.onMarkerClick(marker); + } + + if (!handledDefaultClick) { + // default behaviour show InfoWindow + mInfoWindows.add(marker.showInfoWindow()); + } + + mSelectedMarkers.add(marker); + } + + /** + * Deselects any currently selected marker. All markers will have it's info window closed. + */ + @UiThread + public void deselectMarkers() { + if (mSelectedMarkers.isEmpty()) { + return; + } + + for (Marker marker : mSelectedMarkers) { + if (marker.isInfoWindowShown()) { + marker.hideInfoWindow(); + } + } + + // Removes all selected markers from the list + mSelectedMarkers.clear(); + } + + /** + * Deselects a currently selected marker. The selected marker will have it's info window closed. + */ + @UiThread + public void deselectMarker(@NonNull Marker marker) { + if (!mSelectedMarkers.contains(marker)) { + return; + } + + if (marker.isInfoWindowShown()) { + marker.hideInfoWindow(); + } + + mSelectedMarkers.remove(marker); + } + + // + // Camera + // + + /** + * Changes the map's viewport to fit the given coordinate bounds. + * + * @param bounds The bounds that the viewport will show in its entirety. + */ + @UiThread + public void setVisibleCoordinateBounds(@NonNull CoordinateBounds bounds) { + setVisibleCoordinateBounds(bounds, false); + } + + /** + * Changes the map's viewing area to fit the given coordinate bounds, optionally animating the change. + * + * @param bounds The bounds that the viewport will show in its entirety. + * @param animated If true, animates the change. If false, immediately changes the map. + */ + @UiThread + public void setVisibleCoordinateBounds(@NonNull CoordinateBounds bounds, boolean animated) { + setVisibleCoordinateBounds(bounds, new RectF(), animated); + } + + /** + * Changes the map’s viewport to fit the given coordinate bounds with additional padding at the + * edge of the map, optionally animating the change. + * + * @param bounds The bounds that the viewport will show in its entirety. + * @param padding The minimum padding (in pixels) that will be visible around the given coordinate bounds. + * @param animated If true, animates the change. If false, immediately changes the map. + */ + @UiThread + public void setVisibleCoordinateBounds(@NonNull CoordinateBounds bounds, @NonNull RectF padding, boolean animated) { + LatLng[] coordinates = { + new LatLng(bounds.getNorthEast().getLatitude(), bounds.getSouthWest().getLongitude()), + bounds.getSouthWest(), + new LatLng(bounds.getSouthWest().getLatitude(), bounds.getNorthEast().getLongitude()), + bounds.getNorthEast() + + }; + setVisibleCoordinateBounds(coordinates, padding, animated); + } + + /** + * Changes the map’s viewport to fit the given coordinates, optionally some additional padding on each side + * and animating the change. + * + * @param coordinates The coordinates that the viewport will show. + * @param padding The minimum padding (in pixels) that will be visible around the given coordinate bounds. + * @param animated If true, animates the change. If false, immediately changes the map. + */ + @UiThread + public void setVisibleCoordinateBounds(@NonNull LatLng[] coordinates, @NonNull RectF padding, boolean animated) { + setVisibleCoordinateBounds(coordinates, padding, getDirection(), animated); + } + + private void setVisibleCoordinateBounds(LatLng[] coordinates, RectF padding, double direction, boolean animated) { + setVisibleCoordinateBounds(coordinates, padding, direction, animated ? ANIMATION_DURATION : 0l); + } + + private void setVisibleCoordinateBounds(LatLng[] coordinates, RectF padding, double direction, long duration) { + mNativeMapView.setVisibleCoordinateBounds(coordinates, new RectF(padding.left / mScreenDensity, + padding.top / mScreenDensity, padding.right / mScreenDensity, padding.bottom / mScreenDensity), + direction, duration); + } + + /** + * Gets the currently selected marker. + * + * @return The currently selected marker. + */ + @UiThread + @Nullable + public List<Marker> getSelectedMarkers() { + return mSelectedMarkers; + } + + private void adjustTopOffsetPixels() { + int count = mAnnotations.size(); + for (int i = 0; i < count; i++) { + Annotation annotation = mAnnotations.get(i); + if (annotation instanceof Marker) { + Marker marker = (Marker) annotation; + marker.setTopOffsetPixels( + getTopOffsetPixelsForSprite(marker.getIcon())); + } + } + + for (Marker marker : mSelectedMarkers) { + if (marker.isInfoWindowShown()) { + Marker temp = marker; + temp.hideInfoWindow(); + temp.showInfoWindow(); + marker = temp; + } + } + } + + private void reloadMarkers() { + int count = mAnnotations.size(); + for (int i = 0; i < count; i++) { + Annotation annotation = mAnnotations.get(i); + if (annotation instanceof Marker) { + Marker marker = (Marker) annotation; + mNativeMapView.removeAnnotation(annotation.getId()); + long newId = mNativeMapView.addMarker(marker); + marker.setId(newId); + } + } + } + + // + // Rendering + // + + // Called when the map needs to be rerendered + // Called via JNI from NativeMapView + protected void onInvalidate() { + postInvalidate(); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (isInEditMode()) { + return; + } + + if (!mNativeMapView.isPaused()) { + mNativeMapView.renderSync(); + } + } + + @Override + protected void onSizeChanged(int width, int height, int oldw, int oldh) { + if (!isInEditMode()) { + mNativeMapView.resizeView((int) (width / mScreenDensity), (int) (height / mScreenDensity)); + } + } + + // This class handles TextureView callbacks + private class SurfaceTextureListener implements TextureView.SurfaceTextureListener { + + // Called when the native surface texture has been created + // Must do all EGL/GL ES initialization here + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + mNativeMapView.createSurface(new Surface(surface)); + mNativeMapView.resizeFramebuffer(width, height); + } + + // Called when the native surface texture has been destroyed + // Must do all EGL/GL ES destruction here + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + if (mNativeMapView != null) { + mNativeMapView.destroySurface(); + } + return true; + } + + // Called when the format or size of the native surface texture has been changed + // Must handle window resizing here. + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + mNativeMapView.resizeFramebuffer(width, height); + } + + // Called when the SurfaceTexure frame is drawn to screen + // Must sync with UI here + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + mCompassView.update(getDirection()); + mUserLocationView.update(); + for (InfoWindow infoWindow : mInfoWindows) { + infoWindow.update(); + } + } + } + + // Used by UserLocationView + void update() { + if (mNativeMapView != null) { + mNativeMapView.update(); + } + } + + // Used by UserLocationView + void setBearing(float bearing) { + mNativeMapView.setBearing(bearing, 100); + } + + // + // View events + // + + // Called when view is no longer connected + @Override + @CallSuper + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + // Required by ZoomButtonController (from Android SDK documentation) + if (mZoomControlsEnabled) { + mZoomButtonsController.setVisible(false); + } + } + + // Called when view is hidden and shown + @Override + protected void onVisibilityChanged(@NonNull View changedView, int visibility) { + // Required by ZoomButtonController (from Android SDK documentation) + if (mZoomControlsEnabled && (visibility != View.VISIBLE)) { + mZoomButtonsController.setVisible(false); + } + if (mZoomControlsEnabled && (visibility == View.VISIBLE) + && mZoomEnabled) { + mZoomButtonsController.setVisible(true); + } + } + + // + // Touch events + // + + /** + * <p> + * Sets the preference for whether all gestures should be enabled or disabled. + * </p> + * <p> + * This setting controls only user interactions with the map. If you set the value to false, + * you may still change the map location programmatically. + * </p> + * The default value is true. + * + * @param enabled If true, all gestures are available; otherwise, all gestures are disabled. + * @see MapView#setZoomEnabled(boolean) + * @see MapView#setScrollEnabled(boolean) + * @see MapView#setRotateEnabled(boolean) + * @see MapView#setTiltEnabled(boolean) + */ + public void setAllGesturesEnabled(boolean enabled) { + setZoomEnabled(enabled); + setScrollEnabled(enabled); + setRotateEnabled(enabled); + setTiltEnabled(enabled); + } + + // Called when user touches the screen, all positions are absolute + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + // Check and ignore non touch or left clicks + + if ((event.getButtonState() != 0) && (event.getButtonState() != MotionEvent.BUTTON_PRIMARY)) { + return false; + } + + // Check two finger gestures first + mRotateGestureDetector.onTouchEvent(event); + mScaleGestureDetector.onTouchEvent(event); + mShoveGestureDetector.onTouchEvent(event); + + // Handle two finger tap + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + // First pointer down + mNativeMapView.setGestureInProgress(true); + break; + + case MotionEvent.ACTION_POINTER_DOWN: + // Second pointer down + mTwoTap = event.getPointerCount() == 2; + break; + + case MotionEvent.ACTION_POINTER_UP: + // Second pointer up + break; + + case MotionEvent.ACTION_UP: + // First pointer up + long tapInterval = event.getEventTime() - event.getDownTime(); + boolean isTap = tapInterval <= ViewConfiguration.getTapTimeout(); + boolean inProgress = mRotateGestureDetector.isInProgress() + || mScaleGestureDetector.isInProgress() + || mShoveGestureDetector.isInProgress(); + + if (mTwoTap && isTap && !inProgress) { + PointF focalPoint = TwoFingerGestureDetector.determineFocalPoint(event); + zoom(false, focalPoint.x, focalPoint.y); + mTwoTap = false; + return true; + } + + mTwoTap = false; + mNativeMapView.setGestureInProgress(false); + break; + + case MotionEvent.ACTION_CANCEL: + mTwoTap = false; + mNativeMapView.setGestureInProgress(false); + break; + } + + boolean retVal = mGestureDetector.onTouchEvent(event); + return retVal || super.onTouchEvent(event); + } + + // This class handles one finger gestures + private class GestureListener extends + GestureDetector.SimpleOnGestureListener { + + // Must always return true otherwise all events are ignored + @Override + public boolean onDown(MotionEvent e) { + // Show the zoom controls + if (mZoomControlsEnabled && mZoomEnabled) { + mZoomButtonsController.setVisible(true); + } + + return true; + } + + // Called for double taps + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + if (!mZoomEnabled) { + return false; + } + + switch (e.getAction()) { + case MotionEvent.ACTION_DOWN: + break; + case MotionEvent.ACTION_MOVE: + break; + case MotionEvent.ACTION_UP: + if (mQuickZoom) { + mQuickZoom = false; + break; + } + + // Single finger double tap + if (mUserLocationView.getMyLocationTrackingMode() == MyLocationTracking.TRACKING_NONE) { + // Zoom in on gesture + zoom(true, e.getX(), e.getY()); + } else { + // Zoom in on center map + zoom(true, getWidth() / 2, getHeight() / 2); + } + break; + } + + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + // Cancel any animation + mNativeMapView.cancelTransitions(); + return true; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + // Open / Close InfoWindow + PointF tapPoint = new PointF(e.getX(), e.getY()); + + final float toleranceSides = 30 * mScreenDensity; + final float toleranceTop = 40 * mScreenDensity; + final float toleranceBottom = 10 * mScreenDensity; + + RectF tapRect = new RectF(tapPoint.x - toleranceSides, tapPoint.y + toleranceTop, + tapPoint.x + toleranceSides, tapPoint.y - toleranceBottom); + + List<LatLng> 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<Marker> nearbyMarkers = getMarkersInBounds(tapBounds); + + long newSelectedMarkerId; + + if (nearbyMarkers.size() > 0) { + + // there is at least one nearby marker; select one + // + // first, sort for comparison and iteration + Collections.sort(nearbyMarkers); + + if (nearbyMarkers == mMarkersNearLastTap) { + + // TODO: We still need to adapt this logic to the new mSelectedMarkers list, + // though the basic functionality is there. + + // the selection candidates haven't changed; cycle through them +// if (mSelectedMarker != null +// && (mSelectedMarker.getId() == mMarkersNearLastTap.get(mMarkersNearLastTap.size() - 1).getId())) { +// // the selected marker is the last in the set; cycle back to the first +// // note: this could be the selected marker if only one in set +// newSelectedMarkerId = mMarkersNearLastTap.get(0).getId(); +// } else if (mSelectedMarker != null) { +// // otherwise increment the selection through the candidates +// long result = mMarkersNearLastTap.indexOf(mSelectedMarker); +// newSelectedMarkerId = mMarkersNearLastTap.get((int) result + 1).getId(); +// } else { + // no current selection; select the first one + newSelectedMarkerId = mMarkersNearLastTap.get(0).getId(); +// } + } else { + // start tracking a new set of nearby markers + mMarkersNearLastTap = nearbyMarkers; + + // select the first one + newSelectedMarkerId = mMarkersNearLastTap.get(0).getId(); + } + + } else { + // there are no nearby markers; deselect if necessary + newSelectedMarkerId = -1; + } + + if (newSelectedMarkerId >= 0) { + + int count = mAnnotations.size(); + for (int i = 0; i < count; i++) { + Annotation annotation = mAnnotations.get(i); + if (annotation instanceof Marker) { + if (annotation.getId() == newSelectedMarkerId) { + if (mSelectedMarkers.isEmpty() || !mSelectedMarkers.contains(annotation)) { + selectMarker((Marker) annotation); + } + break; + } + } + } + + } else { + // deselect any selected marker + deselectMarkers(); + + // notify app of map click + if (mOnMapClickListener != null) { + LatLng point = fromScreenLocation(tapPoint); + mOnMapClickListener.onMapClick(point); + } + } + + return true; + } + + // Called for a long press + @Override + public void onLongPress(MotionEvent e) { + if (mOnMapLongClickListener != null && !mQuickZoom) { + LatLng point = fromScreenLocation(new PointF(e.getX(), e.getY())); + mOnMapLongClickListener.onMapLongClick(point); + } + } + + // Called for flings + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + if (!mScrollEnabled) { + return false; + } + + // Fling the map + float ease = 0.25f; + + velocityX = velocityX * ease; + velocityY = velocityY * ease; + + double speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); + double deceleration = 2500; + double duration = speed / (deceleration * ease); + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + mNativeMapView.moveBy(velocityX * duration / 2.0 / mScreenDensity, velocityY * duration / 2.0 / mScreenDensity, (long) (duration * 1000.0f)); + + if (mOnFlingListener != null) { + mOnFlingListener.onFling(); + } + + return true; + } + + // Called for drags + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + if (!mScrollEnabled) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Scroll the map + mNativeMapView.moveBy(-distanceX / mScreenDensity, -distanceY / mScreenDensity); + + if (mOnScrollListener != null) { + mOnScrollListener.onScroll(); + } + + return true; + } + } + + // This class handles two finger gestures and double-tap drag gestures + private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + + long mBeginTime = 0; + float mScaleFactor = 1.0f; + + // Called when two fingers first touch the screen + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + if (!mZoomEnabled) { + return false; + } + + mBeginTime = detector.getEventTime(); + return true; + } + + // Called when fingers leave screen + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mBeginTime = 0; + mScaleFactor = 1.0f; + mZoomStarted = false; + } + + // Called each time a finger moves + // Called for pinch zooms and quickzooms/quickscales + @Override + public boolean onScale(ScaleGestureDetector detector) { + if (!mZoomEnabled) { + return false; + } + + // If scale is large enough ignore a tap + mScaleFactor *= detector.getScaleFactor(); + if ((mScaleFactor > 1.05f) || (mScaleFactor < 0.95f)) { + mZoomStarted = true; + } + + // Ignore short touches in case it is a tap + // Also ignore small scales + long time = detector.getEventTime(); + long interval = time - mBeginTime; + if (!mZoomStarted && (interval <= ViewConfiguration.getTapTimeout())) { + return false; + } + + if (!mZoomStarted) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Gesture is a quickzoom if there aren't two fingers + mQuickZoom = !mTwoTap; + + // Scale the map + if (!mQuickZoom && mUserLocationView.getMyLocationTrackingMode() == MyLocationTracking.TRACKING_NONE) { + // around gesture + mNativeMapView.scaleBy(detector.getScaleFactor(), detector.getFocusX() / mScreenDensity, detector.getFocusY() / mScreenDensity); + } else { + // around center map + mNativeMapView.scaleBy(detector.getScaleFactor(), (getWidth() / 2) / mScreenDensity, (getHeight() / 2) / mScreenDensity); + } + return true; + } + } + + // This class handles two finger rotate gestures + private class RotateGestureListener extends RotateGestureDetector.SimpleOnRotateGestureListener { + + long mBeginTime = 0; + float mTotalAngle = 0.0f; + boolean mStarted = false; + + // Called when two fingers first touch the screen + @Override + public boolean onRotateBegin(RotateGestureDetector detector) { + if (!mRotateEnabled) { + return false; + } + + mBeginTime = detector.getEventTime(); + return true; + } + + // Called when the fingers leave the screen + @Override + public void onRotateEnd(RotateGestureDetector detector) { + mBeginTime = 0; + mTotalAngle = 0.0f; + mStarted = false; + } + + // Called each time one of the two fingers moves + // Called for rotation + @Override + public boolean onRotate(RotateGestureDetector detector) { + if (!mRotateEnabled) { + return false; + } + + // If rotate is large enough ignore a tap + // Also is zoom already started, don't rotate + mTotalAngle += detector.getRotationDegreesDelta(); + if (!mZoomStarted && ((mTotalAngle > 10.0f) || (mTotalAngle < -10.0f))) { + mStarted = true; + } + + // Ignore short touches in case it is a tap + // Also ignore small rotate + long time = detector.getEventTime(); + long interval = time - mBeginTime; + if (!mStarted && (interval <= ViewConfiguration.getTapTimeout())) { + return false; + } + + if (!mStarted) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Get rotate value + double bearing = mNativeMapView.getBearing(); + bearing += detector.getRotationDegreesDelta(); + + // Rotate the map + if (mUserLocationView.getMyLocationTrackingMode() == MyLocationTracking.TRACKING_NONE) { + // around gesture + mNativeMapView.setBearing(bearing, + detector.getFocusX() / mScreenDensity, + (getHeight() - detector.getFocusY()) / mScreenDensity); + } else { + // around center map + mNativeMapView.setBearing(bearing, + (getWidth() / 2) / mScreenDensity, + (getHeight() / 2) / mScreenDensity); + } + return true; + } + } + + // This class handles a vertical two-finger shove. (If you place two fingers on screen with + // less than a 20 degree angle between them, this will detect movement on the Y-axis.) + private class ShoveGestureListener implements ShoveGestureDetector.OnShoveGestureListener { + + long mBeginTime = 0; + float mTotalDelta = 0.0f; + boolean mStarted = false; + + @Override + public boolean onShoveBegin(ShoveGestureDetector detector) { + if (!mTiltEnabled) { + return false; + } + + mBeginTime = detector.getEventTime(); + return true; + } + + @Override + public void onShoveEnd(ShoveGestureDetector detector) { + mBeginTime = 0; + mTotalDelta = 0.0f; + mStarted = false; + } + + @Override + public boolean onShove(ShoveGestureDetector detector) { + if (!mTiltEnabled) { + return false; + } + + // If tilt is large enough ignore a tap + // Also if zoom already started, don't tilt + mTotalDelta += detector.getShovePixelsDelta(); + if (!mZoomStarted && ((mTotalDelta > 10.0f) || (mTotalDelta < -10.0f))) { + mStarted = true; + } + + // Ignore short touches in case it is a tap + // Also ignore small tilt + long time = detector.getEventTime(); + long interval = time - mBeginTime; + if (!mStarted && (interval <= ViewConfiguration.getTapTimeout())) { + return false; + } + + if (!mStarted) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Get tilt value (scale and clamp) + double pitch = getTilt(); + pitch -= 0.1 * detector.getShovePixelsDelta(); + pitch = Math.max(MINIMUM_TILT, Math.min(MAXIMUM_TILT, pitch)); + + // Tilt the map + setTilt(pitch, null); + + return true; + } + } + + // This class handles input events from the zoom control buttons + // Zoom controls allow single touch only devices to zoom in and out + private class OnZoomListener implements ZoomButtonsController.OnZoomListener { + + // Not used + @Override + public void onVisibilityChanged(boolean visible) { + // Ignore + } + + // Called when user pushes a zoom button + @Override + public void onZoom(boolean zoomIn) { + if (!mZoomEnabled) { + return; + } + + // Zoom in or out + zoom(zoomIn); + } + } + + // + // Input events + // + + // Called when the user presses a key, also called for repeating keys held + // down + @Override + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + // If the user has held the scroll key down for a while then accelerate + // the scroll speed + double scrollDist = event.getRepeatCount() >= 5 ? 50.0 : 10.0; + + // Check which key was pressed via hardware/real key code + switch (keyCode) { + // Tell the system to track these keys for long presses on + // onKeyLongPress is fired + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + event.startTracking(); + return true; + + case KeyEvent.KEYCODE_DPAD_LEFT: + if (!mScrollEnabled) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Move left + mNativeMapView.moveBy(scrollDist / mScreenDensity, 0.0 / mScreenDensity); + return true; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (!mScrollEnabled) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Move right + mNativeMapView.moveBy(-scrollDist / mScreenDensity, 0.0 / mScreenDensity); + return true; + + case KeyEvent.KEYCODE_DPAD_UP: + if (!mScrollEnabled) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Move up + mNativeMapView.moveBy(0.0 / mScreenDensity, scrollDist / mScreenDensity); + return true; + + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!mScrollEnabled) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Move down + mNativeMapView.moveBy(0.0 / mScreenDensity, -scrollDist / mScreenDensity); + return true; + + default: + // We are not interested in this key + return super.onKeyUp(keyCode, event); + } + } + + // Called when the user long presses a key that is being tracked + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + // Check which key was pressed via hardware/real key code + switch (keyCode) { + // Tell the system to track these keys for long presses on + // onKeyLongPress is fired + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (!mZoomEnabled) { + return false; + } + + // Zoom out + zoom(false); + return true; + + default: + // We are not interested in this key + return super.onKeyUp(keyCode, event); + } + } + + // Called when the user releases a key + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + // Check if the key action was canceled (used for virtual keyboards) + if (event.isCanceled()) { + return super.onKeyUp(keyCode, event); + } + + // Check which key was pressed via hardware/real key code + // Note if keyboard does not have physical key (ie primary non-shifted + // key) then it will not appear here + // Must use the key character map as physical to character is not + // fixed/guaranteed + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (!mZoomEnabled) { + return false; + } + + // Zoom in + zoom(true); + return true; + } + + // We are not interested in this key + return super.onKeyUp(keyCode, event); + } + + // Called for trackball events, all motions are relative in device specific + // units + @Override + public boolean onTrackballEvent(MotionEvent event) { + // Choose the action + switch (event.getActionMasked()) { + // The trackball was rotated + case MotionEvent.ACTION_MOVE: + if (!mScrollEnabled) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Scroll the map + mNativeMapView.moveBy(-10.0 * event.getX() / mScreenDensity, -10.0 * event.getY() / mScreenDensity); + return true; + + // Trackball was pushed in so start tracking and tell system we are + // interested + // We will then get the up action + case MotionEvent.ACTION_DOWN: + // Set up a delayed callback to check if trackball is still + // After waiting the system long press time out + if (mCurrentTrackballLongPressTimeOut != null) { + mCurrentTrackballLongPressTimeOut.cancel(); + mCurrentTrackballLongPressTimeOut = null; + } + mCurrentTrackballLongPressTimeOut = new TrackballLongPressTimeOut(); + postDelayed(mCurrentTrackballLongPressTimeOut, + ViewConfiguration.getLongPressTimeout()); + return true; + + // Trackball was released + case MotionEvent.ACTION_UP: + if (!mZoomEnabled) { + return false; + } + + // Only handle if we have not already long pressed + if (mCurrentTrackballLongPressTimeOut != null) { + // Zoom in + zoom(true); + } + return true; + + // Trackball was cancelled + case MotionEvent.ACTION_CANCEL: + if (mCurrentTrackballLongPressTimeOut != null) { + mCurrentTrackballLongPressTimeOut.cancel(); + mCurrentTrackballLongPressTimeOut = null; + } + return true; + + default: + // We are not interested in this event + return super.onTrackballEvent(event); + } + } + + // This class implements the trackball long press time out callback + private class TrackballLongPressTimeOut implements Runnable { + + // Track if we have been cancelled + private boolean cancelled; + + public TrackballLongPressTimeOut() { + cancelled = false; + } + + // Cancel the timeout + public void cancel() { + cancelled = true; + } + + // Called when long press time out expires + @Override + public void run() { + // Check if the trackball is still pressed + if (!cancelled) { + // Zoom out + zoom(false); + + // Ensure the up action is not run + mCurrentTrackballLongPressTimeOut = null; + } + } + } + + // Called for events that don't fit the other handlers + // such as mouse scroll events, mouse moves, joystick, trackpad + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + // Mouse events + //if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) { // this is not available before API 18 + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == InputDevice.SOURCE_CLASS_POINTER) { + // Choose the action + switch (event.getActionMasked()) { + // Mouse scrolls + case MotionEvent.ACTION_SCROLL: + if (!mZoomEnabled) { + return false; + } + + // Cancel any animation + mNativeMapView.cancelTransitions(); + + // Get the vertical scroll amount, one click = 1 + float scrollDist = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + + // Scale the map by the appropriate power of two factor + mNativeMapView.scaleBy(Math.pow(2.0, scrollDist), event.getX() / mScreenDensity, event.getY() / mScreenDensity); + + return true; + + default: + // We are not interested in this event + return super.onGenericMotionEvent(event); + } + } + + // We are not interested in this event + return super.onGenericMotionEvent(event); + } + + // Called when the mouse pointer enters or exits the view + // or when it fades in or out due to movement + @Override + public boolean onHoverEvent(@NonNull MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + // Show the zoom controls + if (mZoomControlsEnabled && mZoomEnabled) { + mZoomButtonsController.setVisible(true); + } + return true; + + case MotionEvent.ACTION_HOVER_EXIT: + // Hide the zoom controls + if (mZoomControlsEnabled) { + mZoomButtonsController.setVisible(false); + } + + default: + // We are not interested in this event + return super.onHoverEvent(event); + } + } + + // + // Connectivity events + // + + // This class handles connectivity changes + private class ConnectivityReceiver extends BroadcastReceiver { + + // Called when an action we are listening to in the manifest has been sent + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { + boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + onConnectivityChanged(!noConnectivity); + } + } + } + + // Called when our Internet connectivity has changed + private void onConnectivityChanged(boolean isConnected) { + mNativeMapView.setReachability(isConnected); + } + + // + // Map events + // + + /** + * <p> + * Add a callback that's invoked when the displayed map view changes. + * </p> + * To remove the callback, use {@link MapView#removeOnMapChangedListener(OnMapChangedListener)}. + * + * @param listener The callback that's invoked on every frame rendered to the map view. + * @see MapView#removeOnMapChangedListener(OnMapChangedListener) + */ + @UiThread + public void addOnMapChangedListener(@Nullable OnMapChangedListener listener) { + if (listener != null) { + mOnMapChangedListener.add(listener); + } + } + + /** + * Remove a callback added with {@link MapView#addOnMapChangedListener(OnMapChangedListener)} + * + * @param listener The previously added callback to remove. + * @see MapView#addOnMapChangedListener(OnMapChangedListener) + */ + @UiThread + public void removeOnMapChangedListener(@Nullable OnMapChangedListener listener) { + if (listener != null) { + mOnMapChangedListener.remove(listener); + } + } + + // Called when the map view transformation has changed + // Called via JNI from NativeMapView + // Forward to any listeners + protected void onMapChanged(int mapChange) { + if (mOnMapChangedListener != null) { + int count = mOnMapChangedListener.size(); + for (int i = 0; i < count; i++) { + mOnMapChangedListener.get(i).onMapChanged(mapChange); + } + } + } + + /** + * <p> + * Sets a custom renderer for the contents of info window. + * </p> + * When set your callback is invoked when an info window is about to be shown. By returning + * a custom {@link View}, the default info window will be replaced. + * + * @param infoWindowAdapter The callback to be invoked when an info window will be shown. + * To unset the callback, use null. + */ + @UiThread + public void setInfoWindowAdapter(@Nullable InfoWindowAdapter infoWindowAdapter) { + mInfoWindowAdapter = infoWindowAdapter; + } + + /** + * Gets the callback to be invoked when an info window will be shown. + * + * @return The callback to be invoked when an info window will be shown. + */ + @UiThread + @Nullable + public InfoWindowAdapter getInfoWindowAdapter() { + return mInfoWindowAdapter; + } + + + /** + * Sets a callback that's invoked on every frame rendered to the map view. + * + * @param listener The callback that's invoked on every frame rendered to the map view. + * To unset the callback, use null. + */ + @UiThread + public void setOnFpsChangedListener(@Nullable OnFpsChangedListener listener) { + mOnFpsChangedListener = listener; + } + + // Called when debug mode is enabled to update a FPS counter + // Called via JNI from NativeMapView + // Forward to any listener + protected void onFpsChanged(final double fps) { + post(new Runnable() { + @Override + public void run() { + if (mOnFpsChangedListener != null) { + mOnFpsChangedListener.onFpsChanged(fps); + } + } + }); + } + + /** + * Sets a callback that's invoked when the map is scrolled. + * + * @param listener The callback that's invoked when the map is scrolled. + * To unset the callback, use null. + */ + @UiThread + public void setOnScrollListener(@Nullable OnScrollListener listener) { + mOnScrollListener = listener; + } + + /** + * Sets a callback that's invoked when the map is flinged. + * + * @param listener The callback that's invoked when the map is flinged. + * To unset the callback, use null. + */ + @UiThread + public void setOnFlingListener(@Nullable OnFlingListener listener) { + mOnFlingListener = listener; + } + + /** + * Sets a callback that's invoked when the user clicks on the map view. + * + * @param listener The callback that's invoked when the user clicks on the map view. + * To unset the callback, use null. + */ + @UiThread + public void setOnMapClickListener(@Nullable OnMapClickListener listener) { + mOnMapClickListener = listener; + } + + /** + * Sets a callback that's invoked when the user long clicks on the map view. + * + * @param listener The callback that's invoked when the user long clicks on the map view. + * To unset the callback, use null. + */ + @UiThread + public void setOnMapLongClickListener(@Nullable OnMapLongClickListener listener) { + mOnMapLongClickListener = listener; + } + + /** + * Sets a callback that's invoked when the user clicks on a marker. + * + * @param listener The callback that's invoked when the user clicks on a marker. + * To unset the callback, use null. + */ + @UiThread + public void setOnMarkerClickListener(@Nullable OnMarkerClickListener listener) { + mOnMarkerClickListener = listener; + } + + /** + * Sets a callback that's invoked when the user clicks on an info window. + * + * @return The callback that's invoked when the user clicks on an info window. + */ + @UiThread + @Nullable + public OnInfoWindowClickListener getOnInfoWindowClickListener() { + return mOnInfoWindowClickListener; + } + + /** + * Sets a callback that's invoked when the user clicks on an info window. + * + * @param listener The callback that's invoked when the user clicks on an info window. + * To unset the callback, use null. + */ + @UiThread + public void setOnInfoWindowClickListener(@Nullable OnInfoWindowClickListener listener) { + mOnInfoWindowClickListener = listener; + } + + // + // User location + // + + /** + * Returns the status of the my-location layer. + * + * @return True if the my-location layer is enabled, false otherwise. + */ + @UiThread + public boolean isMyLocationEnabled() { + return mUserLocationView.isEnabled(); + } + + /** + * <p> + * Enables or disables the my-location layer. + * While enabled, the my-location layer continuously draws an indication of a user's current + * location and bearing. + * </p> + * In order to use the my-location layer feature you need to request permission for either + * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} + * or @link android.Manifest.permission#ACCESS_FINE_LOCATION. + * + * @param enabled True to enable; false to disable. + */ + @UiThread + public void setMyLocationEnabled(boolean enabled) { + mUserLocationView.setEnabled(enabled); + } + + /** + * Returns the currently displayed user location, or null if there is no location data available. + * + * @return The currently displayed user location. + */ + @UiThread + @Nullable + public Location getMyLocation() { + return mUserLocationView.getLocation(); + } + + /** + * Sets a callback that's invoked when the the My Location dot + * (which signifies the user's location) changes location. + * + * @param listener The callback that's invoked when the user clicks on a marker. + * To unset the callback, use null. + */ + @UiThread + public void setOnMyLocationChangeListener(@Nullable OnMyLocationChangeListener listener) { + mUserLocationView.setOnMyLocationChangeListener(listener); + } + + /** + * <p> + * Set the current my location tracking mode. + * Tracking my location disables gestures and pans the viewport + * </p> + * See {@link MyLocationTracking} for different values. + * + * @param myLocationTrackingMode The location tracking mode to be used. + * @see MyLocationTracking + */ + @UiThread + public void setMyLocationTrackingMode(@MyLocationTracking.Mode int myLocationTrackingMode) { + mUserLocationView.setMyLocationTrackingMode(myLocationTrackingMode); + validateGesturesForTrackingModes(); + } + + private void validateGesturesForTrackingModes() { + int myLocationTrackingMode = mUserLocationView.getMyLocationTrackingMode(); + int myBearingTrackingMode = mUserLocationView.getMyBearingTrackingMode(); + + // Enable/disable gestures based on tracking mode + if (myLocationTrackingMode == MyLocationTracking.TRACKING_NONE) { + mScrollEnabled = true; + mRotateEnabled = true; + } else { + mScrollEnabled = false; + mRotateEnabled = (myBearingTrackingMode == MyBearingTracking.NONE); + } + } + + + /** + * Returns the current user location tracking mode. + * + * @return The current user location tracking mode. + * One of the values from {@link MyLocationTracking.Mode}. + * @see MyLocationTracking.Mode + */ + @UiThread + @MyLocationTracking.Mode + public int getMyLocationTrackingMode() { + return mUserLocationView.getMyLocationTrackingMode(); + } + + /** + * <p> + * Set the current my bearing tracking mode. + * </p> + * <p> + * Tracking the users bearing will disable gestures and shows the direction the user is heading. + * </p> + * <p> + * When location tracking is disabled the direction of {@link UserLocationView} is rotated + * When location tracking is enabled the {@link MapView} is rotated based on bearing value. + * </p> + * See {@link MyBearingTracking} for different values. + * + * @param myBearingTrackingMode The bearing tracking mode to be used. + * @see MyBearingTracking + */ + @UiThread + public void setMyBearingTrackingMode(@MyBearingTracking.Mode int myBearingTrackingMode) { + mUserLocationView.setMyBearingTrackingMode(myBearingTrackingMode); + validateGesturesForTrackingModes(); + } + + /** + * Returns the current user bearing tracking mode. + * See {@link MyBearingTracking} for possible return values. + * + * @return the current user bearing tracking mode. + * @see MyBearingTracking + */ + @UiThread + @MyLocationTracking.Mode + public int getMyBearingTrackingMode() { + //noinspection ResourceType + return mUserLocationView.getMyBearingTrackingMode(); + } + + // + // Compass + // + + /** + * Returns whether the compass is enabled. + * + * @return True if the compass is enabled; false if the compass is disabled. + */ + @UiThread + public boolean isCompassEnabled() { + return mCompassView.isEnabled(); + } + + /** + * <p> + * Enables or disables the compass. The compass is an icon on the map that indicates the + * direction of north on the map. When a user clicks + * the compass, the camera orients itself to its default orientation and fades away shortly + * after. If disabled, the compass will never be displayed. + * </p> + * By default, the compass is enabled. + * + * @param compassEnabled True to enable the compass; false to disable the compass. + */ + @UiThread + public void setCompassEnabled(boolean compassEnabled) { + mCompassView.setEnabled(compassEnabled); + } + + /** + * <p> + * Sets the gravity of the compass view. Use this to change the corner of the map view that the + * compass is displayed in. + * </p> + * By default, the compass is in the top right corner. + * + * @param gravity One of the values from {@link Gravity}. + * @see Gravity + */ + @UiThread + public void setCompassGravity(int gravity) { + setWidgetGravity(mCompassView, gravity); + } + + /** + * Sets the margins of the compass view. Use this to change the distance of the compass from the + * map view edge. + * + * @param left The left margin in pixels. + * @param top The top margin in pixels. + * @param right The right margin in pixels. + * @param bottom The bottom margin in pixels. + */ + @UiThread + public void setCompassMargins(int left, int top, int right, int bottom) { + setWidgetMargins(mCompassView, left, top, right, bottom); + } + + // + // Logo + // + + /** + * <p> + * Sets the gravity of the logo view. Use this to change the corner of the map view that the + * Mapbox logo is displayed in. + * </p> + * By default, the logo is in the bottom left corner. + * + * @param gravity One of the values from {@link Gravity}. + * @see Gravity + */ + @UiThread + public void setLogoGravity(int gravity) { + setWidgetGravity(mLogoView, gravity); + } + + /** + * Sets the margins of the logo view. Use this to change the distance of the Mapbox logo from the + * map view edge. + * + * @param left The left margin in pixels. + * @param top The top margin in pixels. + * @param right The right margin in pixels. + * @param bottom The bottom margin in pixels. + */ + @UiThread + public void setLogoMargins(int left, int top, int right, int bottom) { + setWidgetMargins(mLogoView, left, top, right, bottom); + } + + /** + * <p> + * Enables or disables the Mapbox logo. + * </p> + * By default, the compass is enabled. + * + * @param visibility True to enable the logo; false to disable the logo. + */ + @UiThread + public void setLogoVisibility(int visibility) { + mLogoView.setVisibility(visibility); + } + + // + // Attribution + // + + /** + * <p> + * Sets the gravity of the attribution button view. Use this to change the corner of the map + * view that the attribution button is displayed in. + * </p> + * By default, the attribution button is in the bottom left corner. + * + * @param gravity One of the values from {@link Gravity}. + * @see Gravity + */ + @UiThread + public void setAttributionGravity(int gravity) { + setWidgetGravity(mAttributionsView, gravity); + } + + /** + * Sets the margins of the attribution button view. Use this to change the distance of the + * attribution button from the map view edge. + * + * @param left The left margin in pixels. + * @param top The top margin in pixels. + * @param right The right margin in pixels. + * @param bottom The bottom margin in pixels. + */ + @UiThread + public void setAttributionMargins(int left, int top, int right, int bottom) { + setWidgetMargins(mAttributionsView, left, top, right, bottom); + } + + /** + * <p> + * Enables or disables the attribution button. The attribution is a button with an "i" than when + * clicked shows a menu with copyright and legal notices. The menu also inlcudes the "Improve + * this map" link which user can report map errors with. + * </p> + * By default, the attribution button is enabled. + * + * @param visibility True to enable the attribution button; false to disable the attribution button. + */ + @UiThread + public void setAttributionVisibility(int visibility) { + mAttributionsView.setVisibility(visibility); + } + + @UiThread + public void addCustomLayer(CustomLayer customLayer, String before) { + mNativeMapView.addCustomLayer(customLayer, before); + } + + private void setWidgetGravity(@NonNull final View view, int gravity) { + LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); + layoutParams.gravity = gravity; + view.setLayoutParams(layoutParams); + } + + private void setWidgetMargins(@NonNull final View view, int left, int top, int right, int bottom) { + LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); + layoutParams.setMargins(left, top, right, bottom); + view.setLayoutParams(layoutParams); + } + + private void setWidgetMargins(@NonNull final View view, float leftDp, float topDp, float rightDp, float bottomDp) { + LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); + layoutParams.setMargins((int) (leftDp * mScreenDensity), (int) (topDp * mScreenDensity), (int) (rightDp * mScreenDensity), (int) (bottomDp * mScreenDensity)); + view.setLayoutParams(layoutParams); + } + + private static class AttributionOnClickListener implements View.OnClickListener, DialogInterface.OnClickListener { + + private MapView mMapView; + + public AttributionOnClickListener(MapView mapView) { + mMapView = mapView; + } + + // Called when someone presses the attribution icon + @Override + public void onClick(View v) { + Context context = v.getContext(); + String[] items = context.getResources().getStringArray(R.array.attribution_names); + AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.AttributionAlertDialogStyle); + builder.setTitle(R.string.attributionsDialogTitle); + builder.setAdapter(new ArrayAdapter<>(context, R.layout.attribution_list_item, items), this); + builder.show(); + } + + // Called when someone selects an attribution, 'Improve this map' adds location data to the url + @Override + public void onClick(DialogInterface dialog, int which) { + Context context = ((Dialog) dialog).getContext(); + String url = context.getResources().getStringArray(R.array.attribution_links)[which]; + if (which == ATTRIBUTION_INDEX_IMPROVE_THIS_MAP) { + LatLng latLng = mMapView.getCenterCoordinate(); + url = String.format(url, latLng.getLongitude(), latLng.getLatitude(), (int) mMapView.getZoomLevel()); + } + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + context.startActivity(intent); + } + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/NativeMapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/NativeMapView.java new file mode 100644 index 0000000000..c142670775 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/NativeMapView.java @@ -0,0 +1,639 @@ +package com.mapbox.mapboxsdk.views; + +import android.graphics.PointF; +import android.graphics.RectF; +import android.view.Surface; + +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.Polygon; +import com.mapbox.mapboxsdk.annotations.Polyline; +import com.mapbox.mapboxsdk.geometry.BoundingBox; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngZoom; +import com.mapbox.mapboxsdk.geometry.ProjectedMeters; +import com.mapbox.mapboxsdk.layers.CustomLayer; + +import java.lang.ref.WeakReference; +import java.util.List; + +// Class that wraps the native methods for convenience +final class NativeMapView { + + // + // Static members + // + + // + // Instance members + // + + boolean mDestroyed = false; + + // Holds the pointer to JNI NativeMapView + private long mNativeMapViewPtr = 0; + + // Used for callbacks + private WeakReference<MapView> mMapView; + + // + // Static methods + // + + static { + System.loadLibrary("mapbox-gl"); + } + + // + // Constructors + // + + public NativeMapView(MapView mapView, String cachePath, String dataPath, String apkPath, float pixelRatio, int availableProcessors, long totalMemory) { + if (availableProcessors < 0) { + throw new IllegalArgumentException("availableProcessors cannot be negative."); + } + + if (totalMemory < 0) { + throw new IllegalArgumentException("totalMemory cannot be negative."); + } + + mMapView = new WeakReference<>(mapView); + + // Create the NativeMapView + mNativeMapViewPtr = nativeCreate(cachePath, dataPath, apkPath, pixelRatio, availableProcessors, totalMemory); + } + + // + // Methods + // + + public void destroy() { + nativeDestroy(mNativeMapViewPtr); + mNativeMapViewPtr = 0; + mMapView = null; + mDestroyed = true; + } + + public boolean wasDestroyed() { + return mDestroyed; + } + + public void initializeDisplay() { + nativeInitializeDisplay(mNativeMapViewPtr); + } + + public void terminateDisplay() { + nativeTerminateDisplay(mNativeMapViewPtr); + } + + public void initializeContext() { + nativeInitializeContext(mNativeMapViewPtr); + } + + public void terminateContext() { + nativeTerminateContext(mNativeMapViewPtr); + } + + public void createSurface(Surface surface) { + nativeCreateSurface(mNativeMapViewPtr, surface); + } + + public void destroySurface() { + nativeDestroySurface(mNativeMapViewPtr); + } + + public void pause() { + nativePause(mNativeMapViewPtr); + } + + public boolean isPaused() { + return nativeIsPaused(mNativeMapViewPtr); + } + + public void resume() { + nativeResume(mNativeMapViewPtr); + } + + public void update() { + nativeUpdate(mNativeMapViewPtr); + } + + public void renderSync() { + nativeRenderSync(mNativeMapViewPtr); + } + + public void resizeView(int width, int height) { + if (width < 0) { + throw new IllegalArgumentException("width cannot be negative."); + } + + if (height < 0) { + throw new IllegalArgumentException("height cannot be negative."); + } + + if (width > 65535) { + throw new IllegalArgumentException( + "width cannot be greater than 65535."); + } + + if (height > 65535) { + throw new IllegalArgumentException( + "height cannot be greater than 65535."); + } + nativeViewResize(mNativeMapViewPtr, width, height); + } + + public void resizeFramebuffer(int fbWidth, int fbHeight) { + if (fbWidth < 0) { + throw new IllegalArgumentException("fbWidth cannot be negative."); + } + + if (fbHeight < 0) { + throw new IllegalArgumentException("fbHeight cannot be negative."); + } + + if (fbWidth > 65535) { + throw new IllegalArgumentException( + "fbWidth cannot be greater than 65535."); + } + + if (fbHeight > 65535) { + throw new IllegalArgumentException( + "fbHeight cannot be greater than 65535."); + } + nativeFramebufferResize(mNativeMapViewPtr, fbWidth, fbHeight); + } + + public void addClass(String clazz) { + nativeAddClass(mNativeMapViewPtr, clazz); + } + + public void removeClass(String clazz) { + nativeRemoveClass(mNativeMapViewPtr, clazz); + } + + public boolean hasClass(String clazz) { + return nativeHasClass(mNativeMapViewPtr, clazz); + } + + public void setClasses(List<String> classes) { + nativeSetClasses(mNativeMapViewPtr, classes); + } + + public List<String> getClasses() { + return nativeGetClasses(mNativeMapViewPtr); + } + + public void setDefaultTransitionDuration() { + setDefaultTransitionDuration(0); + } + + public long getDefaultTransitionDuration() { + return nativeGetDefaultTransitionDuration(mNativeMapViewPtr); + } + + public void setDefaultTransitionDuration(long milliseconds) { + if (milliseconds < 0) { + throw new IllegalArgumentException( + "milliseconds cannot be negative."); + } + + nativeSetDefaultTransitionDuration(mNativeMapViewPtr, + milliseconds); + } + + public void setStyleUrl(String url) { + nativeSetStyleUrl(mNativeMapViewPtr, url); + } + + public void setStyleJson(String newStyleJson) { + setStyleJson(newStyleJson, ""); + } + + public void setStyleJson(String newStyleJson, String base) { + nativeSetStyleJson(mNativeMapViewPtr, newStyleJson, base); + } + + public String getStyleJson() { + return nativeGetStyleJson(mNativeMapViewPtr); + } + + public void setAccessToken(String accessToken) { + nativeSetAccessToken(mNativeMapViewPtr, accessToken); + } + + public String getAccessToken() { + return nativeGetAccessToken(mNativeMapViewPtr); + } + + public void cancelTransitions() { + nativeCancelTransitions(mNativeMapViewPtr); + } + + public void setGestureInProgress(boolean inProgress) { + nativeSetGestureInProgress(mNativeMapViewPtr, inProgress); + } + + public void moveBy(double dx, double dy) { + moveBy(dx, dy, 0); + } + + public void moveBy(double dx, double dy, long duration) { + nativeMoveBy(mNativeMapViewPtr, dx, dy, duration); + } + + public void setLatLng(LatLng latLng) { + setLatLng(latLng, 0); + } + + public void setLatLng(LatLng latLng, long duration) { + nativeSetLatLng(mNativeMapViewPtr, latLng, duration); + } + + public LatLng getLatLng() { + return nativeGetLatLng(mNativeMapViewPtr); + } + + public void resetPosition() { + nativeResetPosition(mNativeMapViewPtr); + } + + public double getPitch() { + return nativeGetPitch(mNativeMapViewPtr); + } + + public void setPitch(double pitch, long duration) { + nativeSetPitch(mNativeMapViewPtr, pitch, duration); + } + + public void scaleBy(double ds) { + scaleBy(ds, -1.0, -1.0); + } + + public void scaleBy(double ds, double cx, double cy) { + scaleBy(ds, cx, cy, 0); + } + + public void scaleBy(double ds, double cx, double cy, long duration) { + nativeScaleBy(mNativeMapViewPtr, ds, cx, cy, duration); + } + + public void setScale(double scale) { + setScale(scale, -1.0, -1.0); + } + + public void setScale(double scale, double cx, double cy) { + setScale(scale, cx, cy, 0); + } + + public void setScale(double scale, double cx, double cy, long duration) { + nativeSetScale(mNativeMapViewPtr, scale, cx, cy, duration); + } + + public double getScale() { + return nativeGetScale(mNativeMapViewPtr); + } + + public void setZoom(double zoom) { + setZoom(zoom, 0); + } + + public void setZoom(double zoom, long duration) { + nativeSetZoom(mNativeMapViewPtr, zoom, duration); + } + + public double getZoom() { + return nativeGetZoom(mNativeMapViewPtr); + } + + public void setLatLngZoom(LatLngZoom latLngZoom) { + setLatLngZoom(latLngZoom, 0); + } + + public void setLatLngZoom(LatLngZoom latLngZoom, long duration) { + nativeSetLatLngZoom(mNativeMapViewPtr, latLngZoom, duration); + } + + public LatLngZoom getLatLngZoom() { + return nativeGetLatLngZoom(mNativeMapViewPtr); + } + + public void resetZoom() { + nativeResetZoom(mNativeMapViewPtr); + } + + public double getMinZoom() { + return nativeGetMinZoom(mNativeMapViewPtr); + } + + public double getMaxZoom() { + return nativeGetMaxZoom(mNativeMapViewPtr); + } + + public void rotateBy(double sx, double sy, double ex, double ey) { + rotateBy(sx, sy, ex, ey, 0); + } + + public void rotateBy(double sx, double sy, double ex, double ey, + long duration) { + nativeRotateBy(mNativeMapViewPtr, sx, sy, ex, ey, duration); + } + + public void setBearing(double degrees) { + setBearing(degrees, 0); + } + + public void setBearing(double degrees, long duration) { + nativeSetBearing(mNativeMapViewPtr, degrees, duration); + } + + public void setBearing(double degrees, double cx, double cy) { + nativeSetBearing(mNativeMapViewPtr, degrees, cx, cy); + } + + public double getBearing() { + return nativeGetBearing(mNativeMapViewPtr); + } + + public void resetNorth() { + nativeResetNorth(mNativeMapViewPtr); + } + + public long addMarker(Marker marker) { + return nativeAddMarker(mNativeMapViewPtr, marker); + } + + public long[] addMarkers(List<Marker> markers) { + return nativeAddMarkers(mNativeMapViewPtr, markers); + } + + public long addPolyline(Polyline polyline) { + return nativeAddPolyline(mNativeMapViewPtr, polyline); + } + + public long[] addPolylines(List<Polyline> polylines) { + return nativeAddPolylines(mNativeMapViewPtr, polylines); + } + + public long addPolygon(Polygon polygon) { + return nativeAddPolygon(mNativeMapViewPtr, polygon); + } + + public long[] addPolygons(List<Polygon> polygon) { + return nativeAddPolygons(mNativeMapViewPtr, polygon); + } + + public void removeAnnotation(long id) { + nativeRemoveAnnotation(mNativeMapViewPtr, id); + } + + public void removeAnnotations(long[] ids) { + nativeRemoveAnnotations(mNativeMapViewPtr, ids); + } + + public long[] getAnnotationsInBounds(BoundingBox bbox) { + return nativeGetAnnotationsInBounds(mNativeMapViewPtr, bbox); + } + + public void addAnnotationIcon(String symbol, int width, int height, float scale, byte[] pixels) { + nativeAddAnnotationIcon(mNativeMapViewPtr, symbol, width, height, scale, pixels); + } + + public void setVisibleCoordinateBounds(LatLng[] coordinates, RectF padding, double direction, long duration) { + nativeSetVisibleCoordinateBounds(mNativeMapViewPtr, coordinates, padding, direction, duration); + } + + public void onLowMemory() { + nativeOnLowMemory(mNativeMapViewPtr); + } + + public void setDebug(boolean debug) { + nativeSetDebug(mNativeMapViewPtr, debug); + } + + public void cycleDebugOptions() { + nativeToggleDebug(mNativeMapViewPtr); + } + + public boolean getDebug() { + return nativeGetDebug(mNativeMapViewPtr); + } + + public boolean isFullyLoaded() { + return nativeIsFullyLoaded(mNativeMapViewPtr); + } + + public void setReachability(boolean status) { + nativeSetReachability(mNativeMapViewPtr, status); + } + + public double getMetersPerPixelAtLatitude(double lat, double zoom) { + return nativeGetMetersPerPixelAtLatitude(mNativeMapViewPtr, lat, zoom); + } + + public ProjectedMeters projectedMetersForLatLng(LatLng latLng) { + return nativeProjectedMetersForLatLng(mNativeMapViewPtr, latLng); + } + + public LatLng latLngForProjectedMeters(ProjectedMeters projectedMeters) { + return nativeLatLngForProjectedMeters(mNativeMapViewPtr, projectedMeters); + } + + public PointF pixelForLatLng(LatLng latLng) { + return nativePixelForLatLng(mNativeMapViewPtr, latLng); + } + + public LatLng latLngForPixel(PointF pixel) { + return nativeLatLngForPixel(mNativeMapViewPtr, pixel); + } + + public double getTopOffsetPixelsForAnnotationSymbol(String symbolName) { + return nativeGetTopOffsetPixelsForAnnotationSymbol(mNativeMapViewPtr, symbolName); + } + + public void addCustomLayer(CustomLayer customLayer, String before) { + nativeAddCustomLayer(mNativeMapViewPtr, customLayer, before); + } + + // + // Callbacks + // + + protected void onInvalidate() { + mMapView.get().onInvalidate(); + } + + protected void onMapChanged(int rawChange) { + mMapView.get().onMapChanged(rawChange); + } + + protected void onFpsChanged(double fps) { + mMapView.get().onFpsChanged(fps); + } + + // + // JNI methods + // + + private native long nativeCreate(String cachePath, String dataPath, String apkPath, float pixelRatio, int availableProcessors, long totalMemory); + + private native void nativeDestroy(long nativeMapViewPtr); + + private native void nativeInitializeDisplay(long nativeMapViewPtr); + + private native void nativeTerminateDisplay(long nativeMapViewPtr); + + private native void nativeInitializeContext(long nativeMapViewPtr); + + private native void nativeTerminateContext(long nativeMapViewPtr); + + private native void nativeCreateSurface(long nativeMapViewPtr, + Surface surface); + + private native void nativeDestroySurface(long nativeMapViewPtr); + + private native void nativePause(long nativeMapViewPtr); + + private native boolean nativeIsPaused(long nativeMapViewPtr); + + private native void nativeResume(long nativeMapViewPtr); + + private native void nativeUpdate(long nativeMapViewPtr); + + private native void nativeRenderSync(long nativeMapViewPtr); + + private native void nativeViewResize(long nativeMapViewPtr, int width, int height); + + private native void nativeFramebufferResize(long nativeMapViewPtr, int fbWidth, int fbHeight); + + private native void nativeAddClass(long nativeMapViewPtr, String clazz); + + private native void nativeRemoveClass(long nativeMapViewPtr, String clazz); + + private native boolean nativeHasClass(long nativeMapViewPtr, String clazz); + + private native void nativeSetClasses(long nativeMapViewPtr, + List<String> classes); + + private native List<String> nativeGetClasses(long nativeMapViewPtr); + + private native void nativeSetDefaultTransitionDuration( + long nativeMapViewPtr, long duration); + + private native long nativeGetDefaultTransitionDuration(long nativeMapViewPtr); + + private native void nativeSetStyleUrl(long nativeMapViewPtr, String url); + + private native void nativeSetStyleJson(long nativeMapViewPtr, + String newStyleJson, String base); + + private native String nativeGetStyleJson(long nativeMapViewPtr); + + private native void nativeSetAccessToken(long nativeMapViewPtr, String accessToken); + + private native String nativeGetAccessToken(long nativeMapViewPtr); + + private native void nativeCancelTransitions(long nativeMapViewPtr); + + private native void nativeSetGestureInProgress(long nativeMapViewPtr, boolean inProgress); + + private native void nativeMoveBy(long nativeMapViewPtr, double dx, + double dy, long duration); + + private native void nativeSetLatLng(long nativeMapViewPtr, LatLng latLng, + long duration); + + private native LatLng nativeGetLatLng(long nativeMapViewPtr); + + private native void nativeResetPosition(long nativeMapViewPtr); + + private native double nativeGetPitch(long nativeMapViewPtr); + + private native void nativeSetPitch(long nativeMapViewPtr, double pitch, long duration); + + private native void nativeScaleBy(long nativeMapViewPtr, double ds, + double cx, double cy, long duration); + + private native void nativeSetScale(long nativeMapViewPtr, double scale, + double cx, double cy, long duration); + + private native double nativeGetScale(long nativeMapViewPtr); + + private native void nativeSetZoom(long nativeMapViewPtr, double zoom, + long duration); + + private native double nativeGetZoom(long nativeMapViewPtr); + + private native void nativeSetLatLngZoom(long nativeMapViewPtr, + LatLngZoom lonLatZoom, long duration); + + private native LatLngZoom nativeGetLatLngZoom(long nativeMapViewPtr); + + private native void nativeResetZoom(long nativeMapViewPtr); + + private native double nativeGetMinZoom(long nativeMapViewPtr); + + private native double nativeGetMaxZoom(long nativeMapViewPtr); + + private native void nativeRotateBy(long nativeMapViewPtr, double sx, + double sy, double ex, double ey, long duration); + + private native void nativeSetBearing(long nativeMapViewPtr, double degrees, + long duration); + + private native void nativeSetBearing(long nativeMapViewPtr, double degrees, + double cx, double cy); + + private native double nativeGetBearing(long nativeMapViewPtr); + + private native void nativeResetNorth(long nativeMapViewPtr); + + private native long nativeAddMarker(long nativeMapViewPtr, Marker marker); + + private native long[] nativeAddMarkers(long nativeMapViewPtr, List<Marker> markers); + + private native long nativeAddPolyline(long nativeMapViewPtr, Polyline polyline); + + private native long[] nativeAddPolylines(long mNativeMapViewPtr, List<Polyline> polygon); + + private native long nativeAddPolygon(long mNativeMapViewPtr, Polygon polygon); + + private native long[] nativeAddPolygons(long mNativeMapViewPtr, List<Polygon> polygon); + + private native void nativeRemoveAnnotation(long nativeMapViewPtr, long id); + + private native void nativeRemoveAnnotations(long nativeMapViewPtr, long[] id); + + private native long[] nativeGetAnnotationsInBounds(long mNativeMapViewPtr, BoundingBox bbox); + + private native void nativeAddAnnotationIcon(long nativeMapViewPtr, String symbol, + int width, int height, float scale, byte[] pixels); + + private native void nativeSetVisibleCoordinateBounds(long mNativeMapViewPtr, LatLng[] coordinates, + RectF padding, double direction, long duration); + + private native void nativeOnLowMemory(long nativeMapViewPtr); + + private native void nativeSetDebug(long nativeMapViewPtr, boolean debug); + + private native void nativeToggleDebug(long nativeMapViewPtr); + + private native boolean nativeGetDebug(long nativeMapViewPtr); + + private native boolean nativeIsFullyLoaded(long nativeMapViewPtr); + + private native void nativeSetReachability(long nativeMapViewPtr, boolean status); + + private native double nativeGetMetersPerPixelAtLatitude(long nativeMapViewPtr, double lat, double zoom); + + private native ProjectedMeters nativeProjectedMetersForLatLng(long nativeMapViewPtr, LatLng latLng); + + private native LatLng nativeLatLngForProjectedMeters(long nativeMapViewPtr, ProjectedMeters projectedMeters); + + private native PointF nativePixelForLatLng(long nativeMapViewPtr, LatLng latLng); + + private native LatLng nativeLatLngForPixel(long nativeMapViewPtr, PointF pixel); + + private native double nativeGetTopOffsetPixelsForAnnotationSymbol(long nativeMapViewPtr, String symbolName); + + private native void nativeAddCustomLayer(long nativeMapViewPtr, CustomLayer customLayer, String before); +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/UserLocationView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/UserLocationView.java new file mode 100644 index 0000000000..ba48eb574f --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/UserLocationView.java @@ -0,0 +1,707 @@ +package com.mapbox.mapboxsdk.views; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.os.Build; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.constants.MyBearingTracking; +import com.mapbox.mapboxsdk.constants.MyLocationTracking; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.location.LocationListener; +import com.mapbox.mapboxsdk.location.LocationServices; + +/** + * This view shows the user's location, as determined from GPS, on the map + * as a dot annotation. + */ +final class UserLocationView extends View implements LocationListener { + + private MapView mMapView; + + private float mDensity; + + private boolean mShowMarker; + private boolean mShowDirection; + private boolean mShowAccuracy; + private boolean mStaleMarker; + + private PointF mMarkerScreenPoint; + private Matrix mMarkerScreenMatrix; + + private Paint mAccuracyPaintFill; + private Paint mAccuracyPaintStroke; + private Path mAccuracyPath; + private RectF mAccuracyBounds; + + private Drawable mUserLocationDrawable; + private RectF mUserLocationDrawableBoundsF; + private Rect mUserLocationDrawableBounds; + + private Drawable mUserLocationBearingDrawable; + private RectF mUserLocationBearingDrawableBoundsF; + private Rect mUserLocationBearingDrawableBounds; + + private Drawable mUserLocationStaleDrawable; + private RectF mUserLocationStaleDrawableBoundsF; + private Rect mUserLocationStaleDrawableBounds; + + private Rect mDirtyRect; + private RectF mDirtyRectF; + + private LatLng mMarkerCoordinate; + private ValueAnimator mMarkerCoordinateAnimator; + private float mGpsMarkerDirection; + private float mCompassMarkerDirection; + private ObjectAnimator mMarkerDirectionAnimator; + private float mMarkerAccuracy; + private ObjectAnimator mMarkerAccuracyAnimator; + + private boolean mPaused = false; + private Location mUserLocation; + + MapView.OnMyLocationChangeListener mOnMyLocationChangeListener; + + @MyLocationTracking.Mode + private int mMyLocationTrackingMode; + + @MyBearingTracking.Mode + private int mMyBearingTrackingMode; + + // Compass data + private MyBearingListener mBearingChangeListener; + + public UserLocationView(Context context) { + super(context); + initialize(context); + } + + public UserLocationView(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(context); + } + + public UserLocationView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(context); + } + + private void initialize(Context context) { + // View configuration + setEnabled(false); + setWillNotDraw(false); + + // Layout params + ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + setLayoutParams(lp); + + // Setup sensors + mBearingChangeListener = new MyBearingListener(context); + + // Setup the custom paint + Resources resources = context.getResources(); + int accuracyColor = resources.getColor(R.color.my_location_ring); + + mDensity = resources.getDisplayMetrics().density; + mMarkerCoordinate = new LatLng(0.0, 0.0); + mMarkerScreenPoint = new PointF(); + mMarkerScreenMatrix = new Matrix(); + + mAccuracyPaintFill = new Paint(); + mAccuracyPaintFill.setAntiAlias(true); + mAccuracyPaintFill.setStyle(Paint.Style.FILL); + mAccuracyPaintFill.setColor(accuracyColor); + mAccuracyPaintFill.setAlpha((int) (255 * 0.25f)); + + mAccuracyPaintStroke = new Paint(); + mAccuracyPaintStroke.setAntiAlias(true); + mAccuracyPaintStroke.setStyle(Paint.Style.STROKE); + mAccuracyPaintStroke.setStrokeWidth(0.5f * mDensity); + mAccuracyPaintStroke.setColor(accuracyColor); + mAccuracyPaintStroke.setAlpha((int) (255 * 0.5f)); + + mAccuracyPath = new Path(); + mAccuracyBounds = new RectF(); + + mUserLocationDrawable = ContextCompat.getDrawable(getContext(), R.drawable.my_location); + mUserLocationDrawableBounds = new Rect( + -mUserLocationDrawable.getIntrinsicWidth() / 2, + -mUserLocationDrawable.getIntrinsicHeight() / 2, + mUserLocationDrawable.getIntrinsicWidth() / 2, + mUserLocationDrawable.getIntrinsicHeight() / 2); + mUserLocationDrawableBoundsF = new RectF( + -mUserLocationDrawable.getIntrinsicWidth() / 2, + -mUserLocationDrawable.getIntrinsicHeight() / 2, + mUserLocationDrawable.getIntrinsicWidth() / 2, + mUserLocationDrawable.getIntrinsicHeight() / 2); + mUserLocationDrawable.setBounds(mUserLocationDrawableBounds); + + mUserLocationBearingDrawable = ContextCompat.getDrawable(getContext(), R.drawable.my_location_bearing); + mUserLocationBearingDrawableBounds = new Rect( + -mUserLocationBearingDrawable.getIntrinsicWidth() / 2, + -mUserLocationBearingDrawable.getIntrinsicHeight() / 2, + mUserLocationBearingDrawable.getIntrinsicWidth() / 2, + mUserLocationBearingDrawable.getIntrinsicHeight() / 2); + mUserLocationBearingDrawableBoundsF = new RectF( + -mUserLocationBearingDrawable.getIntrinsicWidth() / 2, + -mUserLocationBearingDrawable.getIntrinsicHeight() / 2, + mUserLocationBearingDrawable.getIntrinsicWidth() / 2, + mUserLocationBearingDrawable.getIntrinsicHeight() / 2); + mUserLocationBearingDrawable.setBounds(mUserLocationBearingDrawableBounds); + + mUserLocationStaleDrawable = ContextCompat.getDrawable(getContext(), R.drawable.my_location_stale); + mUserLocationStaleDrawableBounds = new Rect( + -mUserLocationStaleDrawable.getIntrinsicWidth() / 2, + -mUserLocationStaleDrawable.getIntrinsicHeight() / 2, + mUserLocationStaleDrawable.getIntrinsicWidth() / 2, + mUserLocationStaleDrawable.getIntrinsicHeight() / 2); + mUserLocationStaleDrawableBoundsF = new RectF( + -mUserLocationStaleDrawable.getIntrinsicWidth() / 2, + -mUserLocationStaleDrawable.getIntrinsicHeight() / 2, + mUserLocationStaleDrawable.getIntrinsicWidth() / 2, + mUserLocationStaleDrawable.getIntrinsicHeight() / 2); + mUserLocationStaleDrawable.setBounds(mUserLocationStaleDrawableBounds); + } + + public void setMapView(MapView mapView) { + mMapView = mapView; + } + + public void onStart() { + if (mMyBearingTrackingMode == MyBearingTracking.COMPASS) { + mBearingChangeListener.onStart(getContext()); + } + } + + public void onStop() { + mBearingChangeListener.onStop(); + cancelAnimations(); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (!mShowMarker) { + return; + } + + canvas.concat(mMarkerScreenMatrix); + + Drawable dotDrawable = mShowDirection ? mUserLocationBearingDrawable : mUserLocationDrawable; + dotDrawable = mStaleMarker ? mUserLocationStaleDrawable : dotDrawable; + // IMPORTANT also update in update() + RectF dotBounds = mShowDirection ? mUserLocationBearingDrawableBoundsF : mUserLocationDrawableBoundsF; + dotBounds = mStaleMarker ? mUserLocationStaleDrawableBoundsF : dotBounds; + + boolean willDraw = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN || !canvas.isHardwareAccelerated()) { + willDraw = mShowAccuracy && !mStaleMarker && !canvas.quickReject(mAccuracyPath, Canvas.EdgeType.AA); + } + willDraw |= !canvas.quickReject(dotBounds, Canvas.EdgeType.AA); + + if (willDraw) { + if (mShowAccuracy && !mStaleMarker) { + canvas.drawPath(mAccuracyPath, mAccuracyPaintFill); + canvas.drawPath(mAccuracyPath, mAccuracyPaintStroke); + } + dotDrawable.draw(canvas); + } + } + + public void setMyLocationTrackingMode(@MyLocationTracking.Mode int myLocationTrackingMode) { + mMyLocationTrackingMode = myLocationTrackingMode; + + if (myLocationTrackingMode != MyLocationTracking.TRACKING_NONE && mUserLocation != null) { + // center map directly if we have a location fix + mMapView.setCenterCoordinate(new LatLng(mUserLocation)); + } + } + + @MyLocationTracking.Mode + public int getMyLocationTrackingMode() { + return mMyLocationTrackingMode; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); + toggleGps(enabled); + } + + public void update() { + if (isEnabled() && mShowMarker) { + setVisibility(View.VISIBLE); + + mStaleMarker = isStale(mUserLocation); + + // compute new marker position + // TODO add JNI method that takes existing pointf + if (mMyLocationTrackingMode == MyLocationTracking.TRACKING_NONE) { + mMarkerScreenPoint = mMapView.toScreenLocation(mMarkerCoordinate); + mMarkerScreenMatrix.reset(); + mMarkerScreenMatrix.setTranslate( + mMarkerScreenPoint.x, + mMarkerScreenPoint.y); + } else if (mMyLocationTrackingMode == MyLocationTracking.TRACKING_FOLLOW) { + mMarkerScreenMatrix.setTranslate(getMeasuredWidth() / 2, getMeasuredHeight() / 2); + mMapView.setCenterCoordinate(mMarkerCoordinate, true); + } + + // rotate so arrow in points to bearing + if (mShowDirection) { + if (mMyBearingTrackingMode == MyBearingTracking.COMPASS && mMyLocationTrackingMode == MyLocationTracking.TRACKING_NONE) { + mMarkerScreenMatrix.preRotate(mCompassMarkerDirection + (float) mMapView.getDirection()); + } else if (mMyBearingTrackingMode == MyBearingTracking.GPS) { + if (mMyLocationTrackingMode == MyLocationTracking.TRACKING_NONE) { + mMarkerScreenMatrix.preRotate(mGpsMarkerDirection + (float) mMapView.getDirection()); + } else { + mMarkerScreenMatrix.preRotate(mGpsMarkerDirection); + } + } + } + + // adjust accuracy circle + if (mShowAccuracy && !mStaleMarker) { + mAccuracyPath.reset(); + mAccuracyPath.addCircle(0.0f, 0.0f, + (float) (mMarkerAccuracy / mMapView.getMetersPerPixelAtLatitude( + mMarkerCoordinate.getLatitude())), + Path.Direction.CW); + + mAccuracyPath.computeBounds(mAccuracyBounds, false); + mAccuracyBounds.inset(-1.0f, -1.0f); + } + + // invalidate changed pixels + if (mDirtyRect == null) { + mDirtyRect = new Rect(); + mDirtyRectF = new RectF(); + } else { + // the old marker location + invalidate(mDirtyRect); + } + + RectF dotBounds = mShowDirection ? mUserLocationBearingDrawableBoundsF : mUserLocationDrawableBoundsF; + dotBounds = mStaleMarker ? mUserLocationStaleDrawableBoundsF : dotBounds; + RectF largerBounds = mShowAccuracy && !mStaleMarker && mAccuracyBounds.contains(dotBounds) + ? mAccuracyBounds : dotBounds; + mMarkerScreenMatrix.mapRect(mDirtyRectF, largerBounds); + mDirtyRectF.roundOut(mDirtyRect); + invalidate(mDirtyRect); // the new marker location + } else { + setVisibility(View.INVISIBLE); + } + } + + public Location getLocation() { + return mUserLocation; + } + + /** + * Enabled / Disable GPS location updates along with updating the UI + * + * @param enableGps true if GPS is to be enabled, false if GPS is to be disabled + */ + private void toggleGps(boolean enableGps) { + + LocationServices locationServices = LocationServices.getLocationServices(getContext()); + + if (enableGps) { + // Set an initial location if one available + Location lastLocation = locationServices.getLastLocation(); + if (lastLocation != null) { + setLocation(lastLocation); + } + + // Register for Location Updates + locationServices.addLocationListener(this); + } else { + // Disable location and user dot + setLocation(null); + + // Deregister for Location Updates + locationServices.removeLocationListener(this); + } + + locationServices.toggleGPS(enableGps); + } + + public void setMyBearingTrackingMode(@MyBearingTracking.Mode int myBearingTrackingMode) { + mMyBearingTrackingMode = myBearingTrackingMode; + + if (myBearingTrackingMode == MyBearingTracking.COMPASS) { + mShowAccuracy = false; + mShowDirection = false; + mBearingChangeListener.onStart(getContext()); + } else { + mBearingChangeListener.onStop(); + if (myBearingTrackingMode == MyBearingTracking.GPS) { + mShowDirection = (mUserLocation != null) && mUserLocation.hasBearing(); + } else { + mShowDirection = false; + } + } + } + + @MyBearingTracking.Mode + public int getMyBearingTrackingMode() { + return mMyBearingTrackingMode; + } + + private class MyBearingListener implements SensorEventListener { + + private SensorManager mSensorManager; + private Sensor mAccelerometer; + private Sensor mMagnetometer; + private float[] mLastAccelerometer = new float[3]; + private float[] mLastMagnetometer = new float[3]; + private boolean mLastAccelerometerSet = false; + private boolean mLastMagnetometerSet = false; + private float[] mR = new float[9]; + private float[] mOrientation = new float[3]; + private float mCurrentDegree = 0f; + + // Controls the sensor update rate in milliseconds + private static final int UPDATE_RATE_MS = 300; + + // Compass data + private float mCompassBearing; + private long mCompassUpdateNextTimestamp = 0; + + public MyBearingListener(Context context) { + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + } + + public void onStart(Context context) { + mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); + mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); + } + + public void onStop() { + mSensorManager.unregisterListener(this, mAccelerometer); + mSensorManager.unregisterListener(this, mMagnetometer); + } + + public float getCompassBearing() { + return mCompassBearing; + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (mPaused) { + return; + } + + long currentTime = SystemClock.elapsedRealtime(); + if (currentTime < mCompassUpdateNextTimestamp) { + return; + } + + if (event.sensor == mAccelerometer) { + System.arraycopy(event.values, 0, mLastAccelerometer, 0, event.values.length); + mLastAccelerometerSet = true; + } else if (event.sensor == mMagnetometer) { + System.arraycopy(event.values, 0, mLastMagnetometer, 0, event.values.length); + mLastMagnetometerSet = true; + } + + if (mLastAccelerometerSet && mLastMagnetometerSet) { + SensorManager.getRotationMatrix(mR, null, mLastAccelerometer, mLastMagnetometer); + SensorManager.getOrientation(mR, mOrientation); + float azimuthInRadians = mOrientation[0]; + float azimuthInDegress = (float) (Math.toDegrees(azimuthInRadians) + 360) % 360; + mCompassBearing = mCurrentDegree; + mCurrentDegree = -azimuthInDegress; + } + mCompassUpdateNextTimestamp = currentTime + UPDATE_RATE_MS; + setCompass(mCompassBearing); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // TODO add accuracy to the equiation + } + } + + /** + * Callback method for receiving location updates from LocationServices. + * + * @param location The new Location data + */ + @Override + public void onLocationChanged(Location location) { + if (mPaused) { + return; + } + setLocation(location); + } + + private boolean isStale(Location location) { + if (location != null) { + long ageInNanos; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + ageInNanos = SystemClock.elapsedRealtimeNanos() - + location.getElapsedRealtimeNanos(); + } else { + ageInNanos = (System.currentTimeMillis() - location.getTime()) * 1000 * 1000; + } + final long oneMinuteInNanos = 60L * 1000 * 1000 * 1000; + return ageInNanos > oneMinuteInNanos; + } else { + return false; + } + } + + // Handles location updates from GPS + private void setLocation(Location location) { + // if null we should hide the marker + if (location == null) { + mShowMarker = false; + mShowDirection = false; + mShowAccuracy = false; + + cancelAnimations(); + + mUserLocation = null; + return; + } + + if (mMarkerCoordinateAnimator != null) { + mMarkerCoordinateAnimator.end(); + mMarkerCoordinateAnimator = null; + } + + if (mMarkerDirectionAnimator != null) { + mMarkerDirectionAnimator.end(); + mMarkerDirectionAnimator = null; + } + + if (mMarkerAccuracyAnimator != null) { + mMarkerAccuracyAnimator.end(); + mMarkerAccuracyAnimator = null; + } + + mShowMarker = true; + + LatLng previousCoordinate; + if (mUserLocation == null) { + previousCoordinate = new LatLng(location); + } else { + previousCoordinate = new LatLng(mUserLocation); + } + + if (mMyLocationTrackingMode == MyLocationTracking.TRACKING_NONE) { + // moving marker above map + mMarkerCoordinateAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); + mMarkerCoordinateAnimator.setDuration(1000); + mMarkerCoordinateAnimator.addUpdateListener(new MarkerCoordinateAnimatorListener( + previousCoordinate, new LatLng(location) + )); + mMarkerCoordinateAnimator.start(); + } else { + // moving map under the tracker + mMarkerCoordinate = new LatLng(location); + mMapView.setCenterCoordinate(mMarkerCoordinate, true); + } + + if (mMyLocationTrackingMode == MyLocationTracking.TRACKING_NONE && mMyBearingTrackingMode == MyBearingTracking.GPS) { + // show GPS direction + mShowDirection = location.hasBearing(); + if (mShowDirection) { + if (mUserLocation != null && mUserLocation.hasBearing()) { + mGpsMarkerDirection = mUserLocation.getBearing(); + } + float oldDir = mGpsMarkerDirection; + float newDir = location.getBearing(); + float diff = oldDir - newDir; + if (diff > 180.0f) { + newDir += 360.0f; + } else if (diff < -180.0f) { + newDir -= 360.f; + } + mMarkerDirectionAnimator = ObjectAnimator.ofFloat(this, "direction", oldDir, newDir); + mMarkerDirectionAnimator.setDuration(1000); + mMarkerDirectionAnimator.start(); + } + } else if (mMyLocationTrackingMode == MyLocationTracking.TRACKING_FOLLOW && mMyBearingTrackingMode == MyBearingTracking.GPS) { + // always show north & rotate map below + mShowDirection = true; + mGpsMarkerDirection = 0; + if (location.hasBearing()) { + mMapView.setBearing(location.getBearing()); + } + } + + mShowAccuracy = location.hasAccuracy(); + if (mShowAccuracy) { + if (mUserLocation != null && mUserLocation.hasAccuracy()) { + mMarkerAccuracy = mUserLocation.getAccuracy(); + } + mMarkerAccuracyAnimator = ObjectAnimator.ofFloat(this, "accuracy", location.getAccuracy()); + mMarkerAccuracyAnimator.setDuration(1000); + mMarkerAccuracyAnimator.start(); + } + + mUserLocation = location; + updateOnNextFrame(); + + if (mOnMyLocationChangeListener != null) { + mOnMyLocationChangeListener.onMyLocationChange(location); + } + } + + // handles compass sensor updates + private void setCompass(float bearing) { + if (mMyLocationTrackingMode == MyLocationTracking.TRACKING_NONE) { + // animate marker + mShowDirection = true; + float oldDir = mCompassMarkerDirection; + float newDir = bearing; + float diff = oldDir - newDir; + if (diff > 180.0f) { + newDir += 360.0f; + } else if (diff < -180.0f) { + newDir -= 360.f; + } + mMarkerDirectionAnimator = ObjectAnimator.ofFloat(this, "direction", oldDir, newDir); + mMarkerDirectionAnimator.setDuration(1000); + mMarkerDirectionAnimator.start(); + mCompassMarkerDirection = bearing; + + } else if (mMyLocationTrackingMode == MyLocationTracking.TRACKING_FOLLOW) { + cancelAnimations(); + if (mMyBearingTrackingMode == MyBearingTracking.COMPASS) { + // always show north & change map direction + mShowDirection = true; + mGpsMarkerDirection = 0; + mCompassMarkerDirection = 0; + mMapView.setBearing(bearing); + } + } + } + + void updateOnNextFrame() { + mMapView.update(); + } + + /** + * Called from MapView.onPause() + */ + public void pause() { + mPaused = true; + toggleGps(false); + } + + /** + * Called from MapView.onResume() + */ + public void resume() { + mPaused = false; + if (isEnabled()) { + toggleGps(true); + } + } + + public void setOnMyLocationChangeListener(@Nullable MapView.OnMyLocationChangeListener listener) { + mOnMyLocationChangeListener = listener; + } + + // public for animator only + public float getDirection() { + if (mMyBearingTrackingMode == MyBearingTracking.COMPASS) { + return mCompassMarkerDirection; + } + return mGpsMarkerDirection; + } + + // public for animator only + public void setDirection(float direction) { + if (mMyBearingTrackingMode == MyBearingTracking.COMPASS) { + mCompassMarkerDirection = direction % 360.0f; + } else { + mGpsMarkerDirection = direction % 360.0f; + } + updateOnNextFrame(); + } + + // public for animator only + public float getAccuracy() { + return mMarkerAccuracy; + } + + // public for animator only + public void setAccuracy(float accuracy) { + mMarkerAccuracy = accuracy; + updateOnNextFrame(); + } + + private class MarkerCoordinateAnimatorListener implements ValueAnimator.AnimatorUpdateListener { + + private double mFromLat; + private double mFromLng; + private double mToLat; + private double mToLng; + + private MarkerCoordinateAnimatorListener(LatLng from, LatLng to) { + mFromLat = from.getLatitude(); + mFromLng = from.getLongitude(); + mToLat = to.getLatitude(); + mToLng = to.getLongitude(); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float frac = animation.getAnimatedFraction(); + double latitude = mFromLat + (mToLat - mFromLat) * frac; + double longitude = mFromLng + (mToLng - mFromLng) * frac; + mMarkerCoordinate.setLatitude(latitude); + mMarkerCoordinate.setLongitude(longitude); + updateOnNextFrame(); + } + } + + public void cancelAnimations() { + if (mMarkerCoordinateAnimator != null) { + mMarkerCoordinateAnimator.cancel(); + mMarkerCoordinateAnimator = null; + } + + if (mMarkerDirectionAnimator != null) { + mMarkerDirectionAnimator.cancel(); + mMarkerDirectionAnimator = null; + } + + if (mMarkerAccuracyAnimator != null) { + mMarkerAccuracyAnimator.cancel(); + mMarkerAccuracyAnimator = null; + } + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/package-info.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/package-info.java new file mode 100644 index 0000000000..a17f268c01 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/package-info.java @@ -0,0 +1,5 @@ +/** + * This package contains the {@link com.mapbox.mapboxsdk.views.MapView} and related classes. + * {@code MapView} is the core component of this SDK and adds a map to your app. + */ +package com.mapbox.mapboxsdk.views; |