1/**
2 *  SVGPan library 1.2.2
3 * ======================
4 *
5 * Given an unique existing element with id "viewport" (or when missing, the
6 * first g-element), including the the library into any SVG adds the following
7 * capabilities:
8 *
9 *  - Mouse panning
10 *  - Mouse zooming (using the wheel)
11 *  - Object dragging
12 *
13 * You can configure the behaviour of the pan/zoom/drag with the variables
14 * listed in the CONFIGURATION section of this file.
15 *
16 * This code is licensed under the following BSD license:
17 *
18 * Copyright 2009-2019 Andrea Leofreddi <[email protected]>. All rights reserved.
19 *
20 * Redistribution and use in source and binary forms, with or without modification, are
21 * permitted provided that the following conditions are met:
22 *
23 *    1. Redistributions of source code must retain the above copyright
24 *       notice, this list of conditions and the following disclaimer.
25 *    2. Redistributions in binary form must reproduce the above copyright
26 *       notice, this list of conditions and the following disclaimer in the
27 *       documentation and/or other materials provided with the distribution.
28 *    3. Neither the name of the copyright holder nor the names of its
29 *       contributors may be used to endorse or promote products derived from
30 *       this software without specific prior written permission.
31 *
32 * THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS
33 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
34 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR
35 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
36 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
37 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
38 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
39 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
40 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
41 *
42 * The views and conclusions contained in the software and documentation are those of the
43 * authors and should not be interpreted as representing official policies, either expressed
44 * or implied, of Andrea Leofreddi.
45 */
46
47"use strict";
48
49/// CONFIGURATION
50/// ====>
51
52var enablePan = 1; // 1 or 0: enable or disable panning (default enabled)
53var enableZoom = 1; // 1 or 0: enable or disable zooming (default enabled)
54var enableDrag = 0; // 1 or 0: enable or disable dragging (default disabled)
55var zoomScale = 0.2; // Zoom sensitivity
56
57/// <====
58/// END OF CONFIGURATION
59
60var root = document.documentElement;
61var state = 'none', svgRoot = null, stateTarget, stateOrigin, stateTf;
62
63setupHandlers(root);
64
65/**
66 * Register handlers
67 */
68function setupHandlers(root){
69	setAttributes(root, {
70		"onmouseup" : "handleMouseUp(evt)",
71		"onmousedown" : "handleMouseDown(evt)",
72		"onmousemove" : "handleMouseMove(evt)",
73		//"onmouseout" : "handleMouseUp(evt)", // Decomment this to stop the pan functionality when dragging out of the SVG element
74	});
75
76	if(navigator.userAgent.toLowerCase().indexOf('webkit') >= 0)
77		window.addEventListener('mousewheel', handleMouseWheel, false); // Chrome/Safari
78	else
79		window.addEventListener('DOMMouseScroll', handleMouseWheel, false); // Others
80}
81
82/**
83 * Retrieves the root element for SVG manipulation. The element is then cached into the svgRoot global variable.
84 */
85function getRoot(root) {
86	if(svgRoot == null) {
87		var r = root.getElementById("viewport") ? root.getElementById("viewport") : root.documentElement, t = r;
88
89		while(t != root) {
90			if(t.getAttribute("viewBox")) {
91				setCTM(r, t.getCTM());
92
93				t.removeAttribute("viewBox");
94			}
95
96			t = t.parentNode;
97		}
98
99		svgRoot = r;
100	}
101
102	return svgRoot;
103}
104
105/**
106 * Instance an SVGPoint object with given event coordinates.
107 */
108function getEventPoint(evt) {
109	var p = root.createSVGPoint();
110
111	p.x = evt.clientX;
112	p.y = evt.clientY;
113
114	return p;
115}
116
117/**
118 * Sets the current transform matrix of an element.
119 */
120function setCTM(element, matrix) {
121	var s = "matrix(" + matrix.a + "," + matrix.b + "," + matrix.c + "," + matrix.d + "," + matrix.e + "," + matrix.f + ")";
122
123	element.setAttribute("transform", s);
124}
125
126/**
127 * Dumps a matrix to a string (useful for debug).
128 */
129function dumpMatrix(matrix) {
130	var s = "[ " + matrix.a + ", " + matrix.c + ", " + matrix.e + "\n  " + matrix.b + ", " + matrix.d + ", " + matrix.f + "\n  0, 0, 1 ]";
131
132	return s;
133}
134
135/**
136 * Sets attributes of an element.
137 */
138function setAttributes(element, attributes){
139	for (var i in attributes)
140		element.setAttributeNS(null, i, attributes[i]);
141}
142
143/**
144 * Handle mouse wheel event.
145 */
146function handleMouseWheel(evt) {
147	if(!enableZoom)
148		return;
149
150	if(evt.preventDefault)
151		evt.preventDefault();
152
153	evt.returnValue = false;
154
155	var svgDoc = evt.target.ownerDocument;
156
157	var delta;
158
159	if(evt.wheelDelta)
160		delta = evt.wheelDelta / 360; // Chrome/Safari
161	else
162		delta = evt.detail / -9; // Mozilla
163
164	var z = Math.pow(1 + zoomScale, delta);
165
166	var g = getRoot(svgDoc);
167
168	var p = getEventPoint(evt);
169
170	p = p.matrixTransform(g.getCTM().inverse());
171
172	// Compute new scale matrix in current mouse position
173	var k = root.createSVGMatrix().translate(p.x, p.y).scale(z).translate(-p.x, -p.y);
174
175	setCTM(g, g.getCTM().multiply(k));
176
177	if(typeof(stateTf) == "undefined")
178		stateTf = g.getCTM().inverse();
179
180	stateTf = stateTf.multiply(k.inverse());
181}
182
183/**
184 * Handle mouse move event.
185 */
186function handleMouseMove(evt) {
187	if(evt.preventDefault)
188		evt.preventDefault();
189
190	evt.returnValue = false;
191
192	var svgDoc = evt.target.ownerDocument;
193
194	var g = getRoot(svgDoc);
195
196	if(state == 'pan' && enablePan) {
197		// Pan mode
198		var p = getEventPoint(evt).matrixTransform(stateTf);
199
200		setCTM(g, stateTf.inverse().translate(p.x - stateOrigin.x, p.y - stateOrigin.y));
201	} else if(state == 'drag' && enableDrag) {
202		// Drag mode
203		var p = getEventPoint(evt).matrixTransform(g.getCTM().inverse());
204
205		setCTM(stateTarget, root.createSVGMatrix().translate(p.x - stateOrigin.x, p.y - stateOrigin.y).multiply(g.getCTM().inverse()).multiply(stateTarget.getCTM()));
206
207		stateOrigin = p;
208	}
209}
210
211/**
212 * Handle click event.
213 */
214function handleMouseDown(evt) {
215	if(evt.preventDefault)
216		evt.preventDefault();
217
218	evt.returnValue = false;
219
220	var svgDoc = evt.target.ownerDocument;
221
222	var g = getRoot(svgDoc);
223
224	if(
225		evt.target.tagName == "svg"
226		|| !enableDrag // Pan anyway when drag is disabled and the user clicked on an element
227	) {
228		// Pan mode
229		state = 'pan';
230
231		stateTf = g.getCTM().inverse();
232
233		stateOrigin = getEventPoint(evt).matrixTransform(stateTf);
234	} else {
235		// Drag mode
236		state = 'drag';
237
238		stateTarget = evt.target;
239
240		stateTf = g.getCTM().inverse();
241
242		stateOrigin = getEventPoint(evt).matrixTransform(stateTf);
243	}
244}
245
246/**
247 * Handle mouse button release event.
248 */
249function handleMouseUp(evt) {
250	if(evt.preventDefault)
251		evt.preventDefault();
252
253	evt.returnValue = false;
254
255	var svgDoc = evt.target.ownerDocument;
256
257	if(state == 'pan' || state == 'drag') {
258		// Quit pan mode
259		state = '';
260	}
261}
262