Home Reference Source

src/Move.js

import { boundingExtent, buffer, extend, getHeight, getWidth, intersects } from 'ol/extent'
import { fromExtent } from 'ol/geom/Polygon'
import { transformExtent } from 'ol/proj'
import View from 'ol/View'

/**
 * @typedef {object} MoveOptions
 * @property {G4UMap} map
 * @property {number} [pixelPadding=50] a default padding around the target extent in pixels
 * @property {number} [meterMinSize=500] the minimal size of the target extent in meters
 * @property {number} [animationDuration=2000]
 * @property {boolean} [animations=true]
 * @property {boolean} [bouncing=false] if the animation should bounce or not
 */

/**
 * @typedef {FitOptions} SingleMoveOptions
 * @property {boolean} [animated] if specified overwrites the default settings
 * @property {number[]|string} [padding] if not set the default value (pixelPadding) is used
 * @property {number} [minSize] if not set the default value (meterMinSize) is used
 */

/**
 * Moves the map. Uses animations if desired.
 */
export class Move {
  /**
   * @param {MoveOptions} options
   */
  constructor (options) {
    /**
     * @type {G4UMap}
     * @private
     */
    this.map_ = options.map

    /**
     * @type {number}
     * @private
     */
    this.pixelPadding_ = options.pixelPadding !== undefined ? options.pixelPadding : 50

    /**
     * @type {number}
     * @private
     */
    this.meterMinSize_ = options.meterMinSize !== undefined ? options.meterMinSize : 500

    /**
     * @type {number}
     * @private
     */
    this.animationDuration_ = options.animationDuration || 2000

    /**
     * @type {boolean}
     * @private
     */
    this.animations_ = options.animations !== undefined ? options.animations : true

    /**
     * @type {boolean}
     * @private
     */
    this.bouncing_ = options.bouncing !== false
  }

  /**
   * Turns animations on or off
   * @param {boolean} animations
   */
  setAnimations (animations) {
    this.animations_ = animations
  }

  /**
   * @returns {boolean}
   */
  getAnimations () {
    return this.animations_
  }

  /**
   * @param {Coordinate} point
   * @param {SingleMoveOptions} [options={}]
   */
  toPoint (point, options = {}) {
    // calculate extent
    const tmpView = new View({
      projection: this.map_.getView().getProjection(),
      center: point,
      resolution: this.map_.getView().getResolution()
    })
    const extent = tmpView.calculateExtent(this.map_.getSize().map(s => s - 2))

    options.padding = [0, 0, 0, 0] // no padding around this extent

    this.toExtent(extent, options)
  }

  /**
   * @param {ol.Extent} extent
   * @param {SingleMoveOptions} [options={}]
   */
  toExtent (extent, options = {}) {
    let newExtent = extent
    const minSize = options.minSize !== undefined ? options.minSize : this.meterMinSize_
    if (minSize) {
      newExtent = this.bufferUpToMinSize_(extent, minSize)
    }
    if (options.padding === undefined) {
      options.padding = [this.pixelPadding_, this.pixelPadding_, this.pixelPadding_, this.pixelPadding_]
    }
    if (options.animated === undefined ? this.animations_ : options.animated) {
      this.animationZoomToExtent_(newExtent, options)
    } else {
      this.fit_(newExtent, options)
    }
  }

  /**
   * @param {ol.Point} point
   * @param {SingleMoveOptions} [options={}]
   */
  zoomToPoint (point, options = {}) {
    this.toExtent(boundingExtent([point]), options)
  }

  /**
   * @param {ol.Extent} extent
   * @param {number} minSize
   * @returns {ol.Extent}
   * @private
   */
  bufferUpToMinSize_ (extent, minSize) {
    // TODO: maybe use something more precise than transforming into 3857 to get meter size.
    let extentInMeters = transformExtent(extent, this.map_.getView().getProjection(), 'EPSG:3857')
    const smallerSize = Math.min(getWidth(extentInMeters), getHeight(extentInMeters))
    if (smallerSize < minSize) {
      extentInMeters = buffer(extentInMeters, minSize - smallerSize / 2)
      return transformExtent(extentInMeters, 'EPSG:3857', this.map_.getView().getProjection())
    } else {
      return extent
    }
  }

  /**
   * @param {ol.Extent} extent
   * @param {SingleMoveOptions} options
   * @private
   */
  fit_ (extent, options) {
    if (options.padding === undefined) {
      options.padding = [this.pixelPadding_, this.pixelPadding_, this.pixelPadding_, this.pixelPadding_]
    }

    // options.constrainResolution = false

    // using fit's padding option
    this.map_.getView().fit(fromExtent(extent), this.map_.getSize(), options)
  }

  /**
   * This function glides or bounces to an extent
   * @param {ol.Extent} endExtent
   * @param {SingleMoveOptions} options
   * @private
   */
  animationZoomToExtent_ (endExtent, options) {
    const map = this.map_
    const view = map.getView()
    const size = map.getSize()

    const startExtent = view.calculateExtent(size)

    const moveExtent = extend(startExtent.slice(0), endExtent) // a extent where both extents are contained.

    if (this.bouncing_ && !intersects(startExtent, endExtent)) {
      const moveOptions = Object.assign({
        duration: this.animationDuration_ / 2
      }, options)
      view.fit(moveExtent, moveOptions)
      setTimeout(() => {
        const endOptions = Object.assign({
          duration: this.animationDuration_ / 2
        }, options)
        view.fit(endExtent, endOptions)
      }, this.animationDuration_ / 2)
    } else {
      const endOptions = Object.assign({
        duration: this.animationDuration_ / 2
      }, options)
      view.fit(endExtent, endOptions)
    }
  }

  /**
   * Easing function based on a circle function
   * @param {number} t
   * @returns {number}
   * @private
   */
  earlyFastRiseEasing_ (t) {
    return Math.sqrt(2 * t - Math.pow(t, 2))
  }

  /**
   * Easing function based on a circle function
   * @param {number} t
   * @returns {number}
   * @private
   */
  lateFastRiseEasing_ (t) {
    return 1 - Math.sqrt(1 - Math.pow(t, 2))
  }

  /**
   * Easing function based on a parabolic function
   * @param {number} t
   * @returns {number}
   * @private
   */
  earlyFastRiseEasing2_ (t) {
    return -Math.pow((t - 1), 2) + 1
  }

  /**
   * Easing function based on a parabolic function
   * @param {number} t
   * @returns {number}
   * @private
   */
  lateFastRiseEasing2_ (t) {
    return Math.pow(t, 2)
  }
}