source: trunk/svg-pan-zoom-klask.js @ 413

Last change on this file since 413 was 413, checked in by g7moreau, 4 years ago
  • Add a JS file but not seem to work. Need more effort on that part !
File size: 60.7 KB
Line 
1// svg-pan-zoom v3.5.2
2// https://github.com/ariutta/svg-pan-zoom
3// wget http://ariutta.github.io/svg-pan-zoom/dist/svg-pan-zoom.js
4(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
5var svgPanZoom = require('./svg-pan-zoom.js');
6
7// UMD module definition
8(function(window, document){
9  // AMD
10  if (typeof define === 'function' && define.amd) {
11    define('svg-pan-zoom', function () {
12      return svgPanZoom;
13    });
14  // CMD
15  } else if (typeof module !== 'undefined' && module.exports) {
16    module.exports = svgPanZoom;
17
18    // Browser
19    // Keep exporting globally as module.exports is available because of browserify
20    window.svgPanZoom = svgPanZoom;
21  }
22})(window, document)
23
24},{"./svg-pan-zoom.js":4}],2:[function(require,module,exports){
25var SvgUtils = require('./svg-utilities');
26
27module.exports = {
28  enable: function(instance) {
29    // Select (and create if necessary) defs
30    var defs = instance.svg.querySelector('defs')
31    if (!defs) {
32      defs = document.createElementNS(SvgUtils.svgNS, 'defs')
33      instance.svg.appendChild(defs)
34    }
35
36    // Check for style element, and create it if it doesn't exist
37    var styleEl = defs.querySelector('style#svg-pan-zoom-controls-styles');
38    if (!styleEl) {
39      var style = document.createElementNS(SvgUtils.svgNS, 'style')
40      style.setAttribute('id', 'svg-pan-zoom-controls-styles')
41      style.setAttribute('type', 'text/css')
42      style.textContent = '.svg-pan-zoom-control { cursor: pointer; fill: black; fill-opacity: 0.333; } .svg-pan-zoom-control:hover { fill-opacity: 0.8; } .svg-pan-zoom-control-background { fill: white; fill-opacity: 0.5; } .svg-pan-zoom-control-background { fill-opacity: 0.8; }'
43      defs.appendChild(style)
44    }
45
46    // Zoom Group
47    var zoomGroup = document.createElementNS(SvgUtils.svgNS, 'g');
48    zoomGroup.setAttribute('id', 'svg-pan-zoom-controls');
49    zoomGroup.setAttribute('transform', 'translate(' + ( instance.width - 70 ) + ' ' + ( instance.height - 76 ) + ') scale(0.75)');
50    zoomGroup.setAttribute('class', 'svg-pan-zoom-control');
51
52    // Control elements
53    zoomGroup.appendChild(this._createZoomIn(instance))
54    zoomGroup.appendChild(this._createZoomReset(instance))
55    zoomGroup.appendChild(this._createZoomOut(instance))
56
57    // Finally append created element
58    instance.svg.appendChild(zoomGroup)
59
60    // Cache control instance
61    instance.controlIcons = zoomGroup
62  }
63
64, _createZoomIn: function(instance) {
65    var zoomIn = document.createElementNS(SvgUtils.svgNS, 'g');
66    zoomIn.setAttribute('id', 'svg-pan-zoom-zoom-in');
67    zoomIn.setAttribute('transform', 'translate(30.5 5) scale(0.015)');
68    zoomIn.setAttribute('class', 'svg-pan-zoom-control');
69    zoomIn.addEventListener('click', function() {instance.getPublicInstance().zoomIn()}, false)
70    zoomIn.addEventListener('touchstart', function() {instance.getPublicInstance().zoomIn()}, false)
71
72    var zoomInBackground = document.createElementNS(SvgUtils.svgNS, 'rect'); // TODO change these background space fillers to rounded rectangles so they look prettier
73    zoomInBackground.setAttribute('x', '0');
74    zoomInBackground.setAttribute('y', '0');
75    zoomInBackground.setAttribute('width', '1500'); // larger than expected because the whole group is transformed to scale down
76    zoomInBackground.setAttribute('height', '1400');
77    zoomInBackground.setAttribute('class', 'svg-pan-zoom-control-background');
78    zoomIn.appendChild(zoomInBackground);
79
80    var zoomInShape = document.createElementNS(SvgUtils.svgNS, 'path');
81    zoomInShape.setAttribute('d', 'M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z');
82    zoomInShape.setAttribute('class', 'svg-pan-zoom-control-element');
83    zoomIn.appendChild(zoomInShape);
84
85    return zoomIn
86  }
87
88, _createZoomReset: function(instance){
89    // reset
90    var resetPanZoomControl = document.createElementNS(SvgUtils.svgNS, 'g');
91    resetPanZoomControl.setAttribute('id', 'svg-pan-zoom-reset-pan-zoom');
92    resetPanZoomControl.setAttribute('transform', 'translate(5 35) scale(0.4)');
93    resetPanZoomControl.setAttribute('class', 'svg-pan-zoom-control');
94    resetPanZoomControl.addEventListener('click', function() {instance.getPublicInstance().reset()}, false);
95    resetPanZoomControl.addEventListener('touchstart', function() {instance.getPublicInstance().reset()}, false);
96
97    var resetPanZoomControlBackground = document.createElementNS(SvgUtils.svgNS, 'rect'); // TODO change these background space fillers to rounded rectangles so they look prettier
98    resetPanZoomControlBackground.setAttribute('x', '2');
99    resetPanZoomControlBackground.setAttribute('y', '2');
100    resetPanZoomControlBackground.setAttribute('width', '182'); // larger than expected because the whole group is transformed to scale down
101    resetPanZoomControlBackground.setAttribute('height', '58');
102    resetPanZoomControlBackground.setAttribute('class', 'svg-pan-zoom-control-background');
103    resetPanZoomControl.appendChild(resetPanZoomControlBackground);
104
105    var resetPanZoomControlShape1 = document.createElementNS(SvgUtils.svgNS, 'path');
106    resetPanZoomControlShape1.setAttribute('d', 'M33.051,20.632c-0.742-0.406-1.854-0.609-3.338-0.609h-7.969v9.281h7.769c1.543,0,2.701-0.188,3.473-0.562c1.365-0.656,2.048-1.953,2.048-3.891C35.032,22.757,34.372,21.351,33.051,20.632z');
107    resetPanZoomControlShape1.setAttribute('class', 'svg-pan-zoom-control-element');
108    resetPanZoomControl.appendChild(resetPanZoomControlShape1);
109
110    var resetPanZoomControlShape2 = document.createElementNS(SvgUtils.svgNS, 'path');
111    resetPanZoomControlShape2.setAttribute('d', 'M170.231,0.5H15.847C7.102,0.5,0.5,5.708,0.5,11.84v38.861C0.5,56.833,7.102,61.5,15.847,61.5h154.384c8.745,0,15.269-4.667,15.269-10.798V11.84C185.5,5.708,178.976,0.5,170.231,0.5z M42.837,48.569h-7.969c-0.219-0.766-0.375-1.383-0.469-1.852c-0.188-0.969-0.289-1.961-0.305-2.977l-0.047-3.211c-0.03-2.203-0.41-3.672-1.142-4.406c-0.732-0.734-2.103-1.102-4.113-1.102h-7.05v13.547h-7.055V14.022h16.524c2.361,0.047,4.178,0.344,5.45,0.891c1.272,0.547,2.351,1.352,3.234,2.414c0.731,0.875,1.31,1.844,1.737,2.906s0.64,2.273,0.64,3.633c0,1.641-0.414,3.254-1.242,4.84s-2.195,2.707-4.102,3.363c1.594,0.641,2.723,1.551,3.387,2.73s0.996,2.98,0.996,5.402v2.32c0,1.578,0.063,2.648,0.19,3.211c0.19,0.891,0.635,1.547,1.333,1.969V48.569z M75.579,48.569h-26.18V14.022h25.336v6.117H56.454v7.336h16.781v6H56.454v8.883h19.125V48.569z M104.497,46.331c-2.44,2.086-5.887,3.129-10.34,3.129c-4.548,0-8.125-1.027-10.731-3.082s-3.909-4.879-3.909-8.473h6.891c0.224,1.578,0.662,2.758,1.316,3.539c1.196,1.422,3.246,2.133,6.15,2.133c1.739,0,3.151-0.188,4.236-0.562c2.058-0.719,3.087-2.055,3.087-4.008c0-1.141-0.504-2.023-1.512-2.648c-1.008-0.609-2.607-1.148-4.796-1.617l-3.74-0.82c-3.676-0.812-6.201-1.695-7.576-2.648c-2.328-1.594-3.492-4.086-3.492-7.477c0-3.094,1.139-5.664,3.417-7.711s5.623-3.07,10.036-3.07c3.685,0,6.829,0.965,9.431,2.895c2.602,1.93,3.966,4.73,4.093,8.402h-6.938c-0.128-2.078-1.057-3.555-2.787-4.43c-1.154-0.578-2.587-0.867-4.301-0.867c-1.907,0-3.428,0.375-4.565,1.125c-1.138,0.75-1.706,1.797-1.706,3.141c0,1.234,0.561,2.156,1.682,2.766c0.721,0.406,2.25,0.883,4.589,1.43l6.063,1.43c2.657,0.625,4.648,1.461,5.975,2.508c2.059,1.625,3.089,3.977,3.089,7.055C108.157,41.624,106.937,44.245,104.497,46.331z M139.61,48.569h-26.18V14.022h25.336v6.117h-18.281v7.336h16.781v6h-16.781v8.883h19.125V48.569z M170.337,20.14h-10.336v28.43h-7.266V20.14h-10.383v-6.117h27.984V20.14z');
112    resetPanZoomControlShape2.setAttribute('class', 'svg-pan-zoom-control-element');
113    resetPanZoomControl.appendChild(resetPanZoomControlShape2);
114
115    return resetPanZoomControl
116  }
117
118, _createZoomOut: function(instance){
119    // zoom out
120    var zoomOut = document.createElementNS(SvgUtils.svgNS, 'g');
121    zoomOut.setAttribute('id', 'svg-pan-zoom-zoom-out');
122    zoomOut.setAttribute('transform', 'translate(30.5 70) scale(0.015)');
123    zoomOut.setAttribute('class', 'svg-pan-zoom-control');
124    zoomOut.addEventListener('click', function() {instance.getPublicInstance().zoomOut()}, false);
125    zoomOut.addEventListener('touchstart', function() {instance.getPublicInstance().zoomOut()}, false);
126
127    var zoomOutBackground = document.createElementNS(SvgUtils.svgNS, 'rect'); // TODO change these background space fillers to rounded rectangles so they look prettier
128    zoomOutBackground.setAttribute('x', '0');
129    zoomOutBackground.setAttribute('y', '0');
130    zoomOutBackground.setAttribute('width', '1500'); // larger than expected because the whole group is transformed to scale down
131    zoomOutBackground.setAttribute('height', '1400');
132    zoomOutBackground.setAttribute('class', 'svg-pan-zoom-control-background');
133    zoomOut.appendChild(zoomOutBackground);
134
135    var zoomOutShape = document.createElementNS(SvgUtils.svgNS, 'path');
136    zoomOutShape.setAttribute('d', 'M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z');
137    zoomOutShape.setAttribute('class', 'svg-pan-zoom-control-element');
138    zoomOut.appendChild(zoomOutShape);
139
140    return zoomOut
141  }
142
143, disable: function(instance) {
144    if (instance.controlIcons) {
145      instance.controlIcons.parentNode.removeChild(instance.controlIcons)
146      instance.controlIcons = null
147    }
148  }
149}
150
151},{"./svg-utilities":5}],3:[function(require,module,exports){
152var SvgUtils = require('./svg-utilities')
153  , Utils = require('./utilities')
154  ;
155
156var ShadowViewport = function(viewport, options){
157  this.init(viewport, options)
158}
159
160/**
161 * Initialization
162 *
163 * @param  {SVGElement} viewport
164 * @param  {Object} options
165 */
166ShadowViewport.prototype.init = function(viewport, options) {
167  // DOM Elements
168  this.viewport = viewport
169  this.options = options
170
171  // State cache
172  this.originalState = {zoom: 1, x: 0, y: 0}
173  this.activeState = {zoom: 1, x: 0, y: 0}
174
175  this.updateCTMCached = Utils.proxy(this.updateCTM, this)
176
177  // Create a custom requestAnimationFrame taking in account refreshRate
178  this.requestAnimationFrame = Utils.createRequestAnimationFrame(this.options.refreshRate)
179
180  // ViewBox
181  this.viewBox = {x: 0, y: 0, width: 0, height: 0}
182  this.cacheViewBox()
183
184  // Process CTM
185  var newCTM = this.processCTM()
186
187  // Update viewport CTM and cache zoom and pan
188  this.setCTM(newCTM)
189
190  // Update CTM in this frame
191  this.updateCTM()
192}
193
194/**
195 * Cache initial viewBox value
196 * If no viewBox is defined, then use viewport size/position instead for viewBox values
197 */
198ShadowViewport.prototype.cacheViewBox = function() {
199  var svgViewBox = this.options.svg.getAttribute('viewBox')
200
201  if (svgViewBox) {
202    var viewBoxValues = svgViewBox.split(/[\s\,]/).filter(function(v){return v}).map(parseFloat)
203
204    // Cache viewbox x and y offset
205    this.viewBox.x = viewBoxValues[0]
206    this.viewBox.y = viewBoxValues[1]
207    this.viewBox.width = viewBoxValues[2]
208    this.viewBox.height = viewBoxValues[3]
209
210    var zoom = Math.min(this.options.width / this.viewBox.width, this.options.height / this.viewBox.height)
211
212    // Update active state
213    this.activeState.zoom = zoom
214    this.activeState.x = (this.options.width - this.viewBox.width * zoom) / 2
215    this.activeState.y = (this.options.height - this.viewBox.height * zoom) / 2
216
217    // Force updating CTM
218    this.updateCTMOnNextFrame()
219
220    this.options.svg.removeAttribute('viewBox')
221  } else {
222    this.simpleViewBoxCache()
223  }
224}
225
226/**
227 * Recalculate viewport sizes and update viewBox cache
228 */
229ShadowViewport.prototype.simpleViewBoxCache = function() {
230  var bBox = this.viewport.getBBox()
231
232  this.viewBox.x = bBox.x
233  this.viewBox.y = bBox.y
234  this.viewBox.width = bBox.width
235  this.viewBox.height = bBox.height
236}
237
238/**
239 * Returns a viewbox object. Safe to alter
240 *
241 * @return {Object} viewbox object
242 */
243ShadowViewport.prototype.getViewBox = function() {
244  return Utils.extend({}, this.viewBox)
245}
246
247/**
248 * Get initial zoom and pan values. Save them into originalState
249 * Parses viewBox attribute to alter initial sizes
250 *
251 * @return {CTM} CTM object based on options
252 */
253ShadowViewport.prototype.processCTM = function() {
254  var newCTM = this.getCTM()
255
256  if (this.options.fit || this.options.contain) {
257    var newScale;
258    if (this.options.fit) {
259      newScale = Math.min(this.options.width/this.viewBox.width, this.options.height/this.viewBox.height);
260    } else {
261      newScale = Math.max(this.options.width/this.viewBox.width, this.options.height/this.viewBox.height);
262    }
263
264    newCTM.a = newScale; //x-scale
265    newCTM.d = newScale; //y-scale
266    newCTM.e = -this.viewBox.x * newScale; //x-transform
267    newCTM.f = -this.viewBox.y * newScale; //y-transform
268  }
269
270  if (this.options.center) {
271    var offsetX = (this.options.width - (this.viewBox.width + this.viewBox.x * 2) * newCTM.a) * 0.5
272      , offsetY = (this.options.height - (this.viewBox.height + this.viewBox.y * 2) * newCTM.a) * 0.5
273
274    newCTM.e = offsetX
275    newCTM.f = offsetY
276  }
277
278  // Cache initial values. Based on activeState and fix+center opitons
279  this.originalState.zoom = newCTM.a
280  this.originalState.x = newCTM.e
281  this.originalState.y = newCTM.f
282
283  return newCTM
284}
285
286/**
287 * Return originalState object. Safe to alter
288 *
289 * @return {Object}
290 */
291ShadowViewport.prototype.getOriginalState = function() {
292  return Utils.extend({}, this.originalState)
293}
294
295/**
296 * Return actualState object. Safe to alter
297 *
298 * @return {Object}
299 */
300ShadowViewport.prototype.getState = function() {
301  return Utils.extend({}, this.activeState)
302}
303
304/**
305 * Get zoom scale
306 *
307 * @return {Float} zoom scale
308 */
309ShadowViewport.prototype.getZoom = function() {
310  return this.activeState.zoom
311}
312
313/**
314 * Get zoom scale for pubilc usage
315 *
316 * @return {Float} zoom scale
317 */
318ShadowViewport.prototype.getRelativeZoom = function() {
319  return this.activeState.zoom / this.originalState.zoom
320}
321
322/**
323 * Compute zoom scale for pubilc usage
324 *
325 * @return {Float} zoom scale
326 */
327ShadowViewport.prototype.computeRelativeZoom = function(scale) {
328  return scale / this.originalState.zoom
329}
330
331/**
332 * Get pan
333 *
334 * @return {Object}
335 */
336ShadowViewport.prototype.getPan = function() {
337  return {x: this.activeState.x, y: this.activeState.y}
338}
339
340/**
341 * Return cached viewport CTM value that can be safely modified
342 *
343 * @return {SVGMatrix}
344 */
345ShadowViewport.prototype.getCTM = function() {
346  var safeCTM = this.options.svg.createSVGMatrix()
347
348  // Copy values manually as in FF they are not itterable
349  safeCTM.a = this.activeState.zoom
350  safeCTM.b = 0
351  safeCTM.c = 0
352  safeCTM.d = this.activeState.zoom
353  safeCTM.e = this.activeState.x
354  safeCTM.f = this.activeState.y
355
356  return safeCTM
357}
358
359/**
360 * Set a new CTM
361 *
362 * @param {SVGMatrix} newCTM
363 */
364ShadowViewport.prototype.setCTM = function(newCTM) {
365  var willZoom = this.isZoomDifferent(newCTM)
366    , willPan = this.isPanDifferent(newCTM)
367
368  if (willZoom || willPan) {
369    // Before zoom
370    if (willZoom) {
371      // If returns false then cancel zooming
372      if (this.options.beforeZoom(this.getRelativeZoom(), this.computeRelativeZoom(newCTM.a)) === false) {
373        newCTM.a = newCTM.d = this.activeState.zoom
374        willZoom = false
375      } else {
376        this.updateCache(newCTM);
377        this.options.onZoom(this.getRelativeZoom())
378      }
379    }
380
381    // Before pan
382    if (willPan) {
383      var preventPan = this.options.beforePan(this.getPan(), {x: newCTM.e, y: newCTM.f})
384          // If prevent pan is an object
385        , preventPanX = false
386        , preventPanY = false
387
388      // If prevent pan is Boolean false
389      if (preventPan === false) {
390        // Set x and y same as before
391        newCTM.e = this.getPan().x
392        newCTM.f = this.getPan().y
393
394        preventPanX = preventPanY = true
395      } else if (Utils.isObject(preventPan)) {
396        // Check for X axes attribute
397        if (preventPan.x === false) {
398          // Prevent panning on x axes
399          newCTM.e = this.getPan().x
400          preventPanX = true
401        } else if (Utils.isNumber(preventPan.x)) {
402          // Set a custom pan value
403          newCTM.e = preventPan.x
404        }
405
406        // Check for Y axes attribute
407        if (preventPan.y === false) {
408          // Prevent panning on x axes
409          newCTM.f = this.getPan().y
410          preventPanY = true
411        } else if (Utils.isNumber(preventPan.y)) {
412          // Set a custom pan value
413          newCTM.f = preventPan.y
414        }
415      }
416
417      // Update willPan flag
418      // Check if newCTM is still different
419      if ((preventPanX && preventPanY) || !this.isPanDifferent(newCTM)) {
420        willPan = false
421      } else {
422        this.updateCache(newCTM);
423        this.options.onPan(this.getPan());
424      }
425    }
426
427    // Check again if should zoom or pan
428    if (willZoom || willPan) {
429      this.updateCTMOnNextFrame()
430    }
431  }
432}
433
434ShadowViewport.prototype.isZoomDifferent = function(newCTM) {
435  return this.activeState.zoom !== newCTM.a
436}
437
438ShadowViewport.prototype.isPanDifferent = function(newCTM) {
439  return this.activeState.x !== newCTM.e || this.activeState.y !== newCTM.f
440}
441
442
443/**
444 * Update cached CTM and active state
445 *
446 * @param {SVGMatrix} newCTM
447 */
448ShadowViewport.prototype.updateCache = function(newCTM) {
449  this.activeState.zoom = newCTM.a
450  this.activeState.x = newCTM.e
451  this.activeState.y = newCTM.f
452}
453
454ShadowViewport.prototype.pendingUpdate = false
455
456/**
457 * Place a request to update CTM on next Frame
458 */
459ShadowViewport.prototype.updateCTMOnNextFrame = function() {
460  if (!this.pendingUpdate) {
461    // Lock
462    this.pendingUpdate = true
463
464    // Throttle next update
465    this.requestAnimationFrame.call(window, this.updateCTMCached)
466  }
467}
468
469/**
470 * Update viewport CTM with cached CTM
471 */
472ShadowViewport.prototype.updateCTM = function() {
473  var ctm = this.getCTM()
474
475  // Updates SVG element
476  SvgUtils.setCTM(this.viewport, ctm, this.defs)
477
478  // Free the lock
479  this.pendingUpdate = false
480
481  // Notify about the update
482  if(this.options.onUpdatedCTM) {
483    this.options.onUpdatedCTM(ctm)
484  }
485}
486
487module.exports = function(viewport, options){
488  return new ShadowViewport(viewport, options)
489}
490
491},{"./svg-utilities":5,"./utilities":7}],4:[function(require,module,exports){
492var Wheel = require('./uniwheel')
493, ControlIcons = require('./control-icons')
494, Utils = require('./utilities')
495, SvgUtils = require('./svg-utilities')
496, ShadowViewport = require('./shadow-viewport')
497
498var SvgPanZoom = function(svg, options) {
499  this.init(svg, options)
500}
501
502var optionsDefaults = {
503  viewportSelector: '.svg-pan-zoom_viewport' // Viewport selector. Can be querySelector string or SVGElement
504, panEnabled: true // enable or disable panning (default enabled)
505, controlIconsEnabled: false // insert icons to give user an option in addition to mouse events to control pan/zoom (default disabled)
506, zoomEnabled: true // enable or disable zooming (default enabled)
507, dblClickZoomEnabled: true // enable or disable zooming by double clicking (default enabled)
508, mouseWheelZoomEnabled: true // enable or disable zooming by mouse wheel (default enabled)
509, preventMouseEventsDefault: true // enable or disable preventDefault for mouse events
510, zoomScaleSensitivity: 0.1 // Zoom sensitivity
511, minZoom: 0.5 // Minimum Zoom level
512, maxZoom: 10 // Maximum Zoom level
513, fit: true // enable or disable viewport fit in SVG (default true)
514, contain: false // enable or disable viewport contain the svg (default false)
515, center: true // enable or disable viewport centering in SVG (default true)
516, refreshRate: 'auto' // Maximum number of frames per second (altering SVG's viewport)
517, beforeZoom: null
518, onZoom: null
519, beforePan: null
520, onPan: null
521, customEventsHandler: null
522, eventsListenerElement: null
523, onUpdatedCTM: null
524}
525
526SvgPanZoom.prototype.init = function(svg, options) {
527  var that = this
528
529  this.svg = svg
530  this.defs = svg.querySelector('defs')
531
532  // Add default attributes to SVG
533  SvgUtils.setupSvgAttributes(this.svg)
534
535  // Set options
536  this.options = Utils.extend(Utils.extend({}, optionsDefaults), options)
537
538  // Set default state
539  this.state = 'none'
540
541  // Get dimensions
542  var boundingClientRectNormalized = SvgUtils.getBoundingClientRectNormalized(svg)
543  this.width = boundingClientRectNormalized.width
544  this.height = boundingClientRectNormalized.height
545
546  // Init shadow viewport
547  this.viewport = ShadowViewport(SvgUtils.getOrCreateViewport(this.svg, this.options.viewportSelector), {
548    svg: this.svg
549  , width: this.width
550  , height: this.height
551  , fit: this.options.fit
552  , contain: this.options.contain
553  , center: this.options.center
554  , refreshRate: this.options.refreshRate
555  // Put callbacks into functions as they can change through time
556  , beforeZoom: function(oldScale, newScale) {
557      if (that.viewport && that.options.beforeZoom) {return that.options.beforeZoom(oldScale, newScale)}
558    }
559  , onZoom: function(scale) {
560      if (that.viewport && that.options.onZoom) {return that.options.onZoom(scale)}
561    }
562  , beforePan: function(oldPoint, newPoint) {
563      if (that.viewport && that.options.beforePan) {return that.options.beforePan(oldPoint, newPoint)}
564    }
565  , onPan: function(point) {
566      if (that.viewport && that.options.onPan) {return that.options.onPan(point)}
567    }
568  , onUpdatedCTM: function(ctm) {
569      if (that.viewport && that.options.onUpdatedCTM) {return that.options.onUpdatedCTM(ctm)}
570    }
571  })
572
573  // Wrap callbacks into public API context
574  var publicInstance = this.getPublicInstance()
575  publicInstance.setBeforeZoom(this.options.beforeZoom)
576  publicInstance.setOnZoom(this.options.onZoom)
577  publicInstance.setBeforePan(this.options.beforePan)
578  publicInstance.setOnPan(this.options.onPan)
579  publicInstance.setOnUpdatedCTM(this.options.onUpdatedCTM)
580
581  if (this.options.controlIconsEnabled) {
582    ControlIcons.enable(this)
583  }
584
585  // Init events handlers
586  this.lastMouseWheelEventTime = Date.now()
587  this.setupHandlers()
588}
589
590/**
591 * Register event handlers
592 */
593SvgPanZoom.prototype.setupHandlers = function() {
594  var that = this
595    , prevEvt = null // use for touchstart event to detect double tap
596    ;
597
598  this.eventListeners = {
599    // Mouse down group
600    mousedown: function(evt) {
601      var result = that.handleMouseDown(evt, prevEvt);
602      prevEvt = evt
603      return result;
604    }
605  , touchstart: function(evt) {
606      var result = that.handleMouseDown(evt, prevEvt);
607      prevEvt = evt
608      return result;
609    }
610
611    // Mouse up group
612  , mouseup: function(evt) {
613      return that.handleMouseUp(evt);
614    }
615  , touchend: function(evt) {
616      return that.handleMouseUp(evt);
617    }
618
619    // Mouse move group
620  , mousemove: function(evt) {
621      return that.handleMouseMove(evt);
622    }
623  , touchmove: function(evt) {
624      return that.handleMouseMove(evt);
625    }
626
627    // Mouse leave group
628  , mouseleave: function(evt) {
629      return that.handleMouseUp(evt);
630    }
631  , touchleave: function(evt) {
632      return that.handleMouseUp(evt);
633    }
634  , touchcancel: function(evt) {
635      return that.handleMouseUp(evt);
636    }
637  }
638
639  // Init custom events handler if available
640  if (this.options.customEventsHandler != null) { // jshint ignore:line
641    this.options.customEventsHandler.init({
642      svgElement: this.svg
643    , eventsListenerElement: this.options.eventsListenerElement
644    , instance: this.getPublicInstance()
645    })
646
647    // Custom event handler may halt builtin listeners
648    var haltEventListeners = this.options.customEventsHandler.haltEventListeners
649    if (haltEventListeners && haltEventListeners.length) {
650      for (var i = haltEventListeners.length - 1; i >= 0; i--) {
651        if (this.eventListeners.hasOwnProperty(haltEventListeners[i])) {
652          delete this.eventListeners[haltEventListeners[i]]
653        }
654      }
655    }
656  }
657
658  // Bind eventListeners
659  for (var event in this.eventListeners) {
660    // Attach event to eventsListenerElement or SVG if not available
661    (this.options.eventsListenerElement || this.svg)
662      .addEventListener(event, this.eventListeners[event], false)
663  }
664
665  // Zoom using mouse wheel
666  if (this.options.mouseWheelZoomEnabled) {
667    this.options.mouseWheelZoomEnabled = false // set to false as enable will set it back to true
668    this.enableMouseWheelZoom()
669  }
670}
671
672/**
673 * Enable ability to zoom using mouse wheel
674 */
675SvgPanZoom.prototype.enableMouseWheelZoom = function() {
676  if (!this.options.mouseWheelZoomEnabled) {
677    var that = this
678
679    // Mouse wheel listener
680    this.wheelListener = function(evt) {
681      return that.handleMouseWheel(evt);
682    }
683
684    // Bind wheelListener
685    Wheel.on(this.options.eventsListenerElement || this.svg, this.wheelListener, false)
686
687    this.options.mouseWheelZoomEnabled = true
688  }
689}
690
691/**
692 * Disable ability to zoom using mouse wheel
693 */
694SvgPanZoom.prototype.disableMouseWheelZoom = function() {
695  if (this.options.mouseWheelZoomEnabled) {
696    Wheel.off(this.options.eventsListenerElement || this.svg, this.wheelListener, false)
697    this.options.mouseWheelZoomEnabled = false
698  }
699}
700
701/**
702 * Handle mouse wheel event
703 *
704 * @param  {Event} evt
705 */
706SvgPanZoom.prototype.handleMouseWheel = function(evt) {
707  if (!this.options.zoomEnabled || this.state !== 'none') {
708    return;
709  }
710
711  if (this.options.preventMouseEventsDefault){
712    if (evt.preventDefault) {
713      evt.preventDefault();
714    } else {
715      evt.returnValue = false;
716    }
717  }
718
719  // Default delta in case that deltaY is not available
720  var delta = evt.deltaY || 1
721    , timeDelta = Date.now() - this.lastMouseWheelEventTime
722    , divider = 3 + Math.max(0, 30 - timeDelta)
723
724  // Update cache
725  this.lastMouseWheelEventTime = Date.now()
726
727  // Make empirical adjustments for browsers that give deltaY in pixels (deltaMode=0)
728  if ('deltaMode' in evt && evt.deltaMode === 0 && evt.wheelDelta) {
729    delta = evt.deltaY === 0 ? 0 :  Math.abs(evt.wheelDelta) / evt.deltaY
730  }
731
732  delta = -0.3 < delta && delta < 0.3 ? delta : (delta > 0 ? 1 : -1) * Math.log(Math.abs(delta) + 10) / divider
733
734  var inversedScreenCTM = this.svg.getScreenCTM().inverse()
735    , relativeMousePoint = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(inversedScreenCTM)
736    , zoom = Math.pow(1 + this.options.zoomScaleSensitivity, (-1) * delta); // multiplying by neg. 1 so as to make zoom in/out behavior match Google maps behavior
737
738  this.zoomAtPoint(zoom, relativeMousePoint)
739}
740
741/**
742 * Zoom in at a SVG point
743 *
744 * @param  {SVGPoint} point
745 * @param  {Float} zoomScale    Number representing how much to zoom
746 * @param  {Boolean} zoomAbsolute Default false. If true, zoomScale is treated as an absolute value.
747 *                                Otherwise, zoomScale is treated as a multiplied (e.g. 1.10 would zoom in 10%)
748 */
749SvgPanZoom.prototype.zoomAtPoint = function(zoomScale, point, zoomAbsolute) {
750  var originalState = this.viewport.getOriginalState()
751
752  if (!zoomAbsolute) {
753    // Fit zoomScale in set bounds
754    if (this.getZoom() * zoomScale < this.options.minZoom * originalState.zoom) {
755      zoomScale = (this.options.minZoom * originalState.zoom) / this.getZoom()
756    } else if (this.getZoom() * zoomScale > this.options.maxZoom * originalState.zoom) {
757      zoomScale = (this.options.maxZoom * originalState.zoom) / this.getZoom()
758    }
759  } else {
760    // Fit zoomScale in set bounds
761    zoomScale = Math.max(this.options.minZoom * originalState.zoom, Math.min(this.options.maxZoom * originalState.zoom, zoomScale))
762    // Find relative scale to achieve desired scale
763    zoomScale = zoomScale/this.getZoom()
764  }
765
766  var oldCTM = this.viewport.getCTM()
767    , relativePoint = point.matrixTransform(oldCTM.inverse())
768    , modifier = this.svg.createSVGMatrix().translate(relativePoint.x, relativePoint.y).scale(zoomScale).translate(-relativePoint.x, -relativePoint.y)
769    , newCTM = oldCTM.multiply(modifier)
770
771  if (newCTM.a !== oldCTM.a) {
772    this.viewport.setCTM(newCTM)
773  }
774}
775
776/**
777 * Zoom at center point
778 *
779 * @param  {Float} scale
780 * @param  {Boolean} absolute Marks zoom scale as relative or absolute
781 */
782SvgPanZoom.prototype.zoom = function(scale, absolute) {
783  this.zoomAtPoint(scale, SvgUtils.getSvgCenterPoint(this.svg, this.width, this.height), absolute)
784}
785
786/**
787 * Zoom used by public instance
788 *
789 * @param  {Float} scale
790 * @param  {Boolean} absolute Marks zoom scale as relative or absolute
791 */
792SvgPanZoom.prototype.publicZoom = function(scale, absolute) {
793  if (absolute) {
794    scale = this.computeFromRelativeZoom(scale)
795  }
796
797  this.zoom(scale, absolute)
798}
799
800/**
801 * Zoom at point used by public instance
802 *
803 * @param  {Float} scale
804 * @param  {SVGPoint|Object} point    An object that has x and y attributes
805 * @param  {Boolean} absolute Marks zoom scale as relative or absolute
806 */
807SvgPanZoom.prototype.publicZoomAtPoint = function(scale, point, absolute) {
808  if (absolute) {
809    // Transform zoom into a relative value
810    scale = this.computeFromRelativeZoom(scale)
811  }
812
813  // If not a SVGPoint but has x and y then create a SVGPoint
814  if (Utils.getType(point) !== 'SVGPoint') {
815    if('x' in point && 'y' in point) {
816      point = SvgUtils.createSVGPoint(this.svg, point.x, point.y)
817    } else {
818      throw new Error('Given point is invalid')
819    }
820  }
821
822  this.zoomAtPoint(scale, point, absolute)
823}
824
825/**
826 * Get zoom scale
827 *
828 * @return {Float} zoom scale
829 */
830SvgPanZoom.prototype.getZoom = function() {
831  return this.viewport.getZoom()
832}
833
834/**
835 * Get zoom scale for public usage
836 *
837 * @return {Float} zoom scale
838 */
839SvgPanZoom.prototype.getRelativeZoom = function() {
840  return this.viewport.getRelativeZoom()
841}
842
843/**
844 * Compute actual zoom from public zoom
845 *
846 * @param  {Float} zoom
847 * @return {Float} zoom scale
848 */
849SvgPanZoom.prototype.computeFromRelativeZoom = function(zoom) {
850  return zoom * this.viewport.getOriginalState().zoom
851}
852
853/**
854 * Set zoom to initial state
855 */
856SvgPanZoom.prototype.resetZoom = function() {
857  var originalState = this.viewport.getOriginalState()
858
859  this.zoom(originalState.zoom, true);
860}
861
862/**
863 * Set pan to initial state
864 */
865SvgPanZoom.prototype.resetPan = function() {
866  this.pan(this.viewport.getOriginalState());
867}
868
869/**
870 * Set pan and zoom to initial state
871 */
872SvgPanZoom.prototype.reset = function() {
873  this.resetZoom()
874  this.resetPan()
875}
876
877/**
878 * Handle double click event
879 * See handleMouseDown() for alternate detection method
880 *
881 * @param {Event} evt
882 */
883SvgPanZoom.prototype.handleDblClick = function(evt) {
884  if (this.options.preventMouseEventsDefault) {
885    if (evt.preventDefault) {
886      evt.preventDefault()
887    } else {
888      evt.returnValue = false
889    }
890  }
891
892  // Check if target was a control button
893  if (this.options.controlIconsEnabled) {
894    var targetClass = evt.target.getAttribute('class') || ''
895    if (targetClass.indexOf('svg-pan-zoom-control') > -1) {
896      return false
897    }
898  }
899
900  var zoomFactor
901
902  if (evt.shiftKey) {
903    zoomFactor = 1/((1 + this.options.zoomScaleSensitivity) * 2) // zoom out when shift key pressed
904  } else {
905    zoomFactor = (1 + this.options.zoomScaleSensitivity) * 2
906  }
907
908  var point = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(this.svg.getScreenCTM().inverse())
909  this.zoomAtPoint(zoomFactor, point)
910}
911
912/**
913 * Handle click event
914 *
915 * @param {Event} evt
916 */
917SvgPanZoom.prototype.handleMouseDown = function(evt, prevEvt) {
918  if (this.options.preventMouseEventsDefault) {
919    if (evt.preventDefault) {
920      evt.preventDefault()
921    } else {
922      evt.returnValue = false
923    }
924  }
925
926  Utils.mouseAndTouchNormalize(evt, this.svg)
927
928  // Double click detection; more consistent than ondblclick
929  if (this.options.dblClickZoomEnabled && Utils.isDblClick(evt, prevEvt)){
930    this.handleDblClick(evt)
931  } else {
932    // Pan mode
933    this.state = 'pan'
934    this.firstEventCTM = this.viewport.getCTM()
935    this.stateOrigin = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(this.firstEventCTM.inverse())
936  }
937}
938
939/**
940 * Handle mouse move event
941 *
942 * @param  {Event} evt
943 */
944SvgPanZoom.prototype.handleMouseMove = function(evt) {
945  if (this.options.preventMouseEventsDefault) {
946    if (evt.preventDefault) {
947      evt.preventDefault()
948    } else {
949      evt.returnValue = false
950    }
951  }
952
953  if (this.state === 'pan' && this.options.panEnabled) {
954    // Pan mode
955    var point = SvgUtils.getEventPoint(evt, this.svg).matrixTransform(this.firstEventCTM.inverse())
956      , viewportCTM = this.firstEventCTM.translate(point.x - this.stateOrigin.x, point.y - this.stateOrigin.y)
957
958    this.viewport.setCTM(viewportCTM)
959  }
960}
961
962/**
963 * Handle mouse button release event
964 *
965 * @param {Event} evt
966 */
967SvgPanZoom.prototype.handleMouseUp = function(evt) {
968  if (this.options.preventMouseEventsDefault) {
969    if (evt.preventDefault) {
970      evt.preventDefault()
971    } else {
972      evt.returnValue = false
973    }
974  }
975
976  if (this.state === 'pan') {
977    // Quit pan mode
978    this.state = 'none'
979  }
980}
981
982/**
983 * Adjust viewport size (only) so it will fit in SVG
984 * Does not center image
985 */
986SvgPanZoom.prototype.fit = function() {
987  var viewBox = this.viewport.getViewBox()
988    , newScale = Math.min(this.width/viewBox.width, this.height/viewBox.height)
989
990  this.zoom(newScale, true)
991}
992
993/**
994 * Adjust viewport size (only) so it will contain the SVG
995 * Does not center image
996 */
997SvgPanZoom.prototype.contain = function() {
998  var viewBox = this.viewport.getViewBox()
999    , newScale = Math.max(this.width/viewBox.width, this.height/viewBox.height)
1000
1001  this.zoom(newScale, true)
1002}
1003
1004/**
1005 * Adjust viewport pan (only) so it will be centered in SVG
1006 * Does not zoom/fit/contain image
1007 */
1008SvgPanZoom.prototype.center = function() {
1009  var viewBox = this.viewport.getViewBox()
1010    , offsetX = (this.width - (viewBox.width + viewBox.x * 2) * this.getZoom()) * 0.5
1011    , offsetY = (this.height - (viewBox.height + viewBox.y * 2) * this.getZoom()) * 0.5
1012
1013  this.getPublicInstance().pan({x: offsetX, y: offsetY})
1014}
1015
1016/**
1017 * Update content cached BorderBox
1018 * Use when viewport contents change
1019 */
1020SvgPanZoom.prototype.updateBBox = function() {
1021  this.viewport.simpleViewBoxCache()
1022}
1023
1024/**
1025 * Pan to a rendered position
1026 *
1027 * @param  {Object} point {x: 0, y: 0}
1028 */
1029SvgPanZoom.prototype.pan = function(point) {
1030  var viewportCTM = this.viewport.getCTM()
1031  viewportCTM.e = point.x
1032  viewportCTM.f = point.y
1033  this.viewport.setCTM(viewportCTM)
1034}
1035
1036/**
1037 * Relatively pan the graph by a specified rendered position vector
1038 *
1039 * @param  {Object} point {x: 0, y: 0}
1040 */
1041SvgPanZoom.prototype.panBy = function(point) {
1042  var viewportCTM = this.viewport.getCTM()
1043  viewportCTM.e += point.x
1044  viewportCTM.f += point.y
1045  this.viewport.setCTM(viewportCTM)
1046}
1047
1048/**
1049 * Get pan vector
1050 *
1051 * @return {Object} {x: 0, y: 0}
1052 */
1053SvgPanZoom.prototype.getPan = function() {
1054  var state = this.viewport.getState()
1055
1056  return {x: state.x, y: state.y}
1057}
1058
1059/**
1060 * Recalculates cached svg dimensions and controls position
1061 */
1062SvgPanZoom.prototype.resize = function() {
1063  // Get dimensions
1064  var boundingClientRectNormalized = SvgUtils.getBoundingClientRectNormalized(this.svg)
1065  this.width = boundingClientRectNormalized.width
1066  this.height = boundingClientRectNormalized.height
1067
1068  // Recalculate original state
1069  var viewport = this.viewport
1070  viewport.options.width = this.width
1071  viewport.options.height = this.height
1072  viewport.processCTM()
1073
1074  // Reposition control icons by re-enabling them
1075  if (this.options.controlIconsEnabled) {
1076    this.getPublicInstance().disableControlIcons()
1077    this.getPublicInstance().enableControlIcons()
1078  }
1079}
1080
1081/**
1082 * Unbind mouse events, free callbacks and destroy public instance
1083 */
1084SvgPanZoom.prototype.destroy = function() {
1085  var that = this
1086
1087  // Free callbacks
1088  this.beforeZoom = null
1089  this.onZoom = null
1090  this.beforePan = null
1091  this.onPan = null
1092  this.onUpdatedCTM = null
1093
1094  // Destroy custom event handlers
1095  if (this.options.customEventsHandler != null) { // jshint ignore:line
1096    this.options.customEventsHandler.destroy({
1097      svgElement: this.svg
1098    , eventsListenerElement: this.options.eventsListenerElement
1099    , instance: this.getPublicInstance()
1100    })
1101  }
1102
1103  // Unbind eventListeners
1104  for (var event in this.eventListeners) {
1105    (this.options.eventsListenerElement || this.svg)
1106      .removeEventListener(event, this.eventListeners[event], false)
1107  }
1108
1109  // Unbind wheelListener
1110  this.disableMouseWheelZoom()
1111
1112  // Remove control icons
1113  this.getPublicInstance().disableControlIcons()
1114
1115  // Reset zoom and pan
1116  this.reset()
1117
1118  // Remove instance from instancesStore
1119  instancesStore = instancesStore.filter(function(instance){
1120    return instance.svg !== that.svg
1121  })
1122
1123  // Delete options and its contents
1124  delete this.options
1125
1126  // Delete viewport to make public shadow viewport functions uncallable
1127  delete this.viewport
1128
1129  // Destroy public instance and rewrite getPublicInstance
1130  delete this.publicInstance
1131  delete this.pi
1132  this.getPublicInstance = function(){
1133    return null
1134  }
1135}
1136
1137/**
1138 * Returns a public instance object
1139 *
1140 * @return {Object} Public instance object
1141 */
1142SvgPanZoom.prototype.getPublicInstance = function() {
1143  var that = this
1144
1145  // Create cache
1146  if (!this.publicInstance) {
1147    this.publicInstance = this.pi = {
1148      // Pan
1149      enablePan: function() {that.options.panEnabled = true; return that.pi}
1150    , disablePan: function() {that.options.panEnabled = false; return that.pi}
1151    , isPanEnabled: function() {return !!that.options.panEnabled}
1152    , pan: function(point) {that.pan(point); return that.pi}
1153    , panBy: function(point) {that.panBy(point); return that.pi}
1154    , getPan: function() {return that.getPan()}
1155      // Pan event
1156    , setBeforePan: function(fn) {that.options.beforePan = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
1157    , setOnPan: function(fn) {that.options.onPan = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
1158      // Zoom and Control Icons
1159    , enableZoom: function() {that.options.zoomEnabled = true; return that.pi}
1160    , disableZoom: function() {that.options.zoomEnabled = false; return that.pi}
1161    , isZoomEnabled: function() {return !!that.options.zoomEnabled}
1162    , enableControlIcons: function() {
1163        if (!that.options.controlIconsEnabled) {
1164          that.options.controlIconsEnabled = true
1165          ControlIcons.enable(that)
1166        }
1167        return that.pi
1168      }
1169    , disableControlIcons: function() {
1170        if (that.options.controlIconsEnabled) {
1171          that.options.controlIconsEnabled = false;
1172          ControlIcons.disable(that)
1173        }
1174        return that.pi
1175      }
1176    , isControlIconsEnabled: function() {return !!that.options.controlIconsEnabled}
1177      // Double click zoom
1178    , enableDblClickZoom: function() {that.options.dblClickZoomEnabled = true; return that.pi}
1179    , disableDblClickZoom: function() {that.options.dblClickZoomEnabled = false; return that.pi}
1180    , isDblClickZoomEnabled: function() {return !!that.options.dblClickZoomEnabled}
1181      // Mouse wheel zoom
1182    , enableMouseWheelZoom: function() {that.enableMouseWheelZoom(); return that.pi}
1183    , disableMouseWheelZoom: function() {that.disableMouseWheelZoom(); return that.pi}
1184    , isMouseWheelZoomEnabled: function() {return !!that.options.mouseWheelZoomEnabled}
1185      // Zoom scale and bounds
1186    , setZoomScaleSensitivity: function(scale) {that.options.zoomScaleSensitivity = scale; return that.pi}
1187    , setMinZoom: function(zoom) {that.options.minZoom = zoom; return that.pi}
1188    , setMaxZoom: function(zoom) {that.options.maxZoom = zoom; return that.pi}
1189      // Zoom event
1190    , setBeforeZoom: function(fn) {that.options.beforeZoom = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
1191    , setOnZoom: function(fn) {that.options.onZoom = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
1192      // Zooming
1193    , zoom: function(scale) {that.publicZoom(scale, true); return that.pi}
1194    , zoomBy: function(scale) {that.publicZoom(scale, false); return that.pi}
1195    , zoomAtPoint: function(scale, point) {that.publicZoomAtPoint(scale, point, true); return that.pi}
1196    , zoomAtPointBy: function(scale, point) {that.publicZoomAtPoint(scale, point, false); return that.pi}
1197    , zoomIn: function() {this.zoomBy(1 + that.options.zoomScaleSensitivity); return that.pi}
1198    , zoomOut: function() {this.zoomBy(1 / (1 + that.options.zoomScaleSensitivity)); return that.pi}
1199    , getZoom: function() {return that.getRelativeZoom()}
1200      // CTM update
1201    , setOnUpdatedCTM: function(fn) {that.options.onUpdatedCTM = fn === null ? null : Utils.proxy(fn, that.publicInstance); return that.pi}
1202      // Reset
1203    , resetZoom: function() {that.resetZoom(); return that.pi}
1204    , resetPan: function() {that.resetPan(); return that.pi}
1205    , reset: function() {that.reset(); return that.pi}
1206      // Fit, Contain and Center
1207    , fit: function() {that.fit(); return that.pi}
1208    , contain: function() {that.contain(); return that.pi}
1209    , center: function() {that.center(); return that.pi}
1210      // Size and Resize
1211    , updateBBox: function() {that.updateBBox(); return that.pi}
1212    , resize: function() {that.resize(); return that.pi}
1213    , getSizes: function() {
1214        return {
1215          width: that.width
1216        , height: that.height
1217        , realZoom: that.getZoom()
1218        , viewBox: that.viewport.getViewBox()
1219        }
1220      }
1221      // Destroy
1222    , destroy: function() {that.destroy(); return that.pi}
1223    }
1224  }
1225
1226  return this.publicInstance
1227}
1228
1229/**
1230 * Stores pairs of instances of SvgPanZoom and SVG
1231 * Each pair is represented by an object {svg: SVGSVGElement, instance: SvgPanZoom}
1232 *
1233 * @type {Array}
1234 */
1235var instancesStore = []
1236
1237var svgPanZoom = function(elementOrSelector, options){
1238  var svg = Utils.getSvg(elementOrSelector)
1239
1240  if (svg === null) {
1241    return null
1242  } else {
1243    // Look for existent instance
1244    for(var i = instancesStore.length - 1; i >= 0; i--) {
1245      if (instancesStore[i].svg === svg) {
1246        return instancesStore[i].instance.getPublicInstance()
1247      }
1248    }
1249
1250    // If instance not found - create one
1251    instancesStore.push({
1252      svg: svg
1253    , instance: new SvgPanZoom(svg, options)
1254    })
1255
1256    // Return just pushed instance
1257    return instancesStore[instancesStore.length - 1].instance.getPublicInstance()
1258  }
1259}
1260
1261module.exports = svgPanZoom;
1262
1263},{"./control-icons":2,"./shadow-viewport":3,"./svg-utilities":5,"./uniwheel":6,"./utilities":7}],5:[function(require,module,exports){
1264var Utils = require('./utilities')
1265  , _browser = 'unknown'
1266  ;
1267
1268// http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
1269if (/*@cc_on!@*/false || !!document.documentMode) { // internet explorer
1270  _browser = 'ie';
1271}
1272
1273module.exports = {
1274  svgNS:  'http://www.w3.org/2000/svg'
1275, xmlNS:  'http://www.w3.org/XML/1998/namespace'
1276, xmlnsNS:  'http://www.w3.org/2000/xmlns/'
1277, xlinkNS:  'http://www.w3.org/1999/xlink'
1278, evNS:  'http://www.w3.org/2001/xml-events'
1279
1280  /**
1281   * Get svg dimensions: width and height
1282   *
1283   * @param  {SVGSVGElement} svg
1284   * @return {Object}     {width: 0, height: 0}
1285   */
1286, getBoundingClientRectNormalized: function(svg) {
1287    if (svg.clientWidth && svg.clientHeight) {
1288      return {width: svg.clientWidth, height: svg.clientHeight}
1289    } else if (!!svg.getBoundingClientRect()) {
1290      return svg.getBoundingClientRect();
1291    } else {
1292      throw new Error('Cannot get BoundingClientRect for SVG.');
1293    }
1294  }
1295
1296  /**
1297   * Gets g element with class of "viewport" or creates it if it doesn't exist
1298   *
1299   * @param  {SVGSVGElement} svg
1300   * @return {SVGElement}     g (group) element
1301   */
1302, getOrCreateViewport: function(svg, selector) {
1303    var viewport = null
1304
1305    if (Utils.isElement(selector)) {
1306      viewport = selector
1307    } else {
1308      viewport = svg.querySelector(selector)
1309    }
1310
1311    // Check if there is just one main group in SVG
1312    if (!viewport) {
1313      var childNodes = Array.prototype.slice.call(svg.childNodes || svg.children).filter(function(el){
1314        return el.nodeName !== 'defs' && el.nodeName !== '#text'
1315      })
1316
1317      // Node name should be SVGGElement and should have no transform attribute
1318      // Groups with transform are not used as viewport because it involves parsing of all transform possibilities
1319      if (childNodes.length === 1 && childNodes[0].nodeName === 'g' && childNodes[0].getAttribute('transform') === null) {
1320        viewport = childNodes[0]
1321      }
1322    }
1323
1324    // If no favorable group element exists then create one
1325    if (!viewport) {
1326      var viewportId = 'viewport-' + new Date().toISOString().replace(/\D/g, '');
1327      viewport = document.createElementNS(this.svgNS, 'g');
1328      viewport.setAttribute('id', viewportId);
1329
1330      // Internet Explorer (all versions?) can't use childNodes, but other browsers prefer (require?) using childNodes
1331      var svgChildren = svg.childNodes || svg.children;
1332      if (!!svgChildren && svgChildren.length > 0) {
1333        for (var i = svgChildren.length; i > 0; i--) {
1334          // Move everything into viewport except defs
1335          if (svgChildren[svgChildren.length - i].nodeName !== 'defs') {
1336            viewport.appendChild(svgChildren[svgChildren.length - i]);
1337          }
1338        }
1339      }
1340      svg.appendChild(viewport);
1341    }
1342
1343    // Parse class names
1344    var classNames = [];
1345    if (viewport.getAttribute('class')) {
1346      classNames = viewport.getAttribute('class').split(' ')
1347    }
1348
1349    // Set class (if not set already)
1350    if (!~classNames.indexOf('svg-pan-zoom_viewport')) {
1351      classNames.push('svg-pan-zoom_viewport')
1352      viewport.setAttribute('class', classNames.join(' '))
1353    }
1354
1355    return viewport
1356  }
1357
1358  /**
1359   * Set SVG attributes
1360   *
1361   * @param  {SVGSVGElement} svg
1362   */
1363  , setupSvgAttributes: function(svg) {
1364    // Setting default attributes
1365    svg.setAttribute('xmlns', this.svgNS);
1366    svg.setAttributeNS(this.xmlnsNS, 'xmlns:xlink', this.xlinkNS);
1367    svg.setAttributeNS(this.xmlnsNS, 'xmlns:ev', this.evNS);
1368
1369    // Needed for Internet Explorer, otherwise the viewport overflows
1370    if (svg.parentNode !== null) {
1371      var style = svg.getAttribute('style') || '';
1372      if (style.toLowerCase().indexOf('overflow') === -1) {
1373        svg.setAttribute('style', 'overflow: hidden; ' + style);
1374      }
1375    }
1376  }
1377
1378/**
1379 * How long Internet Explorer takes to finish updating its display (ms).
1380 */
1381, internetExplorerRedisplayInterval: 300
1382
1383/**
1384 * Forces the browser to redisplay all SVG elements that rely on an
1385 * element defined in a 'defs' section. It works globally, for every
1386 * available defs element on the page.
1387 * The throttling is intentionally global.
1388 *
1389 * This is only needed for IE. It is as a hack to make markers (and 'use' elements?)
1390 * visible after pan/zoom when there are multiple SVGs on the page.
1391 * See bug report: https://connect.microsoft.com/IE/feedback/details/781964/
1392 * also see svg-pan-zoom issue: https://github.com/ariutta/svg-pan-zoom/issues/62
1393 */
1394, refreshDefsGlobal: Utils.throttle(function() {
1395    var allDefs = document.querySelectorAll('defs');
1396    var allDefsCount = allDefs.length;
1397    for (var i = 0; i < allDefsCount; i++) {
1398      var thisDefs = allDefs[i];
1399      thisDefs.parentNode.insertBefore(thisDefs, thisDefs);
1400    }
1401  }, this.internetExplorerRedisplayInterval)
1402
1403  /**
1404   * Sets the current transform matrix of an element
1405   *
1406   * @param {SVGElement} element
1407   * @param {SVGMatrix} matrix  CTM
1408   * @param {SVGElement} defs
1409   */
1410, setCTM: function(element, matrix, defs) {
1411    var that = this
1412      , s = 'matrix(' + matrix.a + ',' + matrix.b + ',' + matrix.c + ',' + matrix.d + ',' + matrix.e + ',' + matrix.f + ')';
1413
1414    element.setAttributeNS(null, 'transform', s);
1415    if ('transform' in element.style) {
1416      element.style.transform = s;
1417    } else if ('-ms-transform' in element.style) {
1418      element.style['-ms-transform'] = s;
1419    } else if ('-webkit-transform' in element.style) {
1420      element.style['-webkit-transform'] = s;
1421    }
1422
1423    // IE has a bug that makes markers disappear on zoom (when the matrix "a" and/or "d" elements change)
1424    // see http://stackoverflow.com/questions/17654578/svg-marker-does-not-work-in-ie9-10
1425    // and http://srndolha.wordpress.com/2013/11/25/svg-line-markers-may-disappear-in-internet-explorer-11/
1426    if (_browser === 'ie' && !!defs) {
1427      // this refresh is intended for redisplaying the SVG during zooming
1428      defs.parentNode.insertBefore(defs, defs);
1429      // this refresh is intended for redisplaying the other SVGs on a page when panning a given SVG
1430      // it is also needed for the given SVG itself, on zoomEnd, if the SVG contains any markers that
1431      // are located under any other element(s).
1432      window.setTimeout(function() {
1433        that.refreshDefsGlobal();
1434      }, that.internetExplorerRedisplayInterval);
1435    }
1436  }
1437
1438  /**
1439   * Instantiate an SVGPoint object with given event coordinates
1440   *
1441   * @param {Event} evt
1442   * @param  {SVGSVGElement} svg
1443   * @return {SVGPoint}     point
1444   */
1445, getEventPoint: function(evt, svg) {
1446    var point = svg.createSVGPoint()
1447
1448    Utils.mouseAndTouchNormalize(evt, svg)
1449
1450    point.x = evt.clientX
1451    point.y = evt.clientY
1452
1453    return point
1454  }
1455
1456  /**
1457   * Get SVG center point
1458   *
1459   * @param  {SVGSVGElement} svg
1460   * @return {SVGPoint}
1461   */
1462, getSvgCenterPoint: function(svg, width, height) {
1463    return this.createSVGPoint(svg, width / 2, height / 2)
1464  }
1465
1466  /**
1467   * Create a SVGPoint with given x and y
1468   *
1469   * @param  {SVGSVGElement} svg
1470   * @param  {Number} x
1471   * @param  {Number} y
1472   * @return {SVGPoint}
1473   */
1474, createSVGPoint: function(svg, x, y) {
1475    var point = svg.createSVGPoint()
1476    point.x = x
1477    point.y = y
1478
1479    return point
1480  }
1481}
1482
1483},{"./utilities":7}],6:[function(require,module,exports){
1484// uniwheel 0.1.2 (customized)
1485// A unified cross browser mouse wheel event handler
1486// https://github.com/teemualap/uniwheel
1487
1488module.exports = (function(){
1489
1490  //Full details: https://developer.mozilla.org/en-US/docs/Web/Reference/Events/wheel
1491
1492  var prefix = "", _addEventListener, _removeEventListener, onwheel, support, fns = [];
1493
1494  // detect event model
1495  if ( window.addEventListener ) {
1496    _addEventListener = "addEventListener";
1497    _removeEventListener = "removeEventListener";
1498  } else {
1499    _addEventListener = "attachEvent";
1500    _removeEventListener = "detachEvent";
1501    prefix = "on";
1502  }
1503
1504  // detect available wheel event
1505  support = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel"
1506            document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel"
1507            "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox
1508
1509
1510  function createCallback(element,callback,capture) {
1511
1512    var fn = function(originalEvent) {
1513
1514      !originalEvent && ( originalEvent = window.event );
1515
1516      // create a normalized event object
1517      var event = {
1518        // keep a ref to the original event object
1519        originalEvent: originalEvent,
1520        target: originalEvent.target || originalEvent.srcElement,
1521        type: "wheel",
1522        deltaMode: originalEvent.type == "MozMousePixelScroll" ? 0 : 1,
1523        deltaX: 0,
1524        delatZ: 0,
1525        preventDefault: function() {
1526          originalEvent.preventDefault ?
1527            originalEvent.preventDefault() :
1528            originalEvent.returnValue = false;
1529        }
1530      };
1531
1532      // calculate deltaY (and deltaX) according to the event
1533      if ( support == "mousewheel" ) {
1534        event.deltaY = - 1/40 * originalEvent.wheelDelta;
1535        // Webkit also support wheelDeltaX
1536        originalEvent.wheelDeltaX && ( event.deltaX = - 1/40 * originalEvent.wheelDeltaX );
1537      } else {
1538        event.deltaY = originalEvent.detail;
1539      }
1540
1541      // it's time to fire the callback
1542      return callback( event );
1543
1544    };
1545
1546    fns.push({
1547      element: element,
1548      fn: fn,
1549      capture: capture
1550    });
1551
1552    return fn;
1553  }
1554
1555  function getCallback(element,capture) {
1556    for (var i = 0; i < fns.length; i++) {
1557      if (fns[i].element === element && fns[i].capture === capture) {
1558        return fns[i].fn;
1559      }
1560    }
1561    return function(){};
1562  }
1563
1564  function removeCallback(element,capture) {
1565    for (var i = 0; i < fns.length; i++) {
1566      if (fns[i].element === element && fns[i].capture === capture) {
1567        return fns.splice(i,1);
1568      }
1569    }
1570  }
1571
1572  function _addWheelListener( elem, eventName, callback, useCapture ) {
1573
1574    var cb;
1575
1576    if (support === "wheel") {
1577      cb = callback;
1578    } else {
1579      cb = createCallback(elem,callback,useCapture);
1580    }
1581
1582    elem[ _addEventListener ]( prefix + eventName, cb, useCapture || false );
1583
1584  }
1585
1586  function _removeWheelListener( elem, eventName, callback, useCapture ) {
1587
1588    var cb;
1589
1590    if (support === "wheel") {
1591      cb = callback;
1592    } else {
1593      cb = getCallback(elem,useCapture);
1594    }
1595
1596    elem[ _removeEventListener ]( prefix + eventName, cb, useCapture || false );
1597
1598    removeCallback(elem,useCapture);
1599
1600  }
1601
1602  function addWheelListener( elem, callback, useCapture ) {
1603    _addWheelListener( elem, support, callback, useCapture );
1604
1605    // handle MozMousePixelScroll in older Firefox
1606    if( support == "DOMMouseScroll" ) {
1607        _addWheelListener( elem, "MozMousePixelScroll", callback, useCapture);
1608    }
1609  }
1610
1611  function removeWheelListener(elem,callback,useCapture){
1612    _removeWheelListener(elem,support,callback,useCapture);
1613
1614    // handle MozMousePixelScroll in older Firefox
1615    if( support == "DOMMouseScroll" ) {
1616        _removeWheelListener(elem, "MozMousePixelScroll", callback, useCapture);
1617    }
1618  }
1619
1620  return {
1621    on: addWheelListener,
1622    off: removeWheelListener
1623  };
1624
1625})();
1626
1627},{}],7:[function(require,module,exports){
1628module.exports = {
1629  /**
1630   * Extends an object
1631   *
1632   * @param  {Object} target object to extend
1633   * @param  {Object} source object to take properties from
1634   * @return {Object}        extended object
1635   */
1636  extend: function(target, source) {
1637    target = target || {};
1638    for (var prop in source) {
1639      // Go recursively
1640      if (this.isObject(source[prop])) {
1641        target[prop] = this.extend(target[prop], source[prop])
1642      } else {
1643        target[prop] = source[prop]
1644      }
1645    }
1646    return target;
1647  }
1648
1649  /**
1650   * Checks if an object is a DOM element
1651   *
1652   * @param  {Object}  o HTML element or String
1653   * @return {Boolean}   returns true if object is a DOM element
1654   */
1655, isElement: function(o){
1656    return (
1657      o instanceof HTMLElement || o instanceof SVGElement || o instanceof SVGSVGElement || //DOM2
1658      (o && typeof o === 'object' && o !== null && o.nodeType === 1 && typeof o.nodeName === 'string')
1659    );
1660  }
1661
1662  /**
1663   * Checks if an object is an Object
1664   *
1665   * @param  {Object}  o Object
1666   * @return {Boolean}   returns true if object is an Object
1667   */
1668, isObject: function(o){
1669    return Object.prototype.toString.call(o) === '[object Object]';
1670  }
1671
1672  /**
1673   * Checks if variable is Number
1674   *
1675   * @param  {Integer|Float}  n
1676   * @return {Boolean}   returns true if variable is Number
1677   */
1678, isNumber: function(n) {
1679    return !isNaN(parseFloat(n)) && isFinite(n);
1680  }
1681
1682  /**
1683   * Search for an SVG element
1684   *
1685   * @param  {Object|String} elementOrSelector DOM Element or selector String
1686   * @return {Object|Null}                   SVG or null
1687   */
1688, getSvg: function(elementOrSelector) {
1689    var element
1690      , svg;
1691
1692    if (!this.isElement(elementOrSelector)) {
1693      // If selector provided
1694      if (typeof elementOrSelector === 'string' || elementOrSelector instanceof String) {
1695        // Try to find the element
1696        element = document.querySelector(elementOrSelector)
1697
1698        if (!element) {
1699          throw new Error('Provided selector did not find any elements. Selector: ' + elementOrSelector)
1700          return null
1701        }
1702      } else {
1703        throw new Error('Provided selector is not an HTML object nor String')
1704        return null
1705      }
1706    } else {
1707      element = elementOrSelector
1708    }
1709
1710    if (element.tagName.toLowerCase() === 'svg') {
1711      svg = element;
1712    } else {
1713      if (element.tagName.toLowerCase() === 'object') {
1714        svg = element.contentDocument.documentElement;
1715      } else {
1716        if (element.tagName.toLowerCase() === 'embed') {
1717          svg = element.getSVGDocument().documentElement;
1718        } else {
1719          if (element.tagName.toLowerCase() === 'img') {
1720            throw new Error('Cannot script an SVG in an "img" element. Please use an "object" element or an in-line SVG.');
1721          } else {
1722            throw new Error('Cannot get SVG.');
1723          }
1724          return null
1725        }
1726      }
1727    }
1728
1729    return svg
1730  }
1731
1732  /**
1733   * Attach a given context to a function
1734   * @param  {Function} fn      Function
1735   * @param  {Object}   context Context
1736   * @return {Function}           Function with certain context
1737   */
1738, proxy: function(fn, context) {
1739    return function() {
1740      return fn.apply(context, arguments)
1741    }
1742  }
1743
1744  /**
1745   * Returns object type
1746   * Uses toString that returns [object SVGPoint]
1747   * And than parses object type from string
1748   *
1749   * @param  {Object} o Any object
1750   * @return {String}   Object type
1751   */
1752, getType: function(o) {
1753    return Object.prototype.toString.apply(o).replace(/^\[object\s/, '').replace(/\]$/, '')
1754  }
1755
1756  /**
1757   * If it is a touch event than add clientX and clientY to event object
1758   *
1759   * @param  {Event} evt
1760   * @param  {SVGSVGElement} svg
1761   */
1762, mouseAndTouchNormalize: function(evt, svg) {
1763    // If no clientX then fallback
1764    if (evt.clientX === void 0 || evt.clientX === null) {
1765      // Fallback
1766      evt.clientX = 0
1767      evt.clientY = 0
1768
1769      // If it is a touch event
1770      if (evt.touches !== void 0 && evt.touches.length) {
1771        if (evt.touches[0].clientX !== void 0) {
1772          evt.clientX = evt.touches[0].clientX
1773          evt.clientY = evt.touches[0].clientY
1774        } else if (evt.touches[0].pageX !== void 0) {
1775          var rect = svg.getBoundingClientRect();
1776
1777          evt.clientX = evt.touches[0].pageX - rect.left
1778          evt.clientY = evt.touches[0].pageY - rect.top
1779        }
1780      // If it is a custom event
1781      } else if (evt.originalEvent !== void 0) {
1782        if (evt.originalEvent.clientX !== void 0) {
1783          evt.clientX = evt.originalEvent.clientX
1784          evt.clientY = evt.originalEvent.clientY
1785        }
1786      }
1787    }
1788  }
1789
1790  /**
1791   * Check if an event is a double click/tap
1792   * TODO: For touch gestures use a library (hammer.js) that takes in account other events
1793   * (touchmove and touchend). It should take in account tap duration and traveled distance
1794   *
1795   * @param  {Event}  evt
1796   * @param  {Event}  prevEvt Previous Event
1797   * @return {Boolean}
1798   */
1799, isDblClick: function(evt, prevEvt) {
1800    // Double click detected by browser
1801    if (evt.detail === 2) {
1802      return true;
1803    }
1804    // Try to compare events
1805    else if (prevEvt !== void 0 && prevEvt !== null) {
1806      var timeStampDiff = evt.timeStamp - prevEvt.timeStamp // should be lower than 250 ms
1807        , touchesDistance = Math.sqrt(Math.pow(evt.clientX - prevEvt.clientX, 2) + Math.pow(evt.clientY - prevEvt.clientY, 2))
1808
1809      return timeStampDiff < 250 && touchesDistance < 10
1810    }
1811
1812    // Nothing found
1813    return false;
1814  }
1815
1816  /**
1817   * Returns current timestamp as an integer
1818   *
1819   * @return {Number}
1820   */
1821, now: Date.now || function() {
1822    return new Date().getTime();
1823  }
1824
1825  // From underscore.
1826  // Returns a function, that, when invoked, will only be triggered at most once
1827  // during a given window of time. Normally, the throttled function will run
1828  // as much as it can, without ever going more than once per `wait` duration;
1829  // but if you'd like to disable the execution on the leading edge, pass
1830  // `{leading: false}`. To disable execution on the trailing edge, ditto.
1831// jscs:disable
1832// jshint ignore:start
1833, throttle: function(func, wait, options) {
1834    var that = this;
1835    var context, args, result;
1836    var timeout = null;
1837    var previous = 0;
1838    if (!options) options = {};
1839    var later = function() {
1840      previous = options.leading === false ? 0 : that.now();
1841      timeout = null;
1842      result = func.apply(context, args);
1843      if (!timeout) context = args = null;
1844    };
1845    return function() {
1846      var now = that.now();
1847      if (!previous && options.leading === false) previous = now;
1848      var remaining = wait - (now - previous);
1849      context = this;
1850      args = arguments;
1851      if (remaining <= 0 || remaining > wait) {
1852        clearTimeout(timeout);
1853        timeout = null;
1854        previous = now;
1855        result = func.apply(context, args);
1856        if (!timeout) context = args = null;
1857      } else if (!timeout && options.trailing !== false) {
1858        timeout = setTimeout(later, remaining);
1859      }
1860      return result;
1861    };
1862  }
1863// jshint ignore:end
1864// jscs:enable
1865
1866  /**
1867   * Create a requestAnimationFrame simulation
1868   *
1869   * @param  {Number|String} refreshRate
1870   * @return {Function}
1871   */
1872, createRequestAnimationFrame: function(refreshRate) {
1873    var timeout = null
1874
1875    // Convert refreshRate to timeout
1876    if (refreshRate !== 'auto' && refreshRate < 60 && refreshRate > 1) {
1877      timeout = Math.floor(1000 / refreshRate)
1878    }
1879
1880    if (timeout === null) {
1881      return window.requestAnimationFrame || requestTimeout(33)
1882    } else {
1883      return requestTimeout(timeout)
1884    }
1885  }
1886}
1887
1888/**
1889 * Create a callback that will execute after a given timeout
1890 *
1891 * @param  {Function} timeout
1892 * @return {Function}
1893 */
1894function requestTimeout(timeout) {
1895  return function(callback) {
1896    window.setTimeout(callback, timeout)
1897  }
1898}
1899
1900},{}]},{},[1]);
Note: See TracBrowser for help on using the repository browser.