content: /*
   * Copyright 2013 Hannes Janetzek
   * Copyright 2016 Stephan Leuschner
   * Copyright 2016 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.Point;
  import org.oscim.core.Tile;
  import org.oscim.renderer.MapRenderer;
  import org.oscim.utils.ThreadUtils;
  import org.oscim.utils.animation.DragForce;
  import org.oscim.utils.animation.Easing;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static org.oscim.utils.FastMath.clamp;
  
  public class Animator2 extends Animator {
      private static final Logger log = LoggerFactory.getLogger(Animator2.class);
  
      private static final int ANIM_KINETIC = 1 << 5;
  
      /**
       * The minimum changes that are pleasant for users.
       */
      private static final float DEFAULT_MIN_VISIBLE_CHANGE_PIXELS = 0.6f;
      private static final float DEFAULT_MIN_VISIBLE_CHANGE_RADIAN = 0.001f;
      private static final float DEFAULT_MIN_VISIBLE_CHANGE_SCALE = 1f;
  
      /**
       * The friction scalar for fling movements (1 as base).
       */
      public static float FLING_FRICTION_MOVE = 1.0f;
  
      /**
       * The friction scalar for fling rotations (1 as base).
       */
      public static float FLING_FRICTION_ROTATE = 1.2f;
  
      /**
       * The friction scalar for fling scales (1 as base).
       */
      public static float FLING_FRICTION_SCALE = 1.2f;
  
      private final DragForce mFlingRotateForce = new DragForce();
      private final DragForce mFlingScaleForce = new DragForce();
      private final DragForce mFlingScrollForce = new DragForce();
  
      private final Point mMovePoint = new Point();
      private final Point mScrollRatio = new Point();
  
      private long mFrameStart = -1;
      private float mScrollDet2D = 1f;
  
      public Animator2(Map map) {
          super(map);
  
          // Init fling force thresholds
          mFlingRotateForce.setValueThreshold(DEFAULT_MIN_VISIBLE_CHANGE_RADIAN);
          mFlingScrollForce.setValueThreshold(DEFAULT_MIN_VISIBLE_CHANGE_PIXELS);
          mFlingScaleForce.setValueThreshold(DEFAULT_MIN_VISIBLE_CHANGE_SCALE);
      }
  
      /**
       * Animates a physical fling for rotations.
       *
       * @param angularVelocity angular velocity in radians
       */
      public void animateFlingRotate(float angularVelocity, float pivotX, float pivotY) {
          ThreadUtils.assertMainThread();
  
          float flingFactor = -0.25f; // Can be changed but should be standardized for all callers
          angularVelocity *= flingFactor;
  
          mFlingRotateForce.setFrictionScalar(FLING_FRICTION_ROTATE);
          mFlingRotateForce.setValueAndVelocity(0f, angularVelocity);
  
          if (!isActive()) {
              mMap.getMapPosition(mStartPos);
  
              mPivot.x = pivotX;
              mPivot.y = pivotY;
  
              animFlingStart(ANIM_ROTATE);
          } else {
              mState |= ANIM_ROTATE;
          }
      }
  
      /**
       * Animates a physical fling for scrolls.
       *
       * @param velocityX the x velocity depends on screen resolution
       * @param velocityY the y velocity depends on screen resolution
       */
      public void animateFlingScroll(float velocityX, float velocityY,
                                     int xmin, int xmax, int ymin, int ymax) {
          ThreadUtils.assertMainThread();
  
          if (velocityX * velocityX + velocityY * velocityY < 2048)
              return;
  
          float flingFactor = 2.0f; // Can be changed but should be standardized for all callers
          float screenFactor = CanvasAdapter.DEFAULT_DPI / CanvasAdapter.dpi;
  
          velocityX *= screenFactor * flingFactor;
          velocityY *= screenFactor * flingFactor;
          velocityX = clamp(velocityX, xmin, xmax);
          velocityY = clamp(velocityY, ymin, ymax);
  
          float sumVelocity = Math.abs(velocityX) + Math.abs(velocityY);
          mScrollRatio.x = velocityX / sumVelocity;
          mScrollRatio.y = velocityY / sumVelocity;
          mScrollDet2D = (float) (mScrollRatio.x * mScrollRatio.x + mScrollRatio.y * mScrollRatio.y);
  
          mFlingScrollForce.setFrictionScalar(FLING_FRICTION_MOVE);
          mFlingScrollForce.setValueAndVelocity(0f, (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY));
  
          if (!isActive()) {
              mMap.getMapPosition(mStartPos);
  
              animFlingStart(ANIM_MOVE);
          } else {
              mState |= ANIM_MOVE;
          }
      }
  
      /**
       * Animates a physical fling for zooms.
       *
       * @param scaleVelocity the scale velocity depends on screen resolution
       */
      public void animateFlingZoom(float scaleVelocity, float pivotX, float pivotY) {
          ThreadUtils.assertMainThread();
  
          // Trekarta changes, was -1.0f
          float flingFactor = -0.3f; // Can be changed but should be standardized for all callers
          float screenFactor = CanvasAdapter.DEFAULT_DPI / CanvasAdapter.dpi;
          scaleVelocity *= flingFactor * screenFactor;
  
          mFlingScaleForce.setFrictionScalar(FLING_FRICTION_SCALE);
          mFlingScaleForce.setValueAndVelocity(0f, scaleVelocity);
  
          if (!isActive()) {
              mMap.getMapPosition(mStartPos);
  
              mPivot.x = pivotX;
              mPivot.y = pivotY;
  
              animFlingStart(ANIM_SCALE);
          } else {
              mState |= ANIM_SCALE;
          }
      }
  
      private void animFlingStart(int state) {
          if (!isActive())
              mMap.events.fire(Map.ANIM_START, mMap.mMapPosition);
          mCurPos.copy(mStartPos);
          mState |= ANIM_FLING | state;
          mFrameStart = MapRenderer.frametime; // CurrentTimeMillis would cause negative delta
          mMap.render();
      }
  
      /**
       * Alternative implementation of Animator's <code>animateFling</code>.
       * Uses scheme of predictable animations using mDeltaPos.
       *
       * @param velocityX the x velocity depends on screen resolution
       * @param velocityY the y velocity depends on screen resolution
       */
      public void kineticScroll(float velocityX, float velocityY,
                                int xmin, int xmax, int ymin, int ymax) {
          ThreadUtils.assertMainThread();
  
          if (velocityX * velocityX + velocityY * velocityY < 2048)
              return;
  
          mMap.getMapPosition(mStartPos);
  
          float duration = 500;
  
          float screenFactor = CanvasAdapter.DEFAULT_DPI / CanvasAdapter.dpi;
          velocityX = velocityX * screenFactor;
          velocityY = velocityY * screenFactor;
          velocityX = clamp(velocityX, xmin, xmax);
          velocityY = clamp(velocityY, ymin, ymax);
          if (Float.isNaN(velocityX) || Float.isNaN(velocityY)) {
              log.debug("fling NaN!");
              return;
          }
  
          double tileScale = mStartPos.scale * Tile.SIZE;
          ViewController.applyRotation(-velocityX, -velocityY, mStartPos.bearing, mMovePoint);
          mDeltaPos.setX(mMovePoint.x / tileScale);
          mDeltaPos.setY(mMovePoint.y / tileScale);
  
          animStart(duration, ANIM_KINETIC | ANIM_MOVE, Easing.Type.SINE_OUT);
      }
  
      /**
       * called by MapRenderer at begin of each frame.
       */
      @Override
      void updateAnimation() {
          if (mState == ANIM_NONE)
              return;
  
          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;
          }
  
          final long currentFrametime = MapRenderer.frametime;
  
          if ((mState & ANIM_FLING) == 0) {
              // Do predicted animations
              long millisLeft = mAnimEnd - currentFrametime;
  
              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_KINETIC) != 0) {
                  // Reduce value to simulate kinetic behaviour
                  adv = (float) Math.sqrt(adv);
              }
  
              if ((mState & ANIM_MOVE) != 0) {
                  v.moveTo(mStartPos.x + mDeltaPos.x * (adv / scaleAdv),
                          mStartPos.y + mDeltaPos.y * (adv / scaleAdv));
              }
  
              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();
              }
          } else {
              // Do physical fling animation
              long deltaT = currentFrametime - mFrameStart;
              mFrameStart = currentFrametime;
  
              if ((mState & ANIM_SCALE) != 0) {
                  float valueDelta = mFlingScaleForce.updateValueAndVelocity(deltaT) / 1000f;
                  float velocity = mFlingScaleForce.getVelocity();
                  if (valueDelta != 0) {
                      valueDelta = valueDelta > 0 ? valueDelta + 1 : -1 / (valueDelta - 1);
                      v.scaleMap(valueDelta, (float) mPivot.x, (float) mPivot.y);
                  }
  
                  if (velocity == 0) {
                      mState &= (~ANIM_SCALE); // End scale mode
                  }
              }
  
              if ((mState & ANIM_MOVE) != 0) {
                  float valueDelta = mFlingScrollForce.updateValueAndVelocity(deltaT);
                  float velocity = mFlingScrollForce.getVelocity();
  
                  float valFactor = (float) Math.sqrt((valueDelta * valueDelta) / mScrollDet2D);
                  float dx = (float) mScrollRatio.x * valFactor;
                  float dy = (float) mScrollRatio.y * valFactor;
  
                  if (dx != 0 || dy != 0) {
                      v.moveMap(dx, dy);
                  }
  
                  if (velocity == 0) {
                      mState &= (~ANIM_MOVE); // End move mode
                  }
              }
  
              if ((mState & ANIM_ROTATE) != 0) {
                  float valueDelta = mFlingRotateForce.updateValueAndVelocity(deltaT);
                  float velocity = mFlingRotateForce.getVelocity();
  
                  v.rotateMap(valueDelta, (float) mPivot.x, (float) mPivot.y);
  
                  if (velocity == 0) {
                      mState &= (~ANIM_ROTATE); // End rotate mode
                  }
              }
  
              /*if ((mState & ANIM_TILT) != 0) {
                  // Do some tilt fling
                  if(velocity == 0) {
                      mState &= (~ANIM_TILT); // End tilt mode
                  }
              }*/
  
              if ((mState & (ANIM_MOVE | ANIM_ROTATE | ANIM_SCALE)) == 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);
          }
      }
  }
  