zingchart/zingtouch

View on GitHub
src/gestures/Swipe.js

Summary

Maintainability
C
1 day
Test Coverage
/**
 * @file Swipe.js
 * Contains the Swipe class
 */

import Gesture from './Gesture.js';
import util from './../core/util.js';

const DEFAULT_INPUTS = 1;
const DEFAULT_MAX_REST_TIME = 100;
const DEFAULT_ESCAPE_VELOCITY = 0.2;
const DEFAULT_TIME_DISTORTION = 100;
const DEFAULT_MAX_PROGRESS_STACK = 10;

/**
 * A swipe is defined as input(s) moving in the same direction in an relatively
 * increasing velocity and leaving the screen at some point before it drops
 * below it's escape velocity.
 * @class Swipe
 */
class Swipe extends Gesture {

  /**
   * Constructor function for the Swipe class.
   * @param {Object} [options] - The options object.
   * @param {Number} [options.numInputs] - The number of inputs to trigger a
   * Swipe can be variable, and the maximum number being a factor of the browser
   *  move and current move events.
   * @param {Number} [options.maxRestTime] - The maximum resting time a point
   *  has between it's last
   * @param {Number} [options.escapeVelocity] - The minimum velocity the input
   *  has to be at to emit a swipe.
   * @param {Number} [options.timeDistortion] - (EXPERIMENTAL) A value of time
   *  in milliseconds to distort between events.
   * @param {Number} [options.maxProgressStack] - (EXPERIMENTAL)The maximum
   *  amount of move events to keep
   * track of for a swipe.
   */
  constructor(options) {
    super();
    /**
     * The type of the Gesture
     * @type {String}
     */
    this.type = 'swipe';

    /**
     * The number of inputs to trigger a Swipe can be variable,
     * and the maximum number being a factor of the browser.
     * @type {Number}
     */
    this.numInputs = (options && options.numInputs) ?
      options.numInputs : DEFAULT_INPUTS;

    /**
     * The maximum resting time a point has between it's last move and
     * current move events.
     * @type {Number}
     */
    this.maxRestTime = (options && options.maxRestTime) ?
      options.maxRestTime : DEFAULT_MAX_REST_TIME;

    /**
     * The minimum velocity the input has to be at to emit a swipe.
     * This is useful for determining the difference between
     * a swipe and a pan gesture.
     * @type {number}
     */
    this.escapeVelocity = (options && options.escapeVelocity) ?
      options.escapeVelocity : DEFAULT_ESCAPE_VELOCITY;

    /**
     * (EXPERIMENTAL) A value of time in milliseconds to distort between events.
     * Browsers do not accurately measure time with the Date constructor in
     * milliseconds, so consecutive events sometimes display the same timestamp
     * but different x/y coordinates. This will distort a previous time
     * in such cases by the timeDistortion's value.
     * @type {number}
     */
    this.timeDistortion = (options && options.timeDistortion) ?
      options.timeDistortion : DEFAULT_TIME_DISTORTION;

    /**
     * (EXPERIMENTAL) The maximum amount of move events to keep track of for a
     * swipe. This helps give a more accurate estimate of the user's velocity.
     * @type {number}
     */
    this.maxProgressStack = (options && options.maxProgressStack) ?
      options.maxProgressStack : DEFAULT_MAX_PROGRESS_STACK;

    /**
     * The on move callback
     */
    if (options && options.onMove && typeof options.onMove === 'function') {
      this.onMove = options.onMove
    }
    /**
     * The on end callback
     */
    if (options && options.onEnd && typeof options.onEnd === 'function') {
      this.onEnd = options.onEnd
    }
  }

  /**
   * Event hook for the move of a gesture. Captures an input's x/y coordinates
   * and the time of it's event on a stack.
   * @param {Array} inputs - The array of Inputs on the screen.
   * @param {Object} state - The state object of the current region.
   * @param {Element} element - The element associated to the binding.
   * @return {null} - Swipe does not emit from a move.
   */
  move(inputs, state, element) {
    if (this.numInputs === inputs.length) {
      for (let i = 0; i < inputs.length; i++) {
        let progress = inputs[i].getGestureProgress(this.getId());
        if (!progress.moves) {
          progress.moves = [];
        }

        progress.moves.push({
          time: new Date().getTime(),
          x: inputs[i].current.x,
          y: inputs[i].current.y,
        });

        if (progress.length > this.maxProgressStack) {
          progress.moves.shift();
        }
      }
    }

    if(this.onMove) {
      this.onMove(inputs, state, element);
    }

    return null;
  }

  /* move*/

  /**
   * Determines if the input's history validates a swipe motion.
   * Determines if it did not come to a complete stop (maxRestTime), and if it
   * had enough of a velocity to be considered (ESCAPE_VELOCITY).
   * @param {Array} inputs - The array of Inputs on the screen
   * @return {null|Object} - null if the gesture is not to be emitted,
   *  Object with information otherwise.
   */
  end(inputs) {
    if (this.numInputs === inputs.length) {
      let output = {
        data: [],
      };

      for (let i = 0; i < inputs.length; i++) {
        // Determine if all input events are on the 'end' event.
        if (inputs[i].current.type !== 'end') {
          return;
        }

        let progress = inputs[i].getGestureProgress(this.getId());
        if (progress.moves && progress.moves.length > 2) {
          // CHECK : Return if the input has not moved in maxRestTime ms.

          let currentMove = progress.moves.pop();
          if ((new Date().getTime()) - currentMove.time > this.maxRestTime) {
            return null;
          }

          let lastMove;
          let index = progress.moves.length - 1;

          /* Date is unreliable, so we retrieve the last move event where
           the time is not the same. */
          while (index !== -1) {
            if (progress.moves[index].time !== currentMove.time) {
              lastMove = progress.moves[index];
              break;
            }

            index--;
          }

          /* If the date is REALLY unreliable, we apply a time distortion
           to the last event.
           */
          if (!lastMove) {
            lastMove = progress.moves.pop();
            lastMove.time += this.timeDistortion;
          }

          var velocity = util.getVelocity(lastMove.x, lastMove.y, lastMove.time,
            currentMove.x, currentMove.y, currentMove.time);

          output.data[i] = {
            velocity: velocity,
            distance: util.distanceBetweenTwoPoints(lastMove.x, currentMove.x, lastMove.y, currentMove.y),
            duration:  currentMove.time - lastMove.time,
            currentDirection: util.getAngle(
              lastMove.x,
              lastMove.y,
              currentMove.x,
              currentMove.y),
          };
        }
      }

      for (var i = 0; i < output.data.length; i++) {
        if (output.data[i].velocity < this.escapeVelocity) {
          return null;
        }
      }

      if (output.data.length > 0) {
        if(this.onEnd) {
          this.onEnd(inputs, output);
        }
        return output;
      }
    }

    return null;
  }

  /* end*/
}

export default Swipe;