package net.osmand.plus.views;
import android.os.SystemClock;
import android.support.v4.util.Pair;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import net.osmand.PlatformUtil;
import net.osmand.core.android.MapRendererView;
import net.osmand.data.RotatedTileBox;
import net.osmand.util.MapUtils;
import org.apache.commons.logging.Log;
/**
* Thread for animated dragging.
* Defines accelerator to stop dragging screen.
*/
public class AnimateDraggingMapThread {
protected static final Log log = PlatformUtil.getLog(AnimateDraggingMapThread.class);
private final static float DRAGGING_ANIMATION_TIME = 1200f;
private final static float ZOOM_ANIMATION_TIME = 250f;
private final static float ZOOM_MOVE_ANIMATION_TIME = 350f;
private final static float MOVE_MOVE_ANIMATION_TIME = 900f;
private final static float NAV_ANIMATION_TIME = 1000f;
private final static int DEFAULT_SLEEP_TO_REDRAW = 15;
private volatile boolean stopped;
private volatile Thread currentThread = null;
private final OsmandMapTileView tileView;
private float targetRotate = -720;
private double targetLatitude = 0;
private double targetLongitude = 0;
private int targetIntZoom = 0;
private int targetFloatZoom = 0;
private boolean isAnimatingZoom;
public AnimateDraggingMapThread(OsmandMapTileView tileView){
this.tileView = tileView;
}
private void pendingRotateAnimation() {
boolean conditionToCountinue = false;
if (targetRotate != -720) {
do {
conditionToCountinue = false;
float rotationDiff = MapUtils.unifyRotationDiff(tileView.getRotate(), targetRotate);
float absDiff = Math.abs(rotationDiff);
if (absDiff > 0) {
try {
Thread.sleep(DEFAULT_SLEEP_TO_REDRAW);
} catch (InterruptedException e) {
//do nothing
}
if (absDiff < 1) {
tileView.rotateToAnimate(targetRotate);
} else {
conditionToCountinue = true;
tileView.rotateToAnimate(rotationDiff / 5 + tileView.getRotate());
}
}
} while (conditionToCountinue && tileView.isMapRotateEnabled());
targetRotate = -720;
}
}
/**
* Stop dragging async
*/
public void stopAnimating(){
stopped = true;
}
public boolean isAnimating(){
return currentThread != null && !stopped;
}
/**
* Stop dragging sync
*/
public void stopAnimatingSync(){
// wait until current thread != null
stopped = true;
while(currentThread != null){
try {
currentThread.join();
} catch (Exception e) {
}
}
}
public void startThreadAnimating(final Runnable runnable){
stopAnimatingSync();
stopped = false;
currentThread = new Thread(new Runnable() {
@Override
public void run() {
try {
suspendUpdate();
runnable.run();
} finally {
currentThread = null;
resumeUpdate();
}
}
}, "Animating Thread");
currentThread.start();
}
public void startMoving(final double finalLat, final double finalLon, final Pair<Integer, Double> finalZoom,
final Float finalRotation, final boolean notifyListener) {
stopAnimatingSync();
final RotatedTileBox rb = tileView.getCurrentRotatedTileBox().copy();
double startLat = rb.getLatitude();
double startLon = rb.getLongitude();
final int startZoom = rb.getZoom();
final double startZoomFP = rb.getZoomFloatPart();
final float startRotaton = rb.getRotate();
final int zoom;
final double zoomFP;
final float rotation;
if (finalZoom != null) {
zoom = finalZoom.first;
zoomFP = finalZoom.second;
} else {
zoom = startZoom;
zoomFP = startZoomFP;
}
if (finalRotation != null) {
rotation = finalRotation;
} else {
rotation = startRotaton;
}
final float mMoveX = rb.getPixXFromLatLon(startLat, startLon) - rb.getPixXFromLatLon(finalLat, finalLon);
final float mMoveY = rb.getPixYFromLatLon(startLat, startLon) - rb.getPixYFromLatLon(finalLat, finalLon);
boolean skipAnimation = !rb.containsLatLon(finalLat, finalLon);
if (skipAnimation) {
tileView.setLatLonAnimate(finalLat, finalLon, notifyListener);
tileView.setFractionalZoom(zoom, 0, notifyListener);
tileView.rotateToAnimate(rotation);
return;
}
startThreadAnimating(new Runnable() {
@Override
public void run() {
setTargetValues(zoom, finalLat, finalLon);
boolean animateZoom = finalZoom != null && (zoom != startZoom || startZoomFP != 0);
boolean animateRotation = rotation != startRotaton;
if (animateZoom) {
animatingZoomInThread(startZoom, startZoomFP, zoom, zoomFP, NAV_ANIMATION_TIME, notifyListener);
}
if (animateRotation) {
animatingRotateInThread(rotation, 500f, notifyListener);
}
if (!stopped){
animatingMoveInThread(mMoveX, mMoveY, NAV_ANIMATION_TIME, notifyListener, null);
}
}
});
}
public void startMoving(final double finalLat, final double finalLon, final int endZoom, final boolean notifyListener) {
startMoving(finalLat, finalLon, endZoom, notifyListener, null);
}
public void startMoving(final double finalLat, final double finalLon, final int endZoom,
final boolean notifyListener, final Runnable finishAminationCallback) {
stopAnimatingSync();
final RotatedTileBox rb = tileView.getCurrentRotatedTileBox().copy();
double startLat = rb.getLatitude();
double startLon = rb.getLongitude();
float rotate = rb.getRotate();
final int startZoom = rb.getZoom();
final double startZoomFP = rb.getZoomFloatPart();
boolean skipAnimation = false;
float mStX = rb.getPixXFromLatLon(startLat, startLon) - rb.getPixXFromLatLon(finalLat, finalLon);
float mStY = rb.getPixYFromLatLon(startLat, startLon) - rb.getPixYFromLatLon(finalLat, finalLon);
while (Math.abs(mStX) + Math.abs(mStY) > 1200) {
rb.setZoom(rb.getZoom() - 1);
if(rb.getZoom() <= 4){
skipAnimation = true;
}
mStX = rb.getPixXFromLatLon(startLat, startLon) - rb.getPixXFromLatLon(finalLat, finalLon);
mStY = rb.getPixYFromLatLon(startLat, startLon) - rb.getPixYFromLatLon(finalLat, finalLon);
}
final int moveZoom = rb.getZoom();
// check if animation needed
skipAnimation = skipAnimation || (Math.abs(moveZoom - startZoom) >= 3 || Math.abs(endZoom - moveZoom) > 3);
if (skipAnimation) {
tileView.setLatLonAnimate(finalLat, finalLon, notifyListener);
tileView.setFractionalZoom(endZoom, 0, notifyListener);
return;
}
final float mMoveX = rb.getPixXFromLatLon(startLat, startLon) - rb.getPixXFromLatLon(finalLat, finalLon);
final float mMoveY = rb.getPixYFromLatLon(startLat, startLon) - rb.getPixYFromLatLon(finalLat, finalLon);
final float animationTime = Math.max(450, (Math.abs(mStX) + Math.abs(mStY)) / 1200f * MOVE_MOVE_ANIMATION_TIME);
startThreadAnimating(new Runnable() {
@Override
public void run() {
setTargetValues(endZoom, finalLat, finalLon);
if(moveZoom != startZoom){
animatingZoomInThread(startZoom, startZoomFP, moveZoom, startZoomFP,ZOOM_MOVE_ANIMATION_TIME, notifyListener);
}
if(!stopped){
animatingMoveInThread(mMoveX, mMoveY, animationTime, notifyListener, finishAminationCallback);
}
if(!stopped){
tileView.setLatLonAnimate(finalLat, finalLon, notifyListener);
}
if (!stopped && (moveZoom != endZoom || startZoomFP != 0)) {
animatingZoomInThread(moveZoom, startZoomFP, endZoom, 0, ZOOM_MOVE_ANIMATION_TIME, notifyListener);
}
tileView.setFractionalZoom(endZoom, 0, notifyListener);
pendingRotateAnimation();
}
});
}
private void animatingRotateInThread(float rotate, float animationTime, boolean notify){
AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
float startRotate = tileView.getRotate();
float rotationDiff = MapUtils.unifyRotationDiff(startRotate, rotate);
if (tileView.isMapRotateEnabled() && Math.abs(rotationDiff) > 1) {
long timeMillis = SystemClock.uptimeMillis();
float normalizedTime;
while (!stopped) {
normalizedTime = (SystemClock.uptimeMillis() - timeMillis) / animationTime;
if (normalizedTime > 1f) {
tileView.rotateToAnimate(rotate);
break;
}
float interpolation = interpolator.getInterpolation(normalizedTime);
tileView.rotateToAnimate(rotationDiff * interpolation + startRotate);
try {
Thread.sleep(DEFAULT_SLEEP_TO_REDRAW);
} catch (InterruptedException e) {
stopped = true;
}
}
} else {
tileView.rotateToAnimate(rotate);
}
}
private void animatingMoveInThread(float moveX, float moveY, float animationTime,
boolean notify, final Runnable finishAnimationCallback){
AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
float cX = 0;
float cY = 0;
long timeMillis = SystemClock.uptimeMillis();
float normalizedTime = 0f;
while (!stopped){
normalizedTime = (SystemClock.uptimeMillis() - timeMillis) / animationTime;
if (normalizedTime > 1f) {
break;
}
float interpolation = interpolator.getInterpolation(normalizedTime);
float nX = interpolation * moveX;
float nY = interpolation * moveY;
tileView.dragToAnimate(cX, cY, nX, nY, notify);
cX = nX;
cY = nY;
try {
Thread.sleep(DEFAULT_SLEEP_TO_REDRAW);
} catch (InterruptedException e) {
stopped = true;
}
}
if (finishAnimationCallback != null) {
tileView.getApplication().runInUIThread(new Runnable() {
@Override
public void run() {
finishAnimationCallback.run();
}
});
}
}
private void animatingZoomInThread(int zoomStart, double zoomFloatStart,
int zoomEnd, double zoomFloatEnd, float animationTime, boolean notifyListener){
try {
isAnimatingZoom = true;
// could be 0 ]-0.5,0.5], -1 ]-1,0], 1 ]0, 1]
int threshold = ((int)(zoomFloatEnd * 2));
double beginZoom = zoomStart + zoomFloatStart;
double endZoom = zoomEnd + zoomFloatEnd;
double curZoom = beginZoom;
animationTime *= Math.abs(endZoom - beginZoom);
// AccelerateInterpolator interpolator = new AccelerateInterpolator(1);
LinearInterpolator interpolator = new LinearInterpolator();
long timeMillis = SystemClock.uptimeMillis();
float normalizedTime = 0f;
while (!stopped) {
normalizedTime = (SystemClock.uptimeMillis() - timeMillis) / animationTime;
if (normalizedTime > 1f) {
break;
}
float interpolation = interpolator.getInterpolation(normalizedTime);
curZoom = interpolation * (endZoom - beginZoom) + beginZoom;
int baseZoom = (int) Math.round(curZoom - 0.5 * threshold);
double zaAnimate = curZoom - baseZoom;
tileView.zoomToAnimate(baseZoom, zaAnimate, notifyListener);
try {
Thread.sleep(DEFAULT_SLEEP_TO_REDRAW);
} catch (InterruptedException e) {
stopped = true;
}
}
tileView.setFractionalZoom(zoomEnd, zoomFloatEnd, notifyListener);
} finally {
isAnimatingZoom = false;
}
}
public boolean isAnimatingZoom() {
return isAnimatingZoom;
}
public void startZooming(final int zoomEnd, final double zoomPart, final boolean notifyListener){
final float animationTime = ZOOM_ANIMATION_TIME;
startThreadAnimating(new Runnable(){
@Override
public void run() {
RotatedTileBox tb = tileView.getCurrentRotatedTileBox();
setTargetValues(zoomEnd, tileView.getLatitude(), tileView.getLongitude());
animatingZoomInThread(tb.getZoom(), tb.getZoomFloatPart(), zoomEnd, zoomPart, animationTime, notifyListener);
pendingRotateAnimation();
}
}); //$NON-NLS-1$
}
public void startDragging(final float velocityX, final float velocityY, float startX, float startY,
final float endX, final float endY, final boolean notifyListener){
final float animationTime = DRAGGING_ANIMATION_TIME;
clearTargetValues();
startThreadAnimating(new Runnable(){
@Override
public void run() {
float curX = endX;
float curY = endY;
DecelerateInterpolator interpolator = new DecelerateInterpolator(1);
long timeMillis = SystemClock.uptimeMillis();
float normalizedTime = 0f;
float prevNormalizedTime = 0f;
while(!stopped){
normalizedTime = (SystemClock.uptimeMillis() - timeMillis) / animationTime;
if(normalizedTime >= 1f){
break;
}
float interpolation = interpolator.getInterpolation(normalizedTime);
float newX = velocityX * (1 - interpolation) * (normalizedTime - prevNormalizedTime) + curX;
float newY = velocityY * (1 - interpolation) * (normalizedTime - prevNormalizedTime) + curY;
tileView.dragToAnimate(curX, curY, newX, newY, notifyListener);
curX = newX;
curY = newY;
prevNormalizedTime = normalizedTime;
try {
Thread.sleep(DEFAULT_SLEEP_TO_REDRAW);
} catch (InterruptedException e) {
stopped = true;
}
}
pendingRotateAnimation();
}
}); //$NON-NLS-1$
}
private void clearTargetValues(){
targetIntZoom = 0;
}
private void suspendUpdate() {
final MapRendererView mapRenderer = tileView.getMapRenderer();
if (mapRenderer != null) {
mapRenderer.suspendSymbolsUpdate();
}
}
private void resumeUpdate() {
final MapRendererView mapRenderer = tileView.getMapRenderer();
if (mapRenderer != null) {
mapRenderer.resumeSymbolsUpdate();
}
}
private void setTargetValues(int zoom, double lat, double lon){
targetIntZoom = zoom;
targetLatitude = lat;
targetLongitude = lon;
}
public void startRotate(final float rotate) {
if (!isAnimating()) {
clearTargetValues();
// stopped = false;
// do we need to kill and recreate the thread? wait would be enough as now it
// also handles the rotation?
startThreadAnimating(new Runnable() {
@Override
public void run() {
targetRotate = rotate;
pendingRotateAnimation();
}
});
} else {
this.targetRotate = rotate;
}
}
public int getTargetIntZoom() {
return targetIntZoom;
}
public double getTargetLatitude() {
return targetLatitude;
}
public double getTargetLongitude() {
return targetLongitude;
}
}