/*
* Copyright 2013 Hannes Janetzek
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.oscim.map;
import static org.oscim.core.MercatorProjection.latitudeToY;
import static org.oscim.core.MercatorProjection.longitudeToX;
import static org.oscim.utils.FastMath.clamp;
import org.oscim.backend.CanvasAdapter;
import org.oscim.core.BoundingBox;
import org.oscim.core.GeoPoint;
import org.oscim.core.MapPosition;
import org.oscim.core.Point;
import org.oscim.core.Tile;
import org.oscim.renderer.MapRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Animator {
static final Logger log = LoggerFactory.getLogger(Animator.class);
//static final Logger log = LoggerFactory.getLogger(MapAnimator.class);
private final static int ANIM_NONE = 0;
private final static int ANIM_MOVE = 1 << 0;
private final static int ANIM_SCALE = 1 << 1;
private final static int ANIM_ROTATE = 1 << 2;
private final static int ANIM_TILT = 1 << 3;
private final static int ANIM_FLING = 1 << 4;
private final Map mMap;
private final MapPosition mCurPos = new MapPosition();
private final MapPosition mStartPos = new MapPosition();
private final MapPosition mDeltaPos = new MapPosition();
private final Point mScroll = new Point();
private final Point mPivot = new Point();
private final Point mVelocity = new Point();
private float mDuration = 500;
private long mAnimEnd = -1;
private int mState = ANIM_NONE;
public Animator(Map map) {
mMap = map;
}
public synchronized void animateTo(long duration, BoundingBox bbox) {
mMap.getMapPosition(mStartPos);
/* TODO for large distance first scale out, then in
* calculate the maximum scale at which the BoundingBox
* is completely visible */
double dx = Math.abs(longitudeToX(bbox.getMaxLongitude())
- longitudeToX(bbox.getMinLongitude()));
double dy = Math.abs(latitudeToY(bbox.getMinLatitude())
- latitudeToY(bbox.getMaxLatitude()));
log.debug("anim bbox " + bbox);
double zx = mMap.getWidth() / (dx * Tile.SIZE);
double zy = mMap.getHeight() / (dy * Tile.SIZE);
double newScale = Math.min(zx, zy);
GeoPoint p = bbox.getCenterPoint();
mDeltaPos.set(longitudeToX(p.getLongitude()) - mStartPos.x,
latitudeToY(p.getLatitude()) - mStartPos.y,
newScale - mStartPos.scale,
-mStartPos.bearing,
-mStartPos.tilt);
animStart(duration, ANIM_MOVE | ANIM_SCALE | ANIM_ROTATE | ANIM_TILT);
}
public synchronized void animateTo(BoundingBox bbox) {
animateTo(1000, bbox);
}
public synchronized void animateTo(long duration, GeoPoint geoPoint,
double scale, boolean relative) {
mMap.getMapPosition(mStartPos);
if (relative)
scale = mStartPos.scale * scale;
scale = clamp(scale, Viewport.MIN_SCALE, Viewport.MAX_SCALE);
mDeltaPos.set(longitudeToX(geoPoint.getLongitude()) - mStartPos.x,
latitudeToY(geoPoint.getLatitude()) - mStartPos.y,
scale - mStartPos.scale,
0, 0);
animStart(duration, ANIM_MOVE | ANIM_SCALE);
}
public synchronized void animateTo(GeoPoint p) {
animateTo(500, p, 1, true);
}
public synchronized void animateTo(long duration, MapPosition pos) {
mMap.getMapPosition(mStartPos);
pos.scale = clamp(pos.scale,
Viewport.MIN_SCALE,
Viewport.MAX_SCALE);
mDeltaPos.set(pos.x - mStartPos.x,
pos.y - mStartPos.y,
pos.scale - mStartPos.scale,
pos.bearing - mStartPos.bearing,
clamp(pos.tilt, 0, Viewport.MAX_TILT) - mStartPos.tilt);
animStart(duration, ANIM_MOVE | ANIM_SCALE | ANIM_ROTATE | ANIM_TILT);
}
public synchronized void animateZoom(long duration, double scaleBy,
float pivotX, float pivotY) {
mMap.getMapPosition(mCurPos);
if (mState == ANIM_SCALE)
scaleBy = (mStartPos.scale + mDeltaPos.scale) * scaleBy;
else
scaleBy = mCurPos.scale * scaleBy;
mStartPos.copy(mCurPos);
scaleBy = clamp(scaleBy, Viewport.MIN_SCALE, Viewport.MAX_SCALE);
mDeltaPos.scale = scaleBy - mStartPos.scale;
mPivot.x = pivotX;
mPivot.y = pivotY;
animStart(duration, ANIM_SCALE);
}
public synchronized void animateFling(float velocityX, float velocityY,
int xmin, int xmax, int ymin, int ymax) {
if (velocityX * velocityX + velocityY * velocityY < 2048)
return;
mMap.getMapPosition(mStartPos);
mScroll.x = 0;
mScroll.y = 0;
float duration = 500;
float flingFactor = 240 / CanvasAdapter.dpi;
mVelocity.x = velocityX * flingFactor;
mVelocity.y = velocityY * flingFactor;
mVelocity.x = clamp(mVelocity.x, xmin, xmax);
mVelocity.y = clamp(mVelocity.y, ymin, ymax);
if (Double.isNaN(mVelocity.x) || Double.isNaN(mVelocity.y)) {
log.debug("fling NaN!");
return;
}
animStart(duration, ANIM_FLING);
}
private void animStart(float duration, int state) {
mState = state;
mCurPos.copy(mStartPos);
mDuration = duration;
mAnimEnd = System.currentTimeMillis() + (long) duration;
mMap.render();
}
private void animCancel() {
mState = ANIM_NONE;
mPivot.x = 0;
mPivot.y = 0;
}
/**
* called by MapRenderer at begin of each frame.
*/
public synchronized void updateAnimation() {
if (mState == ANIM_NONE)
return;
long millisLeft = mAnimEnd - MapRenderer.frametime;
boolean changed = false;
ViewController v = mMap.viewport();
synchronized (v) {
/* cancel animation when position was changed since last
* update, i.e. when it was modified outside the animator. */
if (v.getMapPosition(mCurPos)) {
animCancel();
return;
}
float adv = clamp(1.0f - millisLeft / mDuration, 0, 1);
double scaleAdv = 1;
if ((mState & ANIM_SCALE) != 0) {
scaleAdv = doScale(v, adv);
}
if ((mState & ANIM_MOVE) != 0) {
v.moveTo(mStartPos.x + mDeltaPos.x * (adv / scaleAdv),
mStartPos.y + mDeltaPos.y * (adv / scaleAdv));
}
if ((mState & ANIM_FLING) != 0) {
adv = (float) Math.sqrt(adv);
double dx = mVelocity.x * adv;
double dy = mVelocity.y * adv;
if ((dx - mScroll.x) != 0 || (dy - mScroll.y) != 0) {
v.moveMap((float) (dx - mScroll.x),
(float) (dy - mScroll.y));
mScroll.x = dx;
mScroll.y = dy;
}
}
if ((mState & ANIM_ROTATE) != 0) {
v.setRotation(mStartPos.bearing + mDeltaPos.bearing * adv);
}
if ((mState & ANIM_TILT) != 0) {
v.setTilt(mStartPos.tilt + mDeltaPos.tilt * adv);
}
if (millisLeft <= 0)
animCancel();
/* remember current map position */
changed = v.getMapPosition(mCurPos);
}
if (changed) {
/* render and inform layers that position has changed */
mMap.updateMap(true);
} else {
/* just render next frame */
mMap.render();
}
}
private double doScale(ViewController v, float adv) {
double newScale = mStartPos.scale + mDeltaPos.scale * Math.sqrt(adv);
v.scaleMap((float) (newScale / mCurPos.scale),
(float) mPivot.x, (float) mPivot.y);
return newScale / (mStartPos.scale + mDeltaPos.scale);
}
public synchronized void cancel() {
mState = ANIM_NONE;
}
}