// Created by plusminus on 21:37:08 - 27.09.2008
package org.osmdroid.views;
import java.util.LinkedList;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
import org.osmdroid.util.BoundingBoxE6;
import org.osmdroid.views.MapView.OnFirstLayoutListener;
import org.osmdroid.views.util.MyMath;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.ScaleAnimation;
import static org.osmdroid.views.util.constants.MapViewConstants.*;
/**
*
* @author Nicolas Gramlich
* @author Marc Kurtz
*/
public class MapController implements IMapController, OnFirstLayoutListener {
// ===========================================================
// Constants
// ===========================================================
// ===========================================================
// Fields
// ===========================================================
protected final MapView mMapView;
// Zoom animations
private ValueAnimator mZoomInAnimation;
private ValueAnimator mZoomOutAnimation;
private ScaleAnimation mZoomInAnimationOld;
private ScaleAnimation mZoomOutAnimationOld;
private Animator mCurrentAnimator;
// Keep track of calls before initial layout
private ReplayController mReplayController;
// ===========================================================
// Constructors
// ===========================================================
public MapController(MapView mapView) {
mMapView = mapView;
// Keep track of initial layout
mReplayController = new ReplayController();
if (!mMapView.isLayoutOccurred()) {
mMapView.addOnFirstLayoutListener(this);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
ZoomAnimatorListener zoomAnimatorListener = new ZoomAnimatorListener(this);
mZoomInAnimation = ValueAnimator.ofFloat(1f, 2f);
mZoomInAnimation.addListener(zoomAnimatorListener);
mZoomInAnimation.addUpdateListener(zoomAnimatorListener);
mZoomInAnimation.setDuration(ANIMATION_DURATION_SHORT);
mZoomOutAnimation = ValueAnimator.ofFloat(1f, 0.5f);
mZoomOutAnimation.addListener(zoomAnimatorListener);
mZoomOutAnimation.addUpdateListener(zoomAnimatorListener);
mZoomOutAnimation.setDuration(ANIMATION_DURATION_SHORT);
} else {
ZoomAnimationListener zoomAnimationListener = new ZoomAnimationListener(this);
mZoomInAnimationOld = new ScaleAnimation(1, 2, 1, 2, Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
mZoomOutAnimationOld = new ScaleAnimation(1, 0.5f, 1, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mZoomInAnimationOld.setDuration(ANIMATION_DURATION_SHORT);
mZoomOutAnimationOld.setDuration(ANIMATION_DURATION_SHORT);
mZoomInAnimationOld.setAnimationListener(zoomAnimationListener);
mZoomOutAnimationOld.setAnimationListener(zoomAnimationListener);
}
}
@Override
public void onFirstLayout(View v, int left, int top, int right, int bottom) {
mReplayController.replayCalls();
}
@Deprecated
public void zoomToSpan(final BoundingBoxE6 bb) {
zoomToSpan(bb.getLatitudeSpanE6(), bb.getLongitudeSpanE6());
}
@Override
public void zoomToSpan(double latSpan, double lonSpan) {
if (latSpan <= 0 || lonSpan <= 0) {
return;
}
// If no layout, delay this call
if (!mMapView.isLayoutOccurred()) {
mReplayController.zoomToSpan(latSpan, lonSpan);
return;
}
final BoundingBox bb = this.mMapView.getProjection().getBoundingBox();
final int curZoomLevel = this.mMapView.getProjection().getZoomLevel();
final double curLatSpan = bb.getLatitudeSpan();
final double curLonSpan = bb.getLongitudeSpan();
final double diffNeededLat = (double) latSpan / curLatSpan; // i.e. 600/500 = 1,2
final double diffNeededLon = (double) lonSpan / curLonSpan; // i.e. 300/400 = 0,75
final double diffNeeded = Math.max(diffNeededLat, diffNeededLon); // i.e. 1,2
if (diffNeeded > 1) { // Zoom Out
this.mMapView.setZoomLevel(curZoomLevel - MyMath.getNextSquareNumberAbove((float)diffNeeded));
} else if (diffNeeded < 0.5) { // Can Zoom in
this.mMapView.setZoomLevel(curZoomLevel
+ MyMath.getNextSquareNumberAbove(1 / (float)diffNeeded) - 1);
}
}
// TODO rework zoomToSpan
@Override
public void zoomToSpan(int latSpanE6, int lonSpanE6) {
if (latSpanE6 <= 0 || lonSpanE6 <= 0) {
return;
}
// If no layout, delay this call
if (!mMapView.isLayoutOccurred()) {
mReplayController.zoomToSpan(latSpanE6, lonSpanE6);
return;
}
final BoundingBox bb = this.mMapView.getProjection().getBoundingBox();
final int curZoomLevel = this.mMapView.getProjection().getZoomLevel();
final int curLatSpan = bb.getLatitudeSpanE6();
final int curLonSpan = bb.getLongitudeSpanE6();
final float diffNeededLat = (float) latSpanE6 / curLatSpan; // i.e. 600/500 = 1,2
final float diffNeededLon = (float) lonSpanE6 / curLonSpan; // i.e. 300/400 = 0,75
final float diffNeeded = Math.max(diffNeededLat, diffNeededLon); // i.e. 1,2
if (diffNeeded > 1) { // Zoom Out
this.mMapView.setZoomLevel(curZoomLevel - MyMath.getNextSquareNumberAbove(diffNeeded));
} else if (diffNeeded < 0.5) { // Can Zoom in
this.mMapView.setZoomLevel(curZoomLevel
+ MyMath.getNextSquareNumberAbove(1 / diffNeeded) - 1);
}
}
/**
* Start animating the map towards the given point.
*/
@Override
public void animateTo(final IGeoPoint point) {
// If no layout, delay this call
if (!mMapView.isLayoutOccurred()) {
mReplayController.animateTo(point);
return;
}
Point p = mMapView.getProjection().toPixels(point, null);
animateTo(p.x, p.y);
}
/**
* Start animating the map towards the given point.
*/
public void animateTo(int x, int y) {
// If no layout, delay this call
if (!mMapView.isLayoutOccurred()) {
mReplayController.animateTo(x, y);
return;
}
if (!mMapView.isAnimating()) {
mMapView.mIsFlinging = false;
Point mercatorPoint = mMapView.getProjection().toMercatorPixels(x, y, null);
// The points provided are "center", we want relative to upper-left for scrolling
mercatorPoint.offset(-mMapView.getWidth() / 2, -mMapView.getHeight() / 2);
final int xStart = mMapView.getScrollX();
final int yStart = mMapView.getScrollY();
final int dx = mercatorPoint.x - xStart;
final int dy = mercatorPoint.y - yStart;
if(dx != 0 || dy != 0) {
mMapView.getScroller().startScroll(xStart, yStart, dx, dy, ANIMATION_DURATION_DEFAULT);
mMapView.postInvalidate();
}
}
}
@Override
public void scrollBy(int x, int y) {
this.mMapView.scrollBy(x, y);
}
/**
* Set the map view to the given center. There will be no animation.
*/
@Override
public void setCenter(final IGeoPoint point) {
// If no layout, delay this call
if (mMapView.mListener != null) {
mMapView.mListener.onScroll(new ScrollEvent(mMapView,0,0));
}
if (!mMapView.isLayoutOccurred()) {
mReplayController.setCenter(point);
return;
}
Point p = mMapView.getProjection().toPixels(point, null);
//FIXME This will overflow for zoom > 20, we'll end up with a negative Point.y by the time we hit scroll to.
p = mMapView.getProjection().toMercatorPixels(p.x, p.y, p);
// The points provided are "center", we want relative to upper-left for scrolling
p.offset(-mMapView.getWidth() / 2, -mMapView.getHeight() / 2);
mMapView.scrollTo(p.x, p.y);//-2,040,942,406
// 2,147,483,648
}
@Override
public void stopPanning() {
mMapView.mIsFlinging = false;
mMapView.getScroller().forceFinished(true);
}
/**
* Stops a running animation.
*
* @param jumpToTarget
*/
@Override
public void stopAnimation(final boolean jumpToTarget) {
if (!mMapView.getScroller().isFinished()) {
if (jumpToTarget) {
mMapView.mIsFlinging = false;
mMapView.getScroller().abortAnimation();
} else
stopPanning();
}
// We ignore the jumpToTarget for zoom levels since it doesn't make sense to stop
// the animation in the middle. Maybe we could have it cancel the zoom operation and jump
// back to original zoom level?
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
final Animator currentAnimator = this.mCurrentAnimator;
if (mMapView.mIsAnimating.get()) {
currentAnimator.end();
}
} else {
if (mMapView.mIsAnimating.get()) {
mMapView.clearAnimation();
}
}
}
@Override
public int setZoom(final int zoomlevel) {
return mMapView.setZoomLevel(zoomlevel);
}
/**
* Zoom in by one zoom level.
*/
@Override
public boolean zoomIn() {
return zoomTo(mMapView.getZoomLevel(false) + 1);
}
@Override
public boolean zoomInFixing(final int xPixel, final int yPixel) {
mMapView.mMultiTouchScalePoint.set(xPixel, yPixel);
if (mMapView.canZoomIn()) {
if (mMapView.mListener != null) {
mMapView.mListener.onZoom(new ZoomEvent(mMapView, mMapView.getZoomLevel()+1));
}
if (mMapView.mIsAnimating.getAndSet(true)) {
// TODO extend zoom (and return true)
return false;
} else {
mMapView.mTargetZoomLevel.set(mMapView.getZoomLevel(false) + 1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mCurrentAnimator = mZoomInAnimation;
mZoomInAnimation.start();
} else {
mMapView.startAnimation(mZoomInAnimationOld);
}
return true;
}
} else {
return false;
}
}
/**
* Zoom out by one zoom level.
*/
@Override
public boolean zoomOut() {
return zoomTo(mMapView.getZoomLevel(false) - 1);
}
@Override
public boolean zoomOutFixing(final int xPixel, final int yPixel) {
mMapView.mMultiTouchScalePoint.set(xPixel, yPixel);
if (mMapView.canZoomOut()) {
if (mMapView.mListener != null) {
mMapView.mListener.onZoom(new ZoomEvent(mMapView, mMapView.getZoomLevel()-1));
}
if (mMapView.mIsAnimating.getAndSet(true)) {
// TODO extend zoom (and return true)
return false;
} else {
mMapView.mTargetZoomLevel.set(mMapView.getZoomLevel(false) - 1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mCurrentAnimator = mZoomOutAnimation;
mZoomOutAnimation.start();
} else {
mMapView.startAnimation(mZoomOutAnimationOld);
}
return true;
}
} else {
return false;
}
}
@Override
public boolean zoomTo(int zoomLevel) {
return zoomToFixing(zoomLevel, mMapView.getWidth() / 2, mMapView.getHeight() / 2);
}
@Override
public boolean zoomToFixing(int zoomLevel, int xPixel, int yPixel) {
zoomLevel = zoomLevel > mMapView.getMaxZoomLevel() ? mMapView.getMaxZoomLevel() : zoomLevel;
zoomLevel = zoomLevel < mMapView.getMinZoomLevel() ? mMapView.getMinZoomLevel() : zoomLevel;
int currentZoomLevel = mMapView.getZoomLevel();
boolean canZoom = zoomLevel < currentZoomLevel && mMapView.canZoomOut() ||
zoomLevel > currentZoomLevel && mMapView.canZoomIn();
mMapView.mMultiTouchScalePoint.set(xPixel, yPixel);
if (canZoom) {
if (mMapView.mListener != null) {
mMapView.mListener.onZoom(new ZoomEvent(mMapView, zoomLevel));
}
if (mMapView.mIsAnimating.getAndSet(true)) {
// TODO extend zoom (and return true)
return false;
} else {
mMapView.mTargetZoomLevel.set(zoomLevel);
float end = (float) Math.pow(2.0, zoomLevel - currentZoomLevel);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
ZoomAnimatorListener zoomAnimatorListener = new ZoomAnimatorListener(this);
ValueAnimator zoomToAnimator = ValueAnimator.ofFloat(1f, end);
zoomToAnimator.addListener(zoomAnimatorListener);
zoomToAnimator.addUpdateListener(zoomAnimatorListener);
zoomToAnimator.setDuration(ANIMATION_DURATION_SHORT);
mCurrentAnimator = zoomToAnimator;
zoomToAnimator.start();
} else {
if (zoomLevel > currentZoomLevel)
mMapView.startAnimation(mZoomInAnimationOld);
else
mMapView.startAnimation(mZoomOutAnimationOld);
ScaleAnimation scaleAnimation;
scaleAnimation = new ScaleAnimation(
1f, end, //X
1f, end, //Y
Animation.RELATIVE_TO_SELF, 0.5f, //Pivot X
Animation.RELATIVE_TO_SELF, 0.5f); //Pivot Y
scaleAnimation.setDuration(ANIMATION_DURATION_SHORT);
scaleAnimation.setAnimationListener(new ZoomAnimationListener(this));
}
return true;
}
} else {
return false;
}
}
protected void onAnimationStart() {
mMapView.mIsAnimating.set(true);
}
protected void onAnimationEnd() {
final Rect screenRect = mMapView.getProjection().getScreenRect();
Point p = mMapView.getProjection().unrotateAndScalePoint(screenRect.centerX(),
screenRect.centerY(), null);
p = mMapView.getProjection().toMercatorPixels(p.x, p.y, p);
// The points provided are "center", we want relative to upper-left for scrolling
p.offset(-mMapView.getWidth() / 2, -mMapView.getHeight() / 2);
mMapView.mIsAnimating.set(false);
//scrolls to the point of user input
//no overflow detected
mMapView.scrollTo(p.x, p.y);
setZoom(mMapView.mTargetZoomLevel.get());
mMapView.mMultiTouchScale = 1f;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mCurrentAnimator = null;
}
// Fix for issue 477
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
mMapView.clearAnimation();
mZoomInAnimationOld.reset();
mZoomOutAnimationOld.reset();
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static class ZoomAnimatorListener
implements Animator.AnimatorListener, AnimatorUpdateListener {
private MapController mMapController;
public ZoomAnimatorListener(MapController mapController) {
mMapController = mapController;
}
@Override
public void onAnimationStart(Animator animator) {
mMapController.onAnimationStart();
}
@Override
public void onAnimationEnd(Animator animator) {
mMapController.onAnimationEnd();
}
@Override
public void onAnimationCancel(Animator animator) {
//noOp
}
@Override
public void onAnimationRepeat(Animator animator) {
//noOp
}
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mMapController.mMapView.mMultiTouchScale = (Float) valueAnimator.getAnimatedValue();
mMapController.mMapView.invalidate();
}
}
protected static class ZoomAnimationListener implements AnimationListener {
private MapController mMapController;
public ZoomAnimationListener(MapController mapController) {
mMapController = mapController;
}
@Override
public void onAnimationStart(Animation animation) {
mMapController.onAnimationStart();
}
@Override
public void onAnimationEnd(Animation animation) {
mMapController.onAnimationEnd();
}
@Override
public void onAnimationRepeat(Animation animation) {
//noOp
}
}
private enum ReplayType {
ZoomToSpanPoint, AnimateToPoint, AnimateToGeoPoint, SetCenterPoint
};
private class ReplayController {
private LinkedList<ReplayClass> mReplayList = new LinkedList<ReplayClass>();
public void animateTo(IGeoPoint geoPoint) {
mReplayList.add(new ReplayClass(ReplayType.AnimateToGeoPoint, null, geoPoint));
}
public void animateTo(int x, int y) {
mReplayList.add(new ReplayClass(ReplayType.AnimateToPoint, new Point(x, y), null));
}
public void setCenter(IGeoPoint geoPoint) {
mReplayList.add(new ReplayClass(ReplayType.SetCenterPoint, null, geoPoint));
}
public void zoomToSpan(int x, int y) {
mReplayList.add(new ReplayClass(ReplayType.ZoomToSpanPoint, new Point(x, y), null));
}
public void zoomToSpan(double x, double y) {
mReplayList.add(new ReplayClass(ReplayType.ZoomToSpanPoint, new Point((int)(x*1E6), (int)(y*1E6)), null));
}
public void replayCalls() {
for (ReplayClass replay : mReplayList) {
switch (replay.mReplayType) {
case AnimateToGeoPoint:
MapController.this.animateTo(replay.mGeoPoint);
break;
case AnimateToPoint:
MapController.this.animateTo(replay.mPoint.x, replay.mPoint.y);
break;
case SetCenterPoint:
MapController.this.setCenter(replay.mGeoPoint);
break;
case ZoomToSpanPoint:
MapController.this.zoomToSpan(replay.mPoint.x, replay.mPoint.y);
break;
}
}
mReplayList.clear();
}
private class ReplayClass {
private ReplayType mReplayType;
private Point mPoint;
private IGeoPoint mGeoPoint;
public ReplayClass(ReplayType mReplayType, Point mPoint, IGeoPoint mGeoPoint) {
super();
this.mReplayType = mReplayType;
this.mPoint = mPoint;
this.mGeoPoint = mGeoPoint;
}
}
}
}