Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.oscim.layers.MapEventLayer2 Maven / Gradle / Ivy
/*
* Copyright 2013 Hannes Janetzek
* Copyright 2016 devemux86
* Copyright 2016 Andrey Novikov
* Copyright 2016 Longri
* Copyright 2018 Gustl22
*
* 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.backend.CanvasAdapter;
import org.oscim.backend.Platform;
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.Animator2;
import org.oscim.map.Map;
import org.oscim.map.Map.InputListener;
import org.oscim.map.ViewController;
import org.oscim.utils.FastMath;
import org.oscim.utils.Parameters;
import org.oscim.utils.async.Task;
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 float mPivotX;
private float mPivotY;
private double mAngle;
private double mPrevPinchWidth;
private long mStartMove;
/**
* 1in = 25.4mm
*/
private static final float INCH = 25.4f;
/**
* 2mm as minimal distance to start move: dpi / 25.4
*/
private static final float MIN_SLOP = INCH / 2;
/**
* 1cm distance in dips between the first touch and second touch to still be considered a double tap
*/
private static final float DOUBLE_TAP_SLOP = INCH / 10;
private static final float PINCH_ZOOM_THRESHOLD = INCH / 4; // 4mm
private static final float PINCH_TILT_THRESHOLD = INCH / 4; // 4mm
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 mScrollTracker;
private final VelocityTracker mScaleTracker;
private final VelocityTracker mRotateTracker;
private Task mGestureTask;
private final MapPosition mapPosition = new MapPosition();
public MapEventLayer2(Map map) {
super(map);
mScrollTracker = new VelocityTracker();
mScaleTracker = new VelocityTracker();
mRotateTracker = new VelocityTracker();
}
@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 (mGestureTask != null) {
mGestureTask.cancel();
mGestureTask = 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 (!isDoubleTap(mx, my)) {
mTaps = 0;
mMap.handleGesture(Gesture.TAP, mLastTap);
}
} else {
mMap.animator().cancel();
mStartMove = -1;
mDragZoom = false;
mTwoFingers = false;
mTwoFingersDone = false;
mGestureTask = new Task() {
@Override
public int go(boolean canceled) {
if (canceled || mTwoFingers || mStartMove != -1)
return Task.DONE;
mMap.handleGesture(Gesture.LONG_PRESS, e);
return Task.DONE;
}
};
mMap.postDelayed(mGestureTask, 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 (mGestureTask != null) {
mGestureTask.cancel();
mGestureTask = null;
}
if (mStartMove > 0) {
/* handle fling gesture */
mScrollTracker.update(e.getX(), e.getY(), e.getTime());
float vx = mScrollTracker.getVelocityX();
float vy = mScrollTracker.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;
}
if (mEnableMove)
doFlingScroll(vx, vy);
}
if (Parameters.ANIMATOR2) {
if (mRotateTracker.mNumSamples >= 0) {
mDoRotate = mCanRotate = false;
((Animator2) mMap.animator()).animateFlingRotate(mRotateTracker.getVelocityX(), mPivotX, mPivotY);
mRotateTracker.mNumSamples = -1; // Reset tracker
}
if (mScaleTracker.mNumSamples >= 0) {
mDoScale = mCanScale = false;
((Animator2) mMap.animator()).animateFlingZoom(mScaleTracker.getVelocityX(), mPivotX, mPivotY);
mScaleTracker.mNumSamples = -1; // Reset tracker
}
}
if (time - mStartDown > LONG_PRESS_THRESHOLD) {
// this was not a tap
mTaps = 0;
return true;
}
if (mTaps > 0) {
if ((time - mLastTap.getTime()) >= DOUBLE_TAP_THRESHOLD) {
mTaps = 1;
mMap.handleGesture(Gesture.TAP, mLastTap);
} else {
mTaps += 1;
}
} else {
mTaps = 1;
}
if (mLastTap != null) {
mLastTap.recycle();
}
mLastTap = e.copy();
if (mTaps == 3) {
mTaps = 0;
mMap.handleGesture(Gesture.TRIPLE_TAP, e);
} else if (mTaps == 2) {
mGestureTask = new Task() {
@Override
public int go(boolean canceled) {
if (canceled)
return Task.DONE;
mTaps = 0;
if (mDragZoom)
return Task.DONE;
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);
if (CanvasAdapter.platform.isDesktop())
mMap.updateMap(true);
}
return Task.DONE;
}
};
mMap.postDelayed(mGestureTask, DOUBLE_TAP_THRESHOLD);
} else {
mGestureTask = new Task() {
@Override
public int go(boolean canceled) {
if (canceled)
return Task.DONE;
mTaps = 0;
if (!mTwoFingers && mStartMove == -1) {
mMap.handleGesture(Gesture.TAP, e);
}
return Task.DONE;
}
};
mMap.postDelayed(mGestureTask, 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) {
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();
mScrollTracker.start(x1, y1, mStartMove);
return;
}
mViewport.moveMap(mx, my);
mScrollTracker.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;
if (Parameters.ANIMATOR2) {
double clampedRotation = FastMath.clampRadian(rotateBy);
if (mRotateTracker.mNumSamples < 0)
mRotateTracker.start(mRotateTracker.mLastX + (float) clampedRotation, 0, e.getTime());
else
mRotateTracker.update(mRotateTracker.mLastX + (float) clampedRotation, 0, e.getTime());
}
}
} else {
r = Math.abs(r);
if (r > PINCH_ROTATE_THRESHOLD) {
/* start rotate, disable tilt */
mDoRotate = true;
mCanTilt = false;
mTwoFingersDone = true;
/*start from recognized position (smoother rotation)*/
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;
mTwoFingersDone = true;
/*start from recognized position (smoother rotation)*/
mAngle = rad;
}
}
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 (Parameters.ANIMATOR2) {
if (scaleBy != 1f) {
if (mScaleTracker.mNumSamples < 0)
mScaleTracker.start((float) pinchWidth, 0, e.getTime());
else
mScaleTracker.update((float) pinchWidth, 0, e.getTime());
}
}
}
}
if (!(mDoRotate || mDoScale || mDoTilt))
return;
if (!mFixOnCenter) {
mPivotX = (x2 + x1) / 2 - width / 2;
mPivotY = (y2 + y1) / 2 - height / 2;
}
synchronized (mViewport) {
if (!mDoTilt) {
if (rotateBy != 0)
mViewport.rotateMap(rotateBy, mPivotX, mPivotY);
if (scaleBy != 1)
mViewport.scaleMap(scaleBy, mPivotX, mPivotY);
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);
if (mMap.viewport().getMapPosition(mapPosition)) {
if (mDoScale)
mMap.events.fire(Map.SCALE_EVENT, mapPosition);
if (mDoRotate)
mMap.events.fire(Map.ROTATE_EVENT, mapPosition);
if (mDoTilt)
mMap.events.fire(Map.TILT_EVENT, mapPosition);
}
}
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 isDoubleTap(float mx, float my) {
float minSlop = (dpi / DOUBLE_TAP_SLOP);
return withinSquaredDist(mx, my, minSlop * minSlop);
}
private boolean isMinimalMove(float mx, float my) {
float minSlop = (dpi / MIN_SLOP);
return !withinSquaredDist(mx, my, minSlop * minSlop);
}
private boolean doFlingScroll(float velocityX, float velocityY) {
int w = Tile.SIZE * 5;
int h = Tile.SIZE * 5;
if (Parameters.ANIMATOR2) {
if (!CanvasAdapter.platform.isDesktop() && CanvasAdapter.platform != Platform.WEBGL) {
velocityX *= 2;
velocityY *= 2;
}
((Animator2) mMap.animator()).animateFlingScroll(velocityX, velocityY, -w, w, -h, h);
} else
mMap.animator().animateFling(velocityX * 2, velocityY * 2, -w, w, -h, h);
return true;
}
private 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);
}
@Override
public String toString() {
return "VelocityX: " + getVelocityX()
+ "\tVelocityY: " + getVelocityY()
+ "\tNumSamples: " + mNumSamples;
}
}
}