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;
}