svgpan.js

/**
 *  SVGPan library 1.2.2
 * ======================
 *
 * Given an unique existing element with a given id (or by default, the first
 * g-element), including the library into any SVG adds the following
 * capabilities:
 *
 *  - Mouse panning
 *  - Mouse zooming (using the wheel)
 *  - Object dragging
 *
 * You can configure the behaviour of the pan/zoom/drag via setOptions().
 *
 * Known issues:
 *
 *  - Zooming (while panning) on Safari has still some issues
 *
 * Releases:
 *
 * 1.2.2, Tue Aug 30 17:21:56 CEST 2011, Andrea Leofreddi
 *  - Fixed viewBox on root tag (#7)
 *  - Improved zoom speed (#2)
 *
 * 1.2.1, Mon Jul  4 00:33:18 CEST 2011, Andrea Leofreddi
 *  - Fixed a regression with mouse wheel (now working on Firefox 5)
 *  - Working with viewBox attribute (#4)
 *  - Added "use strict;" and fixed resulting warnings (#5)
 *  - Added configuration variables, dragging is disabled by default (#3)
 *
 * 1.2, Sat Mar 20 08:42:50 GMT 2010, Zeng Xiaohui
 *  Fixed a bug with browser mouse handler interaction
 *
 * 1.1, Wed Feb  3 17:39:33 GMT 2010, Zeng Xiaohui
 *  Updated the zoom code to support the mouse wheel on Safari/Chrome
 *
 * 1.0, Andrea Leofreddi
 *  First release
 */

/**
 * @license
 * This code is licensed under the following BSD license:
 * Copyright 2009-2010 Andrea Leofreddi <a.leofreddi@itcharm.com>. All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY Andrea Leofreddi ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL Andrea Leofreddi OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are
 * those of the authors and should not be interpreted as representing official
 * policies, either expressed or implied, of Andrea Leofreddi.
 *
 */

goog.provide('svgpan.SvgPan');

goog.require('goog.Disposable');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.events.MouseWheelHandler');



/**
 * Instantiates an SvgPan object.
 * @param {string=} opt_graphElementId The id of the graph element.
 * @param {Element=} opt_root An optional document root.
 * @constructor
 * @extends {goog.Disposable}
 */
svgpan.SvgPan = function(opt_graphElementId, opt_root) {
  goog.base(this);

  /** @private {Element} */
  this.root_ = opt_root || document.documentElement;

  /** @private {?string} */
  this.graphElementId_ = opt_graphElementId || null;

  /** @private {boolean} */
  this.cancelNextClick_ = false;

  /** @private {boolean} */
  this.enablePan_ = true;

  /** @private {boolean} */
  this.enableZoom_ = true;

  /** @private {boolean} */
  this.enableDrag_ = false;

  /** @private {number} */
  this.zoomScale_ = 0.4;

  /** @private {svgpan.SvgPan.State} */
  this.state_ = svgpan.SvgPan.State.NONE;

  /** @private {Element} */
  this.svgRoot_ = null;

  /** @private {Element} */
  this.stateTarget_ = null;

  /** @private {Element} */
  this.stateOrigin_ = null;

  /** @private {SVGMatrix} */
  this.stateTf_ = null;

  /** @private {goog.events.MouseWheelHandler} */
  this.mouseWheelHandler_ = null;

  this.setupHandlers_();
};
goog.inherits(svgpan.SvgPan, goog.Disposable);


/** @override */
svgpan.SvgPan.prototype.disposeInternal = function() {
  goog.base(this, 'disposeInternal');
  goog.events.removeAll(this.root_);
  this.mouseWheelHandler_.dispose();
};


/**
 * @enum {string}
 */
svgpan.SvgPan.State = {
  NONE: 'none',
  PAN: 'pan',
  DRAG: 'drag'
};


/**
 * Enables/disables panning the entire SVG (default = true).
 * @param {boolean} enabled Whether or not to allow panning.
 */
svgpan.SvgPan.prototype.setPanEnabled = function(enabled) {
  this.enablePan_ = enabled;
};


/**
 * Enables/disables zooming (default = true).
 * @param {boolean} enabled Whether or not to allow zooming (default = true).
 */
svgpan.SvgPan.prototype.setZoomEnabled = function(enabled) {
  this.enableZoom_ = enabled;
};


/**
 * Enables/disables dragging individual SVG objects (default = false).
 * @param {boolean} enabled Whether or not to allow dragging of objects.
 */
svgpan.SvgPan.prototype.setDragEnabled = function(enabled) {
  this.enableDrag_ = enabled;
};


/**
 * Sets the sensitivity of mousewheel zooming (default = 0.4).
 * @param {number} scale The new zoom scale.
 */
svgpan.SvgPan.prototype.setZoomScale = function(scale) {
  this.zoomScale_ = scale;
};


/**
 * Registers mouse event handlers.
 * @private
 */
svgpan.SvgPan.prototype.setupHandlers_ = function() {
  goog.events.listen(this.root_, goog.events.EventType.CLICK,
      goog.bind(this.handleMouseClick_, this));
  goog.events.listen(this.root_, goog.events.EventType.MOUSEUP,
      goog.bind(this.handleMouseUp_, this));
  goog.events.listen(this.root_, goog.events.EventType.MOUSEDOWN,
      goog.bind(this.handleMouseDown_, this));
  goog.events.listen(this.root_, goog.events.EventType.MOUSEMOVE,
      goog.bind(this.handleMouseMove_, this));
  this.mouseWheelHandler_ = new goog.events.MouseWheelHandler(this.root_);
  goog.events.listen(this.mouseWheelHandler_,
      goog.events.MouseWheelHandler.EventType.MOUSEWHEEL,
      goog.bind(this.handleMouseWheel_, this));
};


/**
 * Retrieves the root element for SVG manipulation. The element is then cached.
 * @param {Document} svgDoc The document.
 * @return {Element} The svg root.
 * @private
 */
svgpan.SvgPan.prototype.getRoot_ = function(svgDoc) {
  if (!this.svgRoot_) {
    var r = this.graphElementId_ ?
        svgDoc.getElementById(this.graphElementId_) : svgDoc.documentElement;
    var t = r;
    while (t != svgDoc) {
      if (t.getAttribute('viewBox')) {
        this.setCtm_(r, r.getCTM());
        t.removeAttribute('viewBox');
      }
      t = t.parentNode;
    }
    this.svgRoot_ = r;
  }
  return this.svgRoot_;
};


/**
 * Instantiates an SVGPoint object with given event coordinates.
 * @param {!goog.events.Event} evt The event with coordinates.
 * @return {SVGPoint} The created point.
 * @private
 */
svgpan.SvgPan.prototype.getEventPoint_ = function(evt) {
  return this.newPoint_(evt.clientX, evt.clientY);
};


/**
 * Instantiates an SVGPoint object with given coordinates.
 * @param {number} x The x coordinate.
 * @param {number} y The y coordinate.
 * @return {SVGPoint} The created point.
 * @private
 */
svgpan.SvgPan.prototype.newPoint_ = function(x, y) {
  var p = this.root_.createSVGPoint();
  p.x = x;
  p.y = y;
  return p;
};


/**
 * Sets the current transform matrix of an element.
 * @param {Element} element The element.
 * @param {SVGMatrix} matrix The transform matrix.
 * @private
 */
svgpan.SvgPan.prototype.setCtm_ = function(element, matrix) {
  var s = 'matrix(' + matrix.a + ',' + matrix.b + ',' + matrix.c + ',' +
      matrix.d + ',' + matrix.e + ',' + matrix.f + ')';
  element.setAttribute('transform', s);
};


/**
 * Handle mouse wheel event.
 * @param {!goog.events.Event} evt The event.
 * @private
 */
svgpan.SvgPan.prototype.handleMouseWheel_ = function(evt) {
  if (!this.enableZoom_)
    return;

  // Prevents scrolling.
  evt.preventDefault();

  var svgDoc = evt.target.ownerDocument;

  var delta = evt.deltaY / -9;
  var z = Math.pow(1 + this.zoomScale_, delta);
  var g = this.getRoot_(svgDoc);
  var p = this.getEventPoint_(evt);
  p = p.matrixTransform(g.getCTM().inverse());

  // Compute new scale matrix in current mouse position
  var k = this.root_.createSVGMatrix().translate(
      p.x, p.y).scale(z).translate(-p.x, -p.y);
  this.setCtm_(g, g.getCTM().multiply(k));

  if (typeof(this.stateTf_) == 'undefined') {
    this.stateTf_ = g.getCTM().inverse();
  }
  this.stateTf_ =
      this.stateTf_ ? this.stateTf_.multiply(k.inverse()) : this.stateTf_;
};


/**
 * Handle mouse move event.
 * @param {!goog.events.Event} evt The event.
 * @private
 */
svgpan.SvgPan.prototype.handleMouseMove_ = function(evt) {
  if (evt.button != 0) {
    return;
  }
  this.handleMove(evt.clientX, evt.clientY, evt.target.ownerDocument);
};


/**
 * Handles mouse motion for the given coordinates.
 * @param {number} x The x coordinate.
 * @param {number} y The y coordinate.
 * @param {Element} svgDoc The svg document.
 */
svgpan.SvgPan.prototype.handleMove = function(x, y, svgDoc) {
  var g = this.getRoot_(svgDoc);
  if (this.state_ == svgpan.SvgPan.State.PAN && this.enablePan_) {
    // Pan mode
    var p = this.newPoint_(x, y).matrixTransform(this.stateTf_);
    this.setCtm_(g, this.stateTf_.inverse().translate(
        p.x - this.stateOrigin_.x, p.y - this.stateOrigin_.y));
    this.cancelNextClick_ = true;
  } else if (this.state_ == svgpan.SvgPan.State.DRAG && this.enableDrag_) {
    // Drag mode
    var p = this.newPoint_(x, y).matrixTransform(g.getCTM().inverse());
    this.setCtm_(this.stateTarget_, this.root_.createSVGMatrix().translate(
        p.x - this.stateOrigin_.x, p.y - this.stateOrigin_.y).multiply(
        g.getCTM().inverse()).multiply(this.stateTarget_.getCTM()));
    this.stateOrigin_ = p;
  }
};


/**
 * Handle click event.
 * @param {!goog.events.Event} evt The event.
 * @private
 */
svgpan.SvgPan.prototype.handleMouseDown_ = function(evt) {
  if (evt.button != 0) {
    return;
  }
  // Prevent selection while dragging.
  evt.preventDefault();
  var svgDoc = evt.target.ownerDocument;

  var g = this.getRoot_(svgDoc);

  if (evt.target.tagName == 'svg' || !this.enableDrag_) {
    // Pan mode
    this.state_ = svgpan.SvgPan.State.PAN;
    this.stateTf_ = g.getCTM().inverse();
    this.stateOrigin_ = this.getEventPoint_(evt).matrixTransform(this.stateTf_);
  } else {
    // Drag mode
    this.state_ = svgpan.SvgPan.State.DRAG;
    this.stateTarget_ = evt.target;
    this.stateTf_ = g.getCTM().inverse();
    this.stateOrigin_ = this.getEventPoint_(evt).matrixTransform(this.stateTf_);
  }
};


/**
 * Handle mouse button release event.
 * @param {!goog.events.Event} evt The event.
 * @private
 */
svgpan.SvgPan.prototype.handleMouseUp_ = function(evt) {
  if (this.state_ != svgpan.SvgPan.State.NONE) {
    this.endPanOrDrag();
  }
};


/**
 * Ends pan/drag mode.
 */
svgpan.SvgPan.prototype.endPanOrDrag = function() {
  if (this.state_ != svgpan.SvgPan.State.NONE) {
    this.state_ = svgpan.SvgPan.State.NONE;
  }
};


/**
 * Handle mouse clicks.
 * @param {!goog.events.Event} evt The event.
 * @private
 */
svgpan.SvgPan.prototype.handleMouseClick_ = function(evt) {
  // We only set cancelNextClick_ after panning occurred, and use it to prevent
  // the default action that would otherwise take place when clicking on the
  // element (for instance, navigation on clickable links, but also any click
  // handler that may be set on an SVG element, in the case of active SVG
  // content)
  if (this.cancelNextClick_) {
    // Cancel potential click handler on active SVG content.
    evt.stopPropagation();
    // Cancel navigation when panning on clickable links.
    evt.preventDefault();
  }
  this.cancelNextClick_ = false;
};


/**
 * Returns the current state.
 * @return {!svgpan.SvgPan.State}
 */
svgpan.SvgPan.prototype.getState = function() {
  return this.state_;
};