content: /*
   * Copyright 2013 Hannes Janetzek
   * Copyright 2016 Stephan Leuschner
   * Copyright 2016-2018 devemux86
   * Copyright 2016 Izumi Kawashima
   * Copyright 2017 Wolfgang Schramm
   * 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 <http://www.gnu.org/licenses/>.
   */
  package org.oscim.map;
  
  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.oscim.utils.ThreadUtils;
  import org.oscim.utils.animation.Easing;
  import org.oscim.utils.async.Task;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static org.oscim.core.MercatorProjection.latitudeToY;
  import static org.oscim.core.MercatorProjection.longitudeToX;
  import static org.oscim.utils.FastMath.clamp;
  
  public class Animator {
      static final Logger log = LoggerFactory.getLogger(Animator.class);
  
      public static final int ANIM_NONE = 0;
      public static final int ANIM_MOVE = 1 << 0;
      public static final int ANIM_SCALE = 1 << 1;
      public static final int ANIM_ROTATE = 1 << 2;
      public static final int ANIM_TILT = 1 << 3;
      public static final int ANIM_FLING = 1 << 4;
  
      final Map mMap;
  
      final MapPosition mCurPos = new MapPosition();
      final MapPosition mStartPos = new MapPosition();
      final MapPosition mDeltaPos = new MapPosition();
  
      private final Point mScroll = new Point();
      final Point mPivot = new Point();
      private final Point mVelocity = new Point();
  
      float mDuration = 500;
      long mAnimEnd = -1;
      Easing.Type mEasingType = Easing.Type.LINEAR;
  
      int mState = ANIM_NONE;
  
      public Animator(Map map) {
          mMap = map;
      }
  
      public synchronized void animateTo(BoundingBox bbox) {
          animateTo(1000, bbox);
      }
  
      public synchronized void animateTo(long duration, BoundingBox bbox) {
          animateTo(duration, bbox, Easing.Type.LINEAR);
      }
  
      public synchronized void animateTo(long duration, BoundingBox bbox, Easing.Type easingType) {
          animateTo(duration, bbox, easingType, ANIM_MOVE | ANIM_SCALE | ANIM_ROTATE | ANIM_TILT);
      }
  
      public synchronized void animateTo(long duration, BoundingBox bbox, Easing.Type easingType, int state) {
          ThreadUtils.assertMainThread();
  
          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 = mMap.viewport().limitScale(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, state, easingType);
      }
  
      public void animateTo(GeoPoint p) {
          animateTo(500, p);
      }
  
      public void animateTo(long duration, GeoPoint p) {
          animateTo(duration, p, 1, true);
      }
  
      /**
       * Animate to GeoPoint
       *
       * @param duration in ms
       * @param geoPoint
       * @param scale
       * @param relative alter scale relative to current scale
       */
      public void animateTo(long duration, GeoPoint geoPoint,
                            double scale, boolean relative) {
          animateTo(duration, geoPoint, scale, relative, Easing.Type.LINEAR);
      }
  
      /**
       * Animate to GeoPoint
       *
       * @param duration   in ms
       * @param geoPoint
       * @param scale
       * @param relative   alter scale relative to current scale
       * @param easingType easing function
       */
      public void animateTo(long duration, GeoPoint geoPoint,
                            double scale, boolean relative, Easing.Type easingType) {
          animateTo(duration, geoPoint, scale, relative, easingType, ANIM_MOVE | ANIM_SCALE);
      }
  
      /**
       * Animate to GeoPoint
       *
       * @param duration   in ms
       * @param geoPoint
       * @param scale
       * @param relative   alter scale relative to current scale
       * @param easingType easing function
       * @param state      animation state
       */
      public void animateTo(long duration, GeoPoint geoPoint,
                            double scale, boolean relative, Easing.Type easingType, int state) {
          ThreadUtils.assertMainThread();
  
          mMap.getMapPosition(mStartPos);
  
          if (relative)
              scale = mStartPos.scale * scale;
  
          scale = mMap.viewport().limitScale(scale);
  
          mDeltaPos.set(longitudeToX(geoPoint.getLongitude()) - mStartPos.x,
                  latitudeToY(geoPoint.getLatitude()) - mStartPos.y,
                  scale - mStartPos.scale,
                  0, 0);
  
          animStart(duration, state, easingType);
      }
  
      public void animateTo(MapPosition pos) {
          animateTo(500, pos);
      }
  
      public void animateTo(long duration, MapPosition pos) {
          animateTo(duration, pos, Easing.Type.LINEAR);
      }
  
      public void animateTo(long duration, MapPosition pos, Easing.Type easingType) {
          animateTo(duration, pos, easingType, ANIM_MOVE | ANIM_SCALE | ANIM_ROTATE | ANIM_TILT);
      }
  
      public void animateTo(long duration, MapPosition pos, Easing.Type easingType, int state) {
          ThreadUtils.assertMainThread();
  
          mMap.getMapPosition(mStartPos);
  
          pos.scale = mMap.viewport().limitScale(pos.scale);
          pos.tilt = mMap.viewport().limitTilt(pos.tilt);
  
          mDeltaPos.set(pos.x - mStartPos.x,
                  pos.y - mStartPos.y,
                  pos.scale - mStartPos.scale,
                  pos.bearing - mStartPos.bearing,
                  pos.tilt - mStartPos.tilt);
  
          animStart(duration, state, easingType);
      }
  
      public void animateZoom(long duration, double scaleBy,
                              float pivotX, float pivotY) {
          animateZoom(duration, scaleBy, pivotX, pivotY, Easing.Type.LINEAR);
      }
  
      public void animateZoom(long duration, double scaleBy,
                              float pivotX, float pivotY, Easing.Type easingType) {
          ThreadUtils.assertMainThread();
  
          mMap.getMapPosition(mCurPos);
  
          if (mState == ANIM_SCALE)
              scaleBy = (mStartPos.scale + mDeltaPos.scale) * scaleBy;
          else
              scaleBy = mCurPos.scale * scaleBy;
  
          mStartPos.copy(mCurPos);
          scaleBy = mMap.viewport().limitScale(scaleBy);
          if (scaleBy == 0.0)
              return;
  
          mDeltaPos.scale = scaleBy - mStartPos.scale;
  
          mPivot.x = pivotX;
          mPivot.y = pivotY;
  
          animStart(duration, ANIM_SCALE, easingType);
      }
  
      public void animateFling(float velocityX, float velocityY,
                               int xmin, int xmax, int ymin, int ymax) {
  
          ThreadUtils.assertMainThread();
  
          if (velocityX * velocityX + velocityY * velocityY < 2048)
              return;
  
          mMap.getMapPosition(mStartPos);
  
          mScroll.x = 0;
          mScroll.y = 0;
  
          float duration = 500;
  
          float flingFactor = CanvasAdapter.DEFAULT_DPI / 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, Easing.Type.SINE_OUT);
      }
  
      void animStart(float duration, int state, Easing.Type easingType) {
          if (!isActive())
              mMap.events.fire(Map.ANIM_START, mMap.mMapPosition);
          mCurPos.copy(mStartPos);
          mState = state;
          mDuration = duration;
          mAnimEnd = System.currentTimeMillis() + (long) duration;
          mEasingType = easingType;
          mMap.render();
      }
  
      /**
       * called by MapRenderer at begin of each frame.
       */
      void updateAnimation() {
          if (mState == ANIM_NONE)
              return;
  
          long millisLeft = mAnimEnd - MapRenderer.frametime;
  
          ViewController v = mMap.viewport();
  
          /* cancel animation when position was changed since last
           * update, i.e. when it was modified outside the animator. */
          if (v.getMapPosition(mCurPos)) {
              log.debug("cancel anim - changed");
              cancel();
              return;
          }
  
          float adv = clamp(1.0f - millisLeft / mDuration, 1E-6f, 1);
          // Avoid redundant calculations in case of linear easing
          if (mEasingType != Easing.Type.LINEAR) {
              adv = Easing.ease(0, (long) (adv * Long.MAX_VALUE), Long.MAX_VALUE, mEasingType);
              adv = clamp(adv, 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) {
              //log.debug("animate END");
              cancel();
          }
  
          /* remember current map position */
          final boolean changed = v.getMapPosition(mCurPos);
  
          if (changed) {
              mMap.updateMap(true);
          } else {
              mMap.postDelayed(updateTask, 10);
          }
      }
  
      Task updateTask = new Task() {
          @Override
          public int go(boolean canceled) {
              if (!canceled)
                  updateAnimation();
              return Task.DONE;
          }
      };
  
      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 void cancel() {
          //ThreadUtils.assertMainThread();
          mState = ANIM_NONE;
          mPivot.x = 0;
          mPivot.y = 0;
          mMap.events.fire(Map.ANIM_END, mMap.mMapPosition);
      }
  
      public boolean isActive() {
          return mState != ANIM_NONE;
      }
  
      /**
       * Get the map position at animation end.<br>
       * Note: valid only with animateTo methods.
       */
      public MapPosition getEndPosition() {
          MapPosition mapPosition = new MapPosition();
          mapPosition.set(mDeltaPos.x + mStartPos.x,
                  mDeltaPos.y + mStartPos.y,
                  mDeltaPos.scale + mStartPos.scale,
                  mDeltaPos.bearing + mStartPos.bearing,
                  mDeltaPos.tilt + mStartPos.tilt);
          return mapPosition;
      }
  }
  