diff --git a/docs/Changelog.md b/docs/Changelog.md index 655010a9..66f35312 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -3,6 +3,7 @@ ## New since 0.6.0 - Mapsforge multiple map files [#208](https://github.com/mapsforge/vtm/issues/208) +- Improved gestures implementation [#253](https://github.com/mapsforge/vtm/issues/253) - Polygon label position enhancements [#80](https://github.com/mapsforge/vtm/issues/80) - vtm-web modules update [#51](https://github.com/mapsforge/vtm/issues/51) - Mapbox vector tiles [#57](https://github.com/mapsforge/vtm/issues/57) diff --git a/vtm-android-example/AndroidManifest.xml b/vtm-android-example/AndroidManifest.xml index 65e58a04..6f941254 100644 --- a/vtm-android-example/AndroidManifest.xml +++ b/vtm-android-example/AndroidManifest.xml @@ -59,6 +59,9 @@ + diff --git a/vtm-android-example/src/org/oscim/android/test/NewGesturesActivity.java b/vtm-android-example/src/org/oscim/android/test/NewGesturesActivity.java new file mode 100644 index 00000000..b058f8ad --- /dev/null +++ b/vtm-android-example/src/org/oscim/android/test/NewGesturesActivity.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 devemux86 + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.android.test; + +import org.oscim.map.Map; + +public class NewGesturesActivity extends MarkerOverlayActivity { + + public NewGesturesActivity() { + super(); + Map.NEW_GESTURES = true; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + // Revert gestures for other activities + Map.NEW_GESTURES = false; + } +} diff --git a/vtm-android-example/src/org/oscim/android/test/Samples.java b/vtm-android-example/src/org/oscim/android/test/Samples.java index 69ed562f..f5a430ff 100644 --- a/vtm-android-example/src/org/oscim/android/test/Samples.java +++ b/vtm-android-example/src/org/oscim/android/test/Samples.java @@ -53,6 +53,7 @@ public class Samples extends Activity { linearLayout.addView(createButton(VectorLayerMapActivity.class)); linearLayout.addView(createButton(MultiMapActivity.class)); linearLayout.addView(createButton(MapFragmentActivity.class)); + linearLayout.addView(createButton(NewGesturesActivity.class)); } private Button createButton(final Class clazz) { diff --git a/vtm-android/src/org/oscim/android/MapView.java b/vtm-android/src/org/oscim/android/MapView.java index 3c44954d..d65c3d93 100644 --- a/vtm-android/src/org/oscim/android/MapView.java +++ b/vtm-android/src/org/oscim/android/MapView.java @@ -1,5 +1,6 @@ /* * Copyright 2012 Hannes Janetzek + * Copyright 2016 devemux86 * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -21,11 +22,13 @@ import android.content.Context; import android.opengl.GLSurfaceView; import android.util.AttributeSet; import android.util.DisplayMetrics; +import android.view.GestureDetector; import org.oscim.android.canvas.AndroidGraphics; import org.oscim.android.gl.AndroidGL; import org.oscim.android.gl.GlConfigChooser; import org.oscim.android.input.AndroidMotionEvent; +import org.oscim.android.input.GestureHandler; import org.oscim.backend.CanvasAdapter; import org.oscim.backend.GLAdapter; import org.oscim.map.Map; @@ -51,6 +54,7 @@ public class MapView extends GLSurfaceView { } protected final AndroidMap mMap; + protected GestureDetector mGestureDetector; protected final AndroidMotionEvent mMotionEvent; public MapView(Context context) { @@ -91,6 +95,12 @@ public class MapView extends GLSurfaceView { mMap.clearMap(); mMap.updateMap(false); + if (!Map.NEW_GESTURES) { + GestureHandler gestureHandler = new GestureHandler(mMap); + mGestureDetector = new GestureDetector(context, gestureHandler); + mGestureDetector.setOnDoubleTapListener(gestureHandler); + } + mMotionEvent = new AndroidMotionEvent(); } @@ -112,6 +122,11 @@ public class MapView extends GLSurfaceView { if (!isClickable()) return false; + if (mGestureDetector != null) { + if (mGestureDetector.onTouchEvent(motionEvent)) + return true; + } + mMap.input.fire(null, mMotionEvent.wrap(motionEvent)); mMotionEvent.recycle(); return true; diff --git a/vtm-android/src/org/oscim/android/input/GestureHandler.java b/vtm-android/src/org/oscim/android/input/GestureHandler.java new file mode 100644 index 00000000..fc875a06 --- /dev/null +++ b/vtm-android/src/org/oscim/android/input/GestureHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013 Hannes Janetzek + * Copyright 2016 devemux86 + * + * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.android.input; + +import android.view.GestureDetector.OnDoubleTapListener; +import android.view.GestureDetector.OnGestureListener; +import android.view.MotionEvent; + +import org.oscim.event.Gesture; +import org.oscim.map.Map; + +public class GestureHandler implements OnGestureListener, OnDoubleTapListener { + private final AndroidMotionEvent mMotionEvent; + private final Map mMap; + + // Quick scale (double tap + swipe) + protected boolean quickScale; + + public GestureHandler(Map map) { + mMotionEvent = new AndroidMotionEvent(); + mMap = map; + } + + /* OnGestureListener */ + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return false; + } + + @Override + public void onShowPress(MotionEvent e) { + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + // Quick scale (no long press) + if (quickScale) + return; + + mMap.handleGesture(Gesture.LONG_PRESS, mMotionEvent.wrap(e)); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return false; + } + + @Override + public boolean onDown(MotionEvent e) { + quickScale = false; + + return mMap.handleGesture(Gesture.PRESS, mMotionEvent.wrap(e)); + } + + /* OnDoubleTapListener */ + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + return mMap.handleGesture(Gesture.TAP, mMotionEvent.wrap(e)); + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + int action = e.getActionMasked(); + + // Quick scale + quickScale = (action == MotionEvent.ACTION_MOVE); + + return false; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mMap.handleGesture(Gesture.DOUBLE_TAP, mMotionEvent.wrap(e)); + } +} diff --git a/vtm-gdx/src/org/oscim/gdx/GdxMap.java b/vtm-gdx/src/org/oscim/gdx/GdxMap.java index 4af31462..04913dcd 100644 --- a/vtm-gdx/src/org/oscim/gdx/GdxMap.java +++ b/vtm-gdx/src/org/oscim/gdx/GdxMap.java @@ -85,11 +85,11 @@ public abstract class GdxMap implements ApplicationListener { mMapRenderer.onSurfaceChanged(w, h); InputMultiplexer mux = new InputMultiplexer(); - mGestureDetector = new GestureDetector(new LayerHandler(mMap)); - mux.addProcessor(mGestureDetector); + if (!Map.NEW_GESTURES) { + mGestureDetector = new GestureDetector(new LayerHandler(mMap)); + mux.addProcessor(mGestureDetector); + } mux.addProcessor(new InputHandler(this)); - //mux.addProcessor(new GestureDetector(20, 0.5f, 2, 0.05f, - // new MapController(mMap))); mux.addProcessor(new MotionHandler(mMap)); Gdx.input.setInputProcessor(mux); diff --git a/vtm-gdx/src/org/oscim/gdx/MotionHandler.java b/vtm-gdx/src/org/oscim/gdx/MotionHandler.java index d5e59191..a4d23f3f 100644 --- a/vtm-gdx/src/org/oscim/gdx/MotionHandler.java +++ b/vtm-gdx/src/org/oscim/gdx/MotionHandler.java @@ -22,7 +22,6 @@ import com.badlogic.gdx.InputProcessor; import org.oscim.event.MotionEvent; import org.oscim.map.Map; -import org.oscim.utils.ArrayUtils; import java.util.Arrays; diff --git a/vtm-playground/src/org/oscim/test/NewGesturesTest.java b/vtm-playground/src/org/oscim/test/NewGesturesTest.java new file mode 100644 index 00000000..e0f3c0b4 --- /dev/null +++ b/vtm-playground/src/org/oscim/test/NewGesturesTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 devemux86 + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.test; + +import org.oscim.gdx.GdxMapApp; +import org.oscim.map.Map; + +public class NewGesturesTest extends MarkerLayerTest { + + public static void main(String[] args) { + Map.NEW_GESTURES = true; + + GdxMapApp.init(); + GdxMapApp.run(new NewGesturesTest()); + } +} diff --git a/vtm-tests/test/org/oscim/layers/MapEventLayerTest.java b/vtm-tests/test/org/oscim/layers/MapEventLayerTest.java index 8d8e390c..6cb3c789 100644 --- a/vtm-tests/test/org/oscim/layers/MapEventLayerTest.java +++ b/vtm-tests/test/org/oscim/layers/MapEventLayerTest.java @@ -18,14 +18,13 @@ import static org.mockito.Mockito.when; public class MapEventLayerTest { private MapEventLayer layer; - private Map mockMap; private ViewController mockViewport; private Animator mockAnimator; private ArgumentCaptor argumentCaptor; @Before public void setUp() throws Exception { - mockMap = Mockito.mock(Map.class); + Map mockMap = Mockito.mock(Map.class); mockViewport = Mockito.mock(ViewController.class); mockAnimator = Mockito.mock(Animator.class); layer = new MapEventLayer(mockMap); @@ -94,12 +93,12 @@ public class MapEventLayerTest { layer.onTouchEvent(new TestMotionEvent(MotionEvent.ACTION_UP, 1, 2)); } - class TestMotionEvent extends MotionEvent { + private class TestMotionEvent extends MotionEvent { final int action; final float x; final float y; - public TestMotionEvent(int action, float x, float y) { + TestMotionEvent(int action, float x, float y) { this.action = action; this.x = x; this.y = y; diff --git a/vtm/src/org/oscim/event/Gesture.java b/vtm/src/org/oscim/event/Gesture.java index 96d6960f..21b99887 100644 --- a/vtm/src/org/oscim/event/Gesture.java +++ b/vtm/src/org/oscim/event/Gesture.java @@ -34,7 +34,7 @@ public interface Gesture { final class TripleTap implements Gesture { } - class TwoFingerTap implements Gesture { + final class TwoFingerTap implements Gesture { } Gesture PRESS = new Press(); diff --git a/vtm/src/org/oscim/layers/AbstractMapEventLayer.java b/vtm/src/org/oscim/layers/AbstractMapEventLayer.java new file mode 100644 index 00000000..b53b600c --- /dev/null +++ b/vtm/src/org/oscim/layers/AbstractMapEventLayer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 devemux86 + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.layers; + +import org.oscim.map.Map; + +public abstract class AbstractMapEventLayer extends Layer { + + public AbstractMapEventLayer(Map map) { + super(map); + } + + public abstract void enableRotation(boolean enable); + + public abstract boolean rotationEnabled(); + + public abstract void enableTilt(boolean enable); + + public abstract boolean tiltEnabled(); + + public abstract void enableMove(boolean enable); + + public abstract boolean moveEnabled(); + + public abstract void enableZoom(boolean enable); + + public abstract boolean zoomEnabled(); + + public abstract void setFixOnCenter(boolean enable); +} diff --git a/vtm/src/org/oscim/layers/MapEventLayer.java b/vtm/src/org/oscim/layers/MapEventLayer.java index 2d8230b2..ea991fc8 100644 --- a/vtm/src/org/oscim/layers/MapEventLayer.java +++ b/vtm/src/org/oscim/layers/MapEventLayer.java @@ -22,15 +22,11 @@ import org.oscim.core.MapPosition; import org.oscim.core.Tile; import org.oscim.event.Event; import org.oscim.event.Gesture; +import org.oscim.event.GestureListener; import org.oscim.event.MotionEvent; import org.oscim.map.Map; import org.oscim.map.Map.InputListener; import org.oscim.map.ViewController; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Timer; -import java.util.TimerTask; import static org.oscim.backend.CanvasAdapter.dpi; import static org.oscim.utils.FastMath.withinSquaredDist; @@ -41,9 +37,7 @@ import static org.oscim.utils.FastMath.withinSquaredDist; * TODO rewrite using gesture primitives to build more complex gestures: * maybe something similar to this https://github.com/ucbvislab/Proton */ -public class MapEventLayer extends Layer implements InputListener { - - private static final Logger log = LoggerFactory.getLogger(MapEventLayer.class); +public class MapEventLayer extends AbstractMapEventLayer implements InputListener, GestureListener { private boolean mEnableRotate = true; private boolean mEnableTilt = true; @@ -62,12 +56,8 @@ public class MapEventLayer extends Layer implements InputListener { private boolean mDoTilt; private boolean mDown; + private boolean mDoubleTap; private boolean mDragZoom; - private boolean mTwoFingers; - private boolean mTwoFingersDone; - private int mTaps; - private long mStartDown; - private MotionEvent mLastTap; private float mPrevX1; private float mPrevY1; @@ -89,30 +79,18 @@ public class MapEventLayer extends Layer implements InputListener { private static final float PINCH_ROTATE_THRESHOLD = 0.2f; private static final float PINCH_ROTATE_THRESHOLD2 = 0.5f; - //TODO Should be initialized with platform specific defaults /** * 100 ms since start of move to reduce fling scroll */ - private static final long FLING_MIN_THRESHOLD = 100; - private static final long DOUBLE_TAP_THRESHOLD = 300; - private static final long LONG_PRESS_THRESHOLD = 500; + private static final float FLING_MIN_THREHSHOLD = 100; private final VelocityTracker mTracker; - private final Timer mTimer; - private TimerTask mTimerTask; private final MapPosition mapPosition = new MapPosition(); public MapEventLayer(Map map) { super(map); mTracker = new VelocityTracker(); - mTimer = new Timer(); - } - - @Override - public void onDetach() { - mTimer.cancel(); - mTimer.purge(); } @Override @@ -120,34 +98,42 @@ public class MapEventLayer extends Layer implements InputListener { onTouchEvent(motionEvent); } + @Override public void enableRotation(boolean enable) { mEnableRotate = enable; } + @Override public boolean rotationEnabled() { return mEnableRotate; } + @Override public void enableTilt(boolean enable) { mEnableTilt = enable; } + @Override public boolean tiltEnabled() { return mEnableTilt; } + @Override public void enableMove(boolean enable) { mEnableMove = enable; } + @Override public boolean moveEnabled() { return mEnableMove; } + @Override public void enableZoom(boolean enable) { mEnableScale = enable; } + @Override public boolean zoomEnabled() { return mEnableScale; } @@ -155,62 +141,29 @@ public class MapEventLayer extends Layer implements InputListener { /** * When enabled zoom- and rotation-gestures will not move the viewport. */ + @Override public void setFixOnCenter(boolean enable) { mFixOnCenter = enable; } - boolean onTouchEvent(final MotionEvent e) { + boolean onTouchEvent(MotionEvent e) { + int action = getAction(e); - final long time = e.getTime(); if (action == MotionEvent.ACTION_DOWN) { - if (mTimerTask != null) { - mTimerTask.cancel(); - mTimer.purge(); - mTimerTask = null; - } - mMap.handleGesture(Gesture.PRESS, e); - mDown = true; - mStartDown = time; - if (mTaps > 0) { - float mx = e.getX(0) - mLastTap.getX(); - float my = e.getY(0) - mLastTap.getY(); - if (isMinimalMove(mx, my)) { - mTaps = 0; - log.debug("tap {} {}", mLastTap.getX(), mLastTap.getY()); - mMap.handleGesture(Gesture.TAP, mLastTap); - } - } else { - mMap.animator().cancel(); + mMap.animator().cancel(); - mStartMove = -1; - mDragZoom = false; - mTwoFingers = false; - mTwoFingersDone = false; - - mTimerTask = new TimerTask() { - @Override - public void run() { - if (mTwoFingers || mStartMove != -1) - return; - mMap.post(new Runnable() { - @Override - public void run() { - log.debug("long press {} {}", e.getX(), e.getY()); - mMap.handleGesture(Gesture.LONG_PRESS, e); - } - }); - } - }; - mTimer.schedule(mTimerTask, LONG_PRESS_THRESHOLD); - } + mStartMove = -1; + mDoubleTap = false; + mDragZoom = false; mPrevX1 = e.getX(0); mPrevY1 = e.getY(0); + mDown = true; return true; } - if (!mDown) { + if (!(mDown || mDoubleTap)) { /* no down event received */ return false; } @@ -221,12 +174,17 @@ public class MapEventLayer extends Layer implements InputListener { } if (action == MotionEvent.ACTION_UP) { mDown = false; - if (mTimerTask != null) { - mTimerTask.cancel(); - mTimer.purge(); - mTimerTask = null; - } - if (mStartMove > 0) { + if (mDoubleTap && !mDragZoom) { + float pivotX = 0, pivotY = 0; + if (!mFixOnCenter) { + pivotX = mPrevX1 - mMap.getWidth() / 2; + pivotY = mPrevY1 - mMap.getHeight() / 2; + } + + /* handle double tap zoom */ + mMap.animator().animateZoom(300, 2, pivotX, pivotY); + + } else if (mStartMove > 0) { /* handle fling gesture */ mTracker.update(e.getX(), e.getY(), e.getTime()); float vx = mTracker.getVelocityX(); @@ -234,86 +192,16 @@ public class MapEventLayer extends Layer implements InputListener { /* reduce velocity for short moves */ float t = e.getTime() - mStartMove; - if (t < FLING_MIN_THRESHOLD) { - t = t / FLING_MIN_THRESHOLD; + if (t < FLING_MIN_THREHSHOLD) { + t = t / FLING_MIN_THREHSHOLD; vy *= t * t; vx *= t * t; } doFling(vx, vy); } - - if (time - mStartDown > LONG_PRESS_THRESHOLD) { - log.debug(" not a tap"); - // this was not a tap - mTaps = 0; - return true; - } - - if (mTaps > 0) { - if ((time - mLastTap.getTime()) >= DOUBLE_TAP_THRESHOLD) { - mTaps = 1; - log.debug("tap {} {}", mLastTap.getX(), mLastTap.getY()); - mMap.handleGesture(Gesture.TAP, mLastTap); - } else { - mTaps += 1; - } - } else { - mTaps = 1; - } - - if (mLastTap != null) { - mLastTap.recycle(); - } - mLastTap = e.copy(); - - if (mTaps == 3) { - mTaps = 0; - log.debug("triple tap {} {}", e.getX(), e.getY()); - mMap.handleGesture(Gesture.TRIPLE_TAP, e); - } else if (mTaps == 2) { - mTimerTask = new TimerTask() { - @Override - public void run() { - mTaps = 0; - if (mDragZoom) - return; - mMap.post(new Runnable() { - @Override - public void run() { - log.debug("double tap {} {}", e.getX(), e.getY()); - if (!mMap.handleGesture(Gesture.DOUBLE_TAP, e)) { - /* handle double tap zoom */ - final float pivotX = mFixOnCenter ? 0 : mPrevX1 - mMap.getWidth() / 2; - final float pivotY = mFixOnCenter ? 0 : mPrevY1 - mMap.getHeight() / 2; - mMap.animator().animateZoom(300, 2, pivotX, pivotY); - } - } - }); - } - }; - mTimer.schedule(mTimerTask, DOUBLE_TAP_THRESHOLD); - } else { - mTimerTask = new TimerTask() { - @Override - public void run() { - mTaps = 0; - if (!mTwoFingers && mStartMove == -1) { - mMap.post(new Runnable() { - @Override - public void run() { - log.debug("tap {} {}", e.getX(), e.getY()); - mMap.handleGesture(Gesture.TAP, e); - } - }); - } - } - }; - mTimer.schedule(mTimerTask, DOUBLE_TAP_THRESHOLD); - } return true; } if (action == MotionEvent.ACTION_CANCEL) { - mTaps = 0; return false; } if (action == MotionEvent.ACTION_POINTER_DOWN) { @@ -322,12 +210,6 @@ public class MapEventLayer extends Layer implements InputListener { return true; } if (action == MotionEvent.ACTION_POINTER_UP) { - if (e.getPointerCount() == 2 && !mTwoFingersDone) { - log.debug("two finger tap"); - if (!mMap.handleGesture(Gesture.TWO_FINGER_TAP, e)) { - mMap.animator().animateZoom(300, 0.5, 0f, 0f); - } - } updateMulti(e); return true; } @@ -355,7 +237,12 @@ public class MapEventLayer extends Layer implements InputListener { mPrevY1 = y1; /* double-tap drag zoom */ - if (mTaps == 1) { + if (mDoubleTap) { + /* just ignore first move event to set mPrevX/Y */ + if (!mDown) { + mDown = true; + return; + } if (!mDragZoom && !isMinimalMove(mx, my)) { mPrevX1 -= mx; mPrevY1 -= my; @@ -418,7 +305,6 @@ public class MapEventLayer extends Layer implements InputListener { mCanScale = false; mCanRotate = false; mDoTilt = true; - mTwoFingersDone = true; } } } @@ -445,7 +331,6 @@ public class MapEventLayer extends Layer implements InputListener { /* start rotate, disable tilt */ mDoRotate = true; mCanTilt = false; - mTwoFingersDone = true; mAngle = rad; } else if (!mDoScale) { @@ -465,7 +350,6 @@ public class MapEventLayer extends Layer implements InputListener { mDoRotate = true; mCanRotate = true; mAngle = rad; - mTwoFingersDone = true; } } @@ -481,7 +365,6 @@ public class MapEventLayer extends Layer implements InputListener { mCanTilt = false; mDoScale = true; - mTwoFingersDone = true; } } if (mDoScale || mDoRotate) { @@ -530,8 +413,6 @@ public class MapEventLayer extends Layer implements InputListener { mPrevY1 = e.getY(0); if (cnt == 2) { - mTwoFingers = true; - mDoScale = false; mDoRotate = false; mDoTilt = false; @@ -564,6 +445,15 @@ public class MapEventLayer extends Layer implements InputListener { return true; } + @Override + public boolean onGesture(Gesture g, MotionEvent e) { + if (g == Gesture.DOUBLE_TAP) { + mDoubleTap = true; + return true; + } + return false; + } + private static class VelocityTracker { /* sample window, 200ms */ private static final int MAX_MS = 200; diff --git a/vtm/src/org/oscim/layers/MapEventLayer2.java b/vtm/src/org/oscim/layers/MapEventLayer2.java new file mode 100644 index 00000000..bc04d631 --- /dev/null +++ b/vtm/src/org/oscim/layers/MapEventLayer2.java @@ -0,0 +1,643 @@ +/* + * Copyright 2013 Hannes Janetzek + * Copyright 2016 devemux86 + * Copyright 2016 Andrey Novikov + * + * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.layers; + +import org.oscim.core.MapPosition; +import org.oscim.core.Tile; +import org.oscim.event.Event; +import org.oscim.event.Gesture; +import org.oscim.event.MotionEvent; +import org.oscim.map.Map; +import org.oscim.map.Map.InputListener; +import org.oscim.map.ViewController; + +import java.util.Timer; +import java.util.TimerTask; + +import static org.oscim.backend.CanvasAdapter.dpi; +import static org.oscim.utils.FastMath.withinSquaredDist; + +/** + * Changes Viewport by handling move, fling, scale, rotation and tilt gestures. + *

+ * TODO rewrite using gesture primitives to build more complex gestures: + * maybe something similar to this https://github.com/ucbvislab/Proton + */ +public class MapEventLayer2 extends AbstractMapEventLayer implements InputListener { + + private boolean mEnableRotate = true; + private boolean mEnableTilt = true; + private boolean mEnableMove = true; + private boolean mEnableScale = true; + private boolean mFixOnCenter = false; + + /* possible state transitions */ + private boolean mCanScale; + private boolean mCanRotate; + private boolean mCanTilt; + + /* current gesture state */ + private boolean mDoRotate; + private boolean mDoScale; + private boolean mDoTilt; + + private boolean mDown; + private boolean mDragZoom; + private boolean mTwoFingers; + private boolean mTwoFingersDone; + private int mTaps; + private long mStartDown; + private MotionEvent mLastTap; + + private float mPrevX1; + private float mPrevY1; + private float mPrevX2; + private float mPrevY2; + + private double mAngle; + private double mPrevPinchWidth; + private long mStartMove; + + /** + * 2mm as minimal distance to start move: dpi / 25.4 + */ + private static final float MIN_SLOP = 25.4f / 2; + + private static final float PINCH_ZOOM_THRESHOLD = MIN_SLOP / 2; + private static final float PINCH_TILT_THRESHOLD = MIN_SLOP / 2; + private static final float PINCH_TILT_SLOPE = 0.75f; + private static final float PINCH_ROTATE_THRESHOLD = 0.2f; + private static final float PINCH_ROTATE_THRESHOLD2 = 0.5f; + + //TODO Should be initialized with platform specific defaults + /** + * 100 ms since start of move to reduce fling scroll + */ + private static final long FLING_MIN_THRESHOLD = 100; + private static final long DOUBLE_TAP_THRESHOLD = 300; + private static final long LONG_PRESS_THRESHOLD = 500; + + private final VelocityTracker mTracker; + private final Timer mTimer; + private TimerTask mTimerTask; + + private final MapPosition mapPosition = new MapPosition(); + + public MapEventLayer2(Map map) { + super(map); + mTracker = new VelocityTracker(); + mTimer = new Timer(); + } + + @Override + public void onDetach() { + mTimer.cancel(); + mTimer.purge(); + } + + @Override + public void onInputEvent(Event e, MotionEvent motionEvent) { + onTouchEvent(motionEvent); + } + + @Override + public void enableRotation(boolean enable) { + mEnableRotate = enable; + } + + @Override + public boolean rotationEnabled() { + return mEnableRotate; + } + + @Override + public void enableTilt(boolean enable) { + mEnableTilt = enable; + } + + @Override + public boolean tiltEnabled() { + return mEnableTilt; + } + + @Override + public void enableMove(boolean enable) { + mEnableMove = enable; + } + + @Override + public boolean moveEnabled() { + return mEnableMove; + } + + @Override + public void enableZoom(boolean enable) { + mEnableScale = enable; + } + + @Override + public boolean zoomEnabled() { + return mEnableScale; + } + + /** + * When enabled zoom- and rotation-gestures will not move the viewport. + */ + @Override + public void setFixOnCenter(boolean enable) { + mFixOnCenter = enable; + } + + private boolean onTouchEvent(final MotionEvent e) { + int action = getAction(e); + final long time = e.getTime(); + + if (action == MotionEvent.ACTION_DOWN) { + if (mTimerTask != null) { + mTimerTask.cancel(); + mTimer.purge(); + mTimerTask = null; + } + mMap.handleGesture(Gesture.PRESS, e); + mDown = true; + mStartDown = time; + if (mTaps > 0) { + float mx = e.getX(0) - mLastTap.getX(); + float my = e.getY(0) - mLastTap.getY(); + if (isMinimalMove(mx, my)) { + mTaps = 0; + //log.debug("tap {} {}", mLastTap.getX(), mLastTap.getY()); + mMap.handleGesture(Gesture.TAP, mLastTap); + } + } else { + mMap.animator().cancel(); + + mStartMove = -1; + mDragZoom = false; + mTwoFingers = false; + mTwoFingersDone = false; + + mTimerTask = new TimerTask() { + @Override + public void run() { + if (mTwoFingers || mStartMove != -1) + return; + mMap.post(new Runnable() { + @Override + public void run() { + //log.debug("long press {} {}", e.getX(), e.getY()); + mMap.handleGesture(Gesture.LONG_PRESS, e); + } + }); + } + }; + mTimer.schedule(mTimerTask, LONG_PRESS_THRESHOLD); + } + + mPrevX1 = e.getX(0); + mPrevY1 = e.getY(0); + + return true; + } + if (!mDown) { + /* no down event received */ + return false; + } + + if (action == MotionEvent.ACTION_MOVE) { + onActionMove(e); + return true; + } + if (action == MotionEvent.ACTION_UP) { + mDown = false; + if (mTimerTask != null) { + mTimerTask.cancel(); + mTimer.purge(); + mTimerTask = null; + } + if (mStartMove > 0) { + /* handle fling gesture */ + mTracker.update(e.getX(), e.getY(), e.getTime()); + float vx = mTracker.getVelocityX(); + float vy = mTracker.getVelocityY(); + + /* reduce velocity for short moves */ + float t = e.getTime() - mStartMove; + if (t < FLING_MIN_THRESHOLD) { + t = t / FLING_MIN_THRESHOLD; + vy *= t * t; + vx *= t * t; + } + doFling(vx, vy); + } + + if (time - mStartDown > LONG_PRESS_THRESHOLD) { + //log.debug(" not a tap"); + // this was not a tap + mTaps = 0; + return true; + } + + if (mTaps > 0) { + if ((time - mLastTap.getTime()) >= DOUBLE_TAP_THRESHOLD) { + mTaps = 1; + //log.debug("tap {} {}", mLastTap.getX(), mLastTap.getY()); + mMap.handleGesture(Gesture.TAP, mLastTap); + } else { + mTaps += 1; + } + } else { + mTaps = 1; + } + + if (mLastTap != null) { + mLastTap.recycle(); + } + mLastTap = e.copy(); + + if (mTaps == 3) { + mTaps = 0; + //log.debug("triple tap {} {}", e.getX(), e.getY()); + mMap.handleGesture(Gesture.TRIPLE_TAP, e); + } else if (mTaps == 2) { + mTimerTask = new TimerTask() { + @Override + public void run() { + mTaps = 0; + if (mDragZoom) + return; + mMap.post(new Runnable() { + @Override + public void run() { + //log.debug("double tap {} {}", e.getX(), e.getY()); + if (!mMap.handleGesture(Gesture.DOUBLE_TAP, e)) { + /* handle double tap zoom */ + final float pivotX = mFixOnCenter ? 0 : mPrevX1 - mMap.getWidth() / 2; + final float pivotY = mFixOnCenter ? 0 : mPrevY1 - mMap.getHeight() / 2; + mMap.animator().animateZoom(300, 2, pivotX, pivotY); + } + } + }); + } + }; + mTimer.schedule(mTimerTask, DOUBLE_TAP_THRESHOLD); + } else { + mTimerTask = new TimerTask() { + @Override + public void run() { + mTaps = 0; + if (!mTwoFingers && mStartMove == -1) { + mMap.post(new Runnable() { + @Override + public void run() { + //log.debug("tap {} {}", e.getX(), e.getY()); + mMap.handleGesture(Gesture.TAP, e); + } + }); + } + } + }; + mTimer.schedule(mTimerTask, DOUBLE_TAP_THRESHOLD); + } + return true; + } + if (action == MotionEvent.ACTION_CANCEL) { + mTaps = 0; + return false; + } + if (action == MotionEvent.ACTION_POINTER_DOWN) { + mStartMove = -1; + updateMulti(e); + return true; + } + if (action == MotionEvent.ACTION_POINTER_UP) { + if (e.getPointerCount() == 2 && !mTwoFingersDone) { + //log.debug("two finger tap"); + if (!mMap.handleGesture(Gesture.TWO_FINGER_TAP, e)) { + mMap.animator().animateZoom(300, 0.5, 0f, 0f); + } + } + updateMulti(e); + return true; + } + + return false; + } + + private static int getAction(MotionEvent e) { + return e.getAction() & MotionEvent.ACTION_MASK; + } + + private void onActionMove(MotionEvent e) { + ViewController mViewport = mMap.viewport(); + float x1 = e.getX(0); + float y1 = e.getY(0); + + float mx = x1 - mPrevX1; + float my = y1 - mPrevY1; + + float width = mMap.getWidth(); + float height = mMap.getHeight(); + + if (e.getPointerCount() < 2) { + mPrevX1 = x1; + mPrevY1 = y1; + + /* double-tap drag zoom */ + if (mTaps == 1) { + if (!mDragZoom && !isMinimalMove(mx, my)) { + mPrevX1 -= mx; + mPrevY1 -= my; + return; + } + + // TODO limit scale properly + mDragZoom = true; + mViewport.scaleMap(1 + my / (height / 6), 0, 0); + mMap.updateMap(true); + mStartMove = -1; + return; + } + + /* simple move */ + if (!mEnableMove) + return; + + if (mStartMove < 0) { + if (!isMinimalMove(mx, my)) { + mPrevX1 -= mx; + mPrevY1 -= my; + return; + } + + mStartMove = e.getTime(); + mTracker.start(x1, y1, mStartMove); + return; + } + mViewport.moveMap(mx, my); + mTracker.update(x1, y1, e.getTime()); + mMap.updateMap(true); + if (mMap.viewport().getMapPosition(mapPosition)) + mMap.events.fire(Map.MOVE_EVENT, mapPosition); + return; + } + mStartMove = -1; + + float x2 = e.getX(1); + float y2 = e.getY(1); + float dx = (x1 - x2); + float dy = (y1 - y2); + + double rotateBy = 0; + float scaleBy = 1; + float tiltBy = 0; + + mx = ((x1 + x2) - (mPrevX1 + mPrevX2)) / 2; + my = ((y1 + y2) - (mPrevY1 + mPrevY2)) / 2; + + if (mCanTilt) { + float slope = (dx == 0) ? 0 : dy / dx; + + if (Math.abs(slope) < PINCH_TILT_SLOPE) { + + if (mDoTilt) { + tiltBy = my / 5; + } else if (Math.abs(my) > (dpi / PINCH_TILT_THRESHOLD)) { + /* enter exclusive tilt mode */ + mCanScale = false; + mCanRotate = false; + mDoTilt = true; + mTwoFingersDone = true; + } + } + } + + double pinchWidth = Math.sqrt(dx * dx + dy * dy); + double deltaPinch = pinchWidth - mPrevPinchWidth; + + if (mCanRotate) { + double rad = Math.atan2(dy, dx); + double r = rad - mAngle; + + if (mDoRotate) { + double da = rad - mAngle; + + if (Math.abs(da) > 0.0001) { + rotateBy = da; + mAngle = rad; + + deltaPinch = 0; + } + } else { + r = Math.abs(r); + if (r > PINCH_ROTATE_THRESHOLD) { + /* start rotate, disable tilt */ + mDoRotate = true; + mCanTilt = false; + mTwoFingersDone = true; + + mAngle = rad; + } else if (!mDoScale) { + /* reduce pinch trigger by the amount of rotation */ + deltaPinch *= 1 - (r / PINCH_ROTATE_THRESHOLD); + } else { + mPrevPinchWidth = pinchWidth; + } + } + } else if (mDoScale && mEnableRotate) { + /* re-enable rotation when higher threshold is reached */ + double rad = Math.atan2(dy, dx); + double r = rad - mAngle; + + if (r > PINCH_ROTATE_THRESHOLD2) { + /* start rotate again */ + mDoRotate = true; + mCanRotate = true; + mAngle = rad; + mTwoFingersDone = true; + } + } + + if (mCanScale || mDoRotate) { + if (!(mDoScale || mDoRotate)) { + /* enter exclusive scale mode */ + if (Math.abs(deltaPinch) > (dpi / PINCH_ZOOM_THRESHOLD)) { + + if (!mDoRotate) { + mPrevPinchWidth = pinchWidth; + mCanRotate = false; + } + + mCanTilt = false; + mDoScale = true; + mTwoFingersDone = true; + } + } + if (mDoScale || mDoRotate) { + scaleBy = (float) (pinchWidth / mPrevPinchWidth); + mPrevPinchWidth = pinchWidth; + } + } + + if (!(mDoRotate || mDoScale || mDoTilt)) + return; + + float pivotX = 0, pivotY = 0; + + if (!mFixOnCenter) { + pivotX = (x2 + x1) / 2 - width / 2; + pivotY = (y2 + y1) / 2 - height / 2; + } + + synchronized (mViewport) { + if (!mDoTilt) { + if (rotateBy != 0) + mViewport.rotateMap(rotateBy, pivotX, pivotY); + if (scaleBy != 1) + mViewport.scaleMap(scaleBy, pivotX, pivotY); + + if (!mFixOnCenter) + mViewport.moveMap(mx, my); + } else { + if (tiltBy != 0 && mViewport.tiltMap(-tiltBy)) + mViewport.moveMap(0, my / 2); + } + } + + mPrevX1 = x1; + mPrevY1 = y1; + mPrevX2 = x2; + mPrevY2 = y2; + + mMap.updateMap(true); + } + + private void updateMulti(MotionEvent e) { + int cnt = e.getPointerCount(); + + mPrevX1 = e.getX(0); + mPrevY1 = e.getY(0); + + if (cnt == 2) { + mTwoFingers = true; + + mDoScale = false; + mDoRotate = false; + mDoTilt = false; + mCanScale = mEnableScale; + mCanRotate = mEnableRotate; + mCanTilt = mEnableTilt; + + mPrevX2 = e.getX(1); + mPrevY2 = e.getY(1); + double dx = mPrevX1 - mPrevX2; + double dy = mPrevY1 - mPrevY2; + + mAngle = Math.atan2(dy, dx); + mPrevPinchWidth = Math.sqrt(dx * dx + dy * dy); + } + } + + private boolean isMinimalMove(float mx, float my) { + float minSlop = (dpi / MIN_SLOP); + return !withinSquaredDist(mx, my, minSlop * minSlop); + } + + private boolean doFling(float velocityX, float velocityY) { + + int w = Tile.SIZE * 5; + int h = Tile.SIZE * 5; + + mMap.animator().animateFling(velocityX * 2, velocityY * 2, + -w, w, -h, h); + return true; + } + + private static class VelocityTracker { + /* sample window, 200ms */ + private static final int MAX_MS = 200; + private static final int SAMPLES = 32; + + private float mLastX, mLastY; + private long mLastTime; + private int mNumSamples; + private int mIndex; + + private float[] mMeanX = new float[SAMPLES]; + private float[] mMeanY = new float[SAMPLES]; + private int[] mMeanTime = new int[SAMPLES]; + + public void start(float x, float y, long time) { + mLastX = x; + mLastY = y; + mNumSamples = 0; + mIndex = SAMPLES; + mLastTime = time; + } + + public void update(float x, float y, long time) { + if (time == mLastTime) + return; + + if (--mIndex < 0) + mIndex = SAMPLES - 1; + + mMeanX[mIndex] = x - mLastX; + mMeanY[mIndex] = y - mLastY; + mMeanTime[mIndex] = (int) (time - mLastTime); + + mLastTime = time; + mLastX = x; + mLastY = y; + + mNumSamples++; + } + + private float getVelocity(float[] move) { + mNumSamples = Math.min(SAMPLES, mNumSamples); + + double duration = 0; + double amount = 0; + + for (int c = 0; c < mNumSamples; c++) { + int index = (mIndex + c) % SAMPLES; + + float d = mMeanTime[index]; + if (c > 0 && duration + d > MAX_MS) + break; + + duration += d; + amount += move[index] * (d / duration); + } + + if (duration == 0) + return 0; + + return (float) ((amount * 1000) / duration); + } + + float getVelocityY() { + return getVelocity(mMeanY); + } + + float getVelocityX() { + return getVelocity(mMeanX); + } + } +} diff --git a/vtm/src/org/oscim/map/Map.java b/vtm/src/org/oscim/map/Map.java index e10f5303..6e48b118 100644 --- a/vtm/src/org/oscim/map/Map.java +++ b/vtm/src/org/oscim/map/Map.java @@ -27,8 +27,10 @@ import org.oscim.event.EventDispatcher; import org.oscim.event.EventListener; import org.oscim.event.Gesture; import org.oscim.event.MotionEvent; +import org.oscim.layers.AbstractMapEventLayer; import org.oscim.layers.Layer; import org.oscim.layers.MapEventLayer; +import org.oscim.layers.MapEventLayer2; import org.oscim.layers.tile.TileLayer; import org.oscim.layers.tile.vector.OsmTileLayer; import org.oscim.layers.tile.vector.VectorTileLayer; @@ -45,7 +47,12 @@ import org.slf4j.LoggerFactory; public abstract class Map implements TaskQueue { - static final Logger log = LoggerFactory.getLogger(Map.class); + private static final Logger log = LoggerFactory.getLogger(Map.class); + + /** + * If true the {@link MapEventLayer2} will be used instead of default {@link MapEventLayer}. + */ + public static boolean NEW_GESTURES = false; /** * Listener interface for map update notifications. @@ -105,7 +112,7 @@ public abstract class Map implements TaskQueue { protected final Animator mAnimator; protected final MapPosition mMapPosition; - protected final MapEventLayer mEventLayer; + protected final AbstractMapEventLayer mEventLayer; protected boolean mClearMap = true; @@ -134,12 +141,15 @@ public abstract class Map implements TaskQueue { mAsyncExecutor = new AsyncExecutor(4, this); mMapPosition = new MapPosition(); - mEventLayer = new MapEventLayer(this); + if (NEW_GESTURES) + mEventLayer = new MapEventLayer2(this); + else + mEventLayer = new MapEventLayer(this); mLayers.add(0, mEventLayer); } - public MapEventLayer getEventLayer() { + public AbstractMapEventLayer getEventLayer() { return mEventLayer; }