API Docs for: 0.1.1
Show:

File: src/components/map/core/zoom/mapZoom.js

(function () {
  /*---------------------
  ------- IMPORT --------
  ----------------------*/
  var eventListeners = window.flatworld.eventListeners;
  var utils = window.flatworld.utils;
  var log = window.flatworld.log

  /*---------------------
  --------- API ---------
  ----------------------*/
  window.flatworld.extensions.mapZoom = setupMap_zoom();

  /*---------------------
  -------- PUBLIC -------
  ----------------------*/
  /**
   * Core plugin for the engine. Handles zooming for the map. Core plugins can always be overwrote if needed. Zooming happens when the
   * user scrolls the mousewheel or in mobile, pinches the screen.
   *
   * @class extensions.mapZoom
   * @return {Object}      init
   */
  function setupMap_zoom() {
    /*---------------------
    ------ VARIABLES ------
    ----------------------*/
    const TIMEOUT_AFTER_ZOOM = 40;
    var initialized = false;
    var mobileInitialized = false;
    var difference = {};
    var map;
    /**
     * Maximum and minimum amount, the player can zoom the map
     *
     * @attribute zoomLimit
     * @type {{ farther: Number, closer: Number }}
     */
    var zoomLimit = {
      farther: 0.4,
      closer: 2.5,
    };
    /**
     * How much one step of zooming affects
     *
     * @attribute zoomModifier
     * @type {Number}
     */
    var zoomModifier = 0.1;

    /*---------------------
    --------- API ---------
    ---------------------*/
    return {
      init,
      pluginName: 'mapZoom',
    };

    /*---------------------
    ------- PUBLIC --------
    ----------------------*/
    /**
     * Required init functions for the plugin
     *
     * @method init
     * @param {Map} mapObj       instantiated Map class object
     *
     * @todo think through should setZoomLimits and setZoomModifier be in map.prototype?
     * But zoomLimit and modifier need to be setable in creation, init or later with setters
     **/
    function init(thisMap) {
      map = thisMap;
      map.setPrototype('zoomIn', zoomIn);
      map.setPrototype('zoomOut', zoomOut);
      map.setPrototype('setZoomLimits', setZoomLimits);
      map.setPrototype('setZoomModifier', setZoomModifier);

      /* Singleton should have been instantiated before, we only retrieve it with 0 params */
      eventListeners.on('zoom', unifiedEventCB);
    }

    /*----------------------------------------
    ------ PROTOTYPE extensions for map ------
    ----------------------------------------*/
    /**
     * How much one mouse wheel step zooms
     *
     * @method setZoomModifier
     * @param {Number} amount           How much one mouse wheel step zooms. Needs to be in between 0 - 0.5
     **/
    function setZoomModifier(amount) {
      if (! (amount > 0 || amount <= 0.5)) {
        throw new Error('Wrong zoom modifier! (needs to be >0 and <=0.5, given:' + amount);
      }
      zoomModifier = amount;

      return this;
    }
    /**
     * How much can be zoomed in maximum and minimum
     *
     * @method setZoomLimits
     * @param {Number} farther          (>1) How much one mouse wheel step zooms out
     * @param {Number} closer           (0 - 1) How much one mouse wheel step zooms in
     **/
    function setZoomLimits(farther, closer) {
      zoomLimit.farther = farther;
      zoomLimit.closer = closer;

      return this;
    }
    /**
     * Zoom in to the map
     *
     * @method zoomIn
     * @param {Number} amount how much map is zoomed in
     * */
    function zoomIn(amount) {
      var presentScale = this.getZoom();
      const IS_ZOOM_IN = true;

      return _zoom(this, presentScale, Math.abs(amount) || zoomModifier, IS_ZOOM_IN);
    }
    /**
     * Zoom out of the map
     *
     * @method zoomOut
     * @param {Number} amount how much map is zoomed out
     * */
    function zoomOut(amount) {
      var presentScale = this.getZoom();
      const IS_ZOOM_IN = false;

      amount = amount < 0 ? amount : -amount;

      return _zoom(this, presentScale, amount || -zoomModifier, IS_ZOOM_IN);
    }

    /*---------------------------
    ------ EVENT FUNCTIONS ------
    ---------------------------*/
    /**
     * This starts the correct eventListener for the current environment. Mousewheel and pinch differ quite dramatically
     * so we keep them as separate functions.
     *
     * @method unifiedEventCB
     * @param  {Event} e             Event object
     * @param  {Integer} delta       Hamster.js provided delta
     * @param  {Integer} deltaX      Hamster.js provided delta
     * @param  {Integer} deltaY      Hamster.js provided delta
     */
    function unifiedEventCB(e, delta, deltaX, deltaY) {
      if (delta) {
        handleZoomEventDesktop(e, delta, deltaX, deltaY);
      } else if (e.pointers) {
        if (!mobileInitialized) {
          mobileInitialized = true;
          setZoomModifier(zoomModifier * 0.5);
        }
        handleZoomEventMobile(e);
      }
    }
    /**
     * Setup desktop zoomEvent by currying. Internally: Sets up correct scale + moves map accordingly to zoom to the
     * current center coordinates
     *
     * @method handleZoomEventDesktop
     * @param  {Map} map             Map instance
     */
    function handleZoomEventDesktop(e, delta, deltaX, deltaY) {
      var mouseWheelDelta = deltaY;
      /* Scale changes when the map is drawn. We make calculations with the old scale before draw */
      var oldScale = map.getZoom();

      /* No nasty scrolling side-effects */
      e.preventDefault();

      if (mouseWheelDelta > 0) {
        if (map.zoomIn()) {
          map.moveMap(_calculateCenterMoveCoordinates(oldScale, true));
        }
      } else if (mouseWheelDelta < 0) {
        if (map.zoomOut()) {
          map.moveMap(_calculateCenterMoveCoordinates(oldScale));
        }
      }
    }
    /**
     * handleZoomEventMobile
     *
     * @method handleZoomEventMobile
     * @param  {Event} e
     */
    function handleZoomEventMobile(e) {
      var pointers = e.pointers;
      var coords = [{
        x: pointers[0].pageX,
        y: pointers[0].pageY,
      }, {
        x: pointers[1].pageX,
        y: pointers[1].pageY,
      }];
      var changeX = Math.abs(coords[0].x - coords[1].x);
      var changeY = Math.abs(coords[0].y - coords[1].y);

      e.preventDefault();

      try {
        if (!initialized) {
          difference = {
            x: changeX,
            y: changeY,
          };
          eventListeners.setActivityState('zoom', true);
          initialized = true;

          return;
        } else if (e.eventType === 4 || e.eventType === 8) { /* e.eventType 4 = event canceled, e.eventType 8 = event finished */
          /* We don't want another event to be fired right after a pinch event. It makes the zoomign experience rather
           * bad if after zoom there is immediately an unexplainable drag and the map moves a bit
           * */
          window.setTimeout(function () {
            eventListeners.setActivityState('zoom', false);
          }, TIMEOUT_AFTER_ZOOM);
          initialized = false;
        }

        if (difference.x + difference.y < changeX + changeY) {
          if (map.zoomIn()) {
            map.moveMap(_calculateCenterMoveCoordinates(map.getZoom(), true));
          }
        } else {
          if (map.zoomOut()) {
            map.moveMap(_calculateCenterMoveCoordinates(map.getZoom()));
          }
        }

        difference = {
          x: changeX,
          y: changeY,
        };
      } catch (ev) {
        log('Error! ', ev);
      }
    }

    /*---------------------
    ------- PRIVATE -------
    ---------------------*/
    /**
     * _isOverZoomLimit
     *
     * @private
     * @static
     * @method _isOverZoomLimit
     **/
    function _isOverZoomLimit(amount, isZoomIn) {
      if ((isZoomIn && amount > zoomLimit.closer) || (!isZoomIn && amount < zoomLimit.farther)) {
        return true;
      }

      return false;
    }
    /**
     * @private
     * @static
     * @method _calculateCenterMoveCoordinates
     **/
    function _calculateCenterMoveCoordinates(scale, isZoomIn) {
      var windowSize = utils.resize.getWindowSize();
      var halfWindowSize = {
        x: (windowSize.x / 2) / scale,
        y: (windowSize.y / 2) / scale,
      };
      var realMovement = {
        x: (halfWindowSize.x) * ((isZoomIn ? -zoomModifier : zoomModifier)),
        y: (halfWindowSize.y) * ((isZoomIn ? -zoomModifier : zoomModifier)),
      };

      return realMovement;
    }
    /**
     * @private
     * @static
     * @method _zoom
     * @todo zoom should always product integers, not floats (this seems to happen)
     **/
    function _zoom(map, presentScale, amount, isZoomIn) {
      var newScale;

      if (!_isOverZoomLimit(presentScale, isZoomIn)) {
        newScale = map.setZoom(amount ? presentScale + amount : presentScale + zoomModifier);
      }

      return newScale;
    }
  }
})();