/*! JointJS v1.1.1-alpha.1 (2017-06-02) - JavaScript diagramming library
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// For AMD.
define(['backbone', 'lodash', 'jquery'], function(Backbone, _, $) {
Backbone.$ = $;
return factory(root, Backbone, _, $);
});
} else if (typeof exports !== 'undefined') {
// For Node.js or CommonJS.
var Backbone = require('backbone');
var _ = require('lodash');
var $ = Backbone.$ = require('jquery');
module.exports = factory(root, Backbone, _, $);
} else {
// As a browser global.
var Backbone = root.Backbone;
var _ = root._;
var $ = Backbone.$ = root.jQuery || root.$;
root.joint = factory(root, Backbone, _, $);
root.g = root.joint.g;
root.V = root.Vectorizer = root.joint.V;
}
}(this, function(root, Backbone, _, $) {
(function() {
/**
* version: 0.3.0
* git://github.com/davidchambers/Base64.js.git
*/
var object = typeof exports != 'undefined' ? exports : this; // #8: web workers
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
function InvalidCharacterError(message) {
this.message = message;
}
InvalidCharacterError.prototype = new Error;
InvalidCharacterError.prototype.name = 'InvalidCharacterError';
// encoder
// [https://gist.github.com/999166] by [https://github.com/nignag]
object.btoa || (
object.btoa = function(input) {
var str = String(input);
for (
// initialize result and counter
var block, charCode, idx = 0, map = chars, output = '';
// if the next str index does not exist:
// change the mapping table to "="
// check if d has no fractional digits
str.charAt(idx | 0) || (map = '=', idx % 1);
// "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
output += map.charAt(63 & block >> 8 - idx % 1 * 8)
) {
charCode = str.charCodeAt(idx += 3 / 4);
if (charCode > 0xFF) {
throw new InvalidCharacterError("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
}
block = block << 8 | charCode;
}
return output;
});
// decoder
// [https://gist.github.com/1020396] by [https://github.com/atk]
object.atob || (
object.atob = function(input) {
var str = String(input).replace(/=+$/, '');
if (str.length % 4 == 1) {
throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = '';
// get next character
// eslint-disable-next-line no-cond-assign
buffer = str.charAt(idx++);
// character found in table? initialize bit storage and add its ascii value;
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf(buffer);
}
return output;
});
}());
(function() {
if (typeof Uint8Array !== 'undefined' || typeof window === 'undefined') {
return;
}
function subarray(start, end) {
return this.slice(start, end);
}
function set_(array, offset) {
if (arguments.length < 2) {
offset = 0;
}
for (var i = 0, n = array.length; i < n; ++i, ++offset) {
this[offset] = array[i] & 0xFF;
}
}
// we need typed arrays
function TypedArray(arg1) {
var result;
if (typeof arg1 === 'number') {
result = new Array(arg1);
for (var i = 0; i < arg1; ++i) {
result[i] = 0;
}
} else {
result = arg1.slice(0);
}
result.subarray = subarray;
result.buffer = result;
result.byteLength = result.length;
result.set = set_;
if (typeof arg1 === 'object' && arg1.buffer) {
result.buffer = arg1.buffer;
}
return result;
}
window.Uint8Array = TypedArray;
window.Uint32Array = TypedArray;
window.Int32Array = TypedArray;
})();
/**
* make xhr.response = 'arraybuffer' available for the IE9
*/
(function() {
if (typeof XMLHttpRequest === 'undefined') {
return;
}
if ('response' in XMLHttpRequest.prototype ||
'mozResponseArrayBuffer' in XMLHttpRequest.prototype ||
'mozResponse' in XMLHttpRequest.prototype ||
'responseArrayBuffer' in XMLHttpRequest.prototype) {
return;
}
Object.defineProperty(XMLHttpRequest.prototype, 'response', {
get: function() {
return new Uint8Array(new VBArray(this.responseBody).toArray());
}
});
})();
// Geometry library.
var g = (function() {
var g = {};
// Declare shorthands to the most used math functions.
var math = Math;
var abs = math.abs;
var cos = math.cos;
var sin = math.sin;
var sqrt = math.sqrt;
var mmin = math.min;
var mmax = math.max;
var atan2 = math.atan2;
var round = math.round;
var floor = math.floor;
var PI = math.PI;
var random = math.random;
var pow = math.pow;
g.bezier = {
// Cubic Bezier curve path through points.
// Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx).
// @param {array} points Array of points through which the smooth line will go.
// @return {array} SVG Path commands as an array
curveThroughPoints: function(points) {
var controlPoints = this.getCurveControlPoints(points);
var path = ['M', points[0].x, points[0].y];
for (var i = 0; i < controlPoints[0].length; i++) {
path.push('C', controlPoints[0][i].x, controlPoints[0][i].y, controlPoints[1][i].x, controlPoints[1][i].y, points[i + 1].x, points[i + 1].y);
}
return path;
},
// Get open-ended Bezier Spline Control Points.
// @param knots Input Knot Bezier spline points (At least two points!).
// @param firstControlPoints Output First Control points. Array of knots.length - 1 length.
// @param secondControlPoints Output Second Control points. Array of knots.length - 1 length.
getCurveControlPoints: function(knots) {
var firstControlPoints = [];
var secondControlPoints = [];
var n = knots.length - 1;
var i;
// Special case: Bezier curve should be a straight line.
if (n == 1) {
// 3P1 = 2P0 + P3
firstControlPoints[0] = Point((2 * knots[0].x + knots[1].x) / 3,
(2 * knots[0].y + knots[1].y) / 3);
// P2 = 2P1 – P0
secondControlPoints[0] = Point(2 * firstControlPoints[0].x - knots[0].x,
2 * firstControlPoints[0].y - knots[0].y);
return [firstControlPoints, secondControlPoints];
}
// Calculate first Bezier control points.
// Right hand side vector.
var rhs = [];
// Set right hand side X values.
for (i = 1; i < n - 1; i++) {
rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x;
}
rhs[0] = knots[0].x + 2 * knots[1].x;
rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0;
// Get first control points X-values.
var x = this.getFirstControlPoints(rhs);
// Set right hand side Y values.
for (i = 1; i < n - 1; ++i) {
rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y;
}
rhs[0] = knots[0].y + 2 * knots[1].y;
rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0;
// Get first control points Y-values.
var y = this.getFirstControlPoints(rhs);
// Fill output arrays.
for (i = 0; i < n; i++) {
// First control point.
firstControlPoints.push(Point(x[i], y[i]));
// Second control point.
if (i < n - 1) {
secondControlPoints.push(Point(2 * knots [i + 1].x - x[i + 1],
2 * knots[i + 1].y - y[i + 1]));
} else {
secondControlPoints.push(Point((knots[n].x + x[n - 1]) / 2,
(knots[n].y + y[n - 1]) / 2));
}
}
return [firstControlPoints, secondControlPoints];
},
// Divide a Bezier curve into two at point defined by value 't' <0,1>.
// Using deCasteljau algorithm. http://math.stackexchange.com/a/317867
// @param control points (start, control start, control end, end)
// @return a function accepts t and returns 2 curves each defined by 4 control points.
getCurveDivider: function(p0, p1, p2, p3) {
return function divideCurve(t) {
var l = Line(p0, p1).pointAt(t);
var m = Line(p1, p2).pointAt(t);
var n = Line(p2, p3).pointAt(t);
var p = Line(l, m).pointAt(t);
var q = Line(m, n).pointAt(t);
var r = Line(p, q).pointAt(t);
return [{ p0: p0, p1: l, p2: p, p3: r }, { p0: r, p1: q, p2: n, p3: p3 }];
};
},
// Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points.
// @param rhs Right hand side vector.
// @return Solution vector.
getFirstControlPoints: function(rhs) {
var n = rhs.length;
// `x` is a solution vector.
var x = [];
var tmp = [];
var b = 2.0;
x[0] = rhs[0] / b;
// Decomposition and forward substitution.
for (var i = 1; i < n; i++) {
tmp[i] = 1 / b;
b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
x[i] = (rhs[i] - x[i - 1]) / b;
}
for (i = 1; i < n; i++) {
// Backsubstitution.
x[n - i - 1] -= tmp[n - i] * x[n - i];
}
return x;
},
// Solves an inversion problem -- Given the (x, y) coordinates of a point which lies on
// a parametric curve x = x(t)/w(t), y = y(t)/w(t), find the parameter value t
// which corresponds to that point.
// @param control points (start, control start, control end, end)
// @return a function accepts a point and returns t.
getInversionSolver: function(p0, p1, p2, p3) {
var pts = arguments;
function l(i, j) {
// calculates a determinant 3x3
// [p.x p.y 1]
// [pi.x pi.y 1]
// [pj.x pj.y 1]
var pi = pts[i];
var pj = pts[j];
return function(p) {
var w = (i % 3 ? 3 : 1) * (j % 3 ? 3 : 1);
var lij = p.x * (pi.y - pj.y) + p.y * (pj.x - pi.x) + pi.x * pj.y - pi.y * pj.x;
return w * lij;
};
}
return function solveInversion(p) {
var ct = 3 * l(2, 3)(p1);
var c1 = l(1, 3)(p0) / ct;
var c2 = -l(2, 3)(p0) / ct;
var la = c1 * l(3, 1)(p) + c2 * (l(3, 0)(p) + l(2, 1)(p)) + l(2, 0)(p);
var lb = c1 * l(3, 0)(p) + c2 * l(2, 0)(p) + l(1, 0)(p);
return lb / (lb - la);
};
}
};
var Ellipse = g.Ellipse = function(c, a, b) {
if (!(this instanceof Ellipse)) {
return new Ellipse(c, a, b);
}
if (c instanceof Ellipse) {
return new Ellipse(Point(c), c.a, c.b);
}
c = Point(c);
this.x = c.x;
this.y = c.y;
this.a = a;
this.b = b;
};
g.Ellipse.fromRect = function(rect) {
rect = Rect(rect);
return Ellipse(rect.center(), rect.width / 2, rect.height / 2);
};
g.Ellipse.prototype = {
bbox: function() {
return Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b);
},
clone: function() {
return Ellipse(this);
},
/**
* @param {g.Point} point
* @returns {number} result < 1 - inside ellipse, result == 1 - on ellipse boundary, result > 1 - outside
*/
normalizedDistance: function(point) {
var x0 = point.x;
var y0 = point.y;
var a = this.a;
var b = this.b;
var x = this.x;
var y = this.y;
return ((x0 - x) * (x0 - x)) / (a * a ) + ((y0 - y) * (y0 - y)) / (b * b);
},
// inflate by dx and dy
// @param dx {delta_x} representing additional size to x
// @param dy {delta_y} representing additional size to y -
// dy param is not required -> in that case y is sized by dx
inflate: function(dx, dy) {
if (dx === undefined) {
dx = 0;
}
if (dy === undefined) {
dy = dx;
}
this.a += 2 * dx;
this.b += 2 * dy;
return this;
},
/**
* @param {g.Point} p
* @returns {boolean}
*/
containsPoint: function(p) {
return this.normalizedDistance(p) <= 1;
},
/**
* @returns {g.Point}
*/
center: function() {
return Point(this.x, this.y);
},
/** Compute angle between tangent and x axis
* @param {g.Point} p Point of tangency, it has to be on ellipse boundaries.
* @returns {number} angle between tangent and x axis
*/
tangentTheta: function(p) {
var refPointDelta = 30;
var x0 = p.x;
var y0 = p.y;
var a = this.a;
var b = this.b;
var center = this.bbox().center();
var m = center.x;
var n = center.y;
var q1 = x0 > center.x + a / 2;
var q3 = x0 < center.x - a / 2;
var y, x;
if (q1 || q3) {
y = x0 > center.x ? y0 - refPointDelta : y0 + refPointDelta;
x = (a * a / (x0 - m)) - (a * a * (y0 - n) * (y - n)) / (b * b * (x0 - m)) + m;
} else {
x = y0 > center.y ? x0 + refPointDelta : x0 - refPointDelta;
y = ( b * b / (y0 - n)) - (b * b * (x0 - m) * (x - m)) / (a * a * (y0 - n)) + n;
}
return g.point(x, y).theta(p);
},
equals: function(ellipse) {
ellipse = Ellipse(ellipse);
return ellipse.x === this.x &&
ellipse.y === this.y &&
ellipse.a === this.a &&
ellipse.b === this.b;
},
// Find point on me where line from my center to
// point p intersects my boundary.
// @param {number} angle If angle is specified, intersection with rotated ellipse is computed.
intersectionWithLineFromCenterToPoint: function(p, angle) {
p = Point(p);
if (angle) p.rotate(Point(this.x, this.y), angle);
var dx = p.x - this.x;
var dy = p.y - this.y;
var result;
if (dx === 0) {
result = this.bbox().pointNearestToPoint(p);
if (angle) return result.rotate(Point(this.x, this.y), -angle);
return result;
}
var m = dy / dx;
var mSquared = m * m;
var aSquared = this.a * this.a;
var bSquared = this.b * this.b;
var x = sqrt(1 / ((1 / aSquared) + (mSquared / bSquared)));
x = dx < 0 ? -x : x;
var y = m * x;
result = Point(this.x + x, this.y + y);
if (angle) return result.rotate(Point(this.x, this.y), -angle);
return result;
},
toString: function() {
return Point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b;
}
};
var Line = g.Line = function(p1, p2) {
if (!(this instanceof Line)) {
return new Line(p1, p2);
}
this.start = Point(p1);
this.end = Point(p2);
};
g.Line.prototype = {
// @return the bearing (cardinal direction) of the line. For example N, W, or SE.
// @returns {String} One of the following bearings : NE, E, SE, S, SW, W, NW, N.
bearing: function() {
var lat1 = toRad(this.start.y);
var lat2 = toRad(this.end.y);
var lon1 = this.start.x;
var lon2 = this.end.x;
var dLon = toRad(lon2 - lon1);
var y = sin(dLon) * cos(lat2);
var x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon);
var brng = toDeg(atan2(y, x));
var bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N'];
var index = brng - 22.5;
if (index < 0)
index += 360;
index = parseInt(index / 45);
return bearings[index];
},
clone: function() {
return Line(this);
},
equals: function(l) {
return this.start.x === l.start.x &&
this.start.y === l.start.y &&
this.end.x === l.end.x &&
this.end.y === l.end.y;
},
// @return {point} Point where I'm intersecting a line.
// @return [point] Points where I'm intersecting a rectangle.
// @see Squeak Smalltalk, LineSegment>>intersectionWith:
intersect: function(l) {
if (l instanceof Line) {
// Passed in parameter is a line.
var pt1Dir = Point(this.end.x - this.start.x, this.end.y - this.start.y);
var pt2Dir = Point(l.end.x - l.start.x, l.end.y - l.start.y);
var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x);
var deltaPt = Point(l.start.x - this.start.x, l.start.y - this.start.y);
var alpha = (deltaPt.x * pt2Dir.y) - (deltaPt.y * pt2Dir.x);
var beta = (deltaPt.x * pt1Dir.y) - (deltaPt.y * pt1Dir.x);
if (det === 0 ||
alpha * det < 0 ||
beta * det < 0) {
// No intersection found.
return null;
}
if (det > 0) {
if (alpha > det || beta > det) {
return null;
}
} else {
if (alpha < det || beta < det) {
return null;
}
}
return Point(this.start.x + (alpha * pt1Dir.x / det),
this.start.y + (alpha * pt1Dir.y / det));
} else if (l instanceof Rect) {
// Passed in parameter is a rectangle.
var r = l;
var rectLines = [ r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine() ];
var points = [];
var dedupeArr = [];
var pt, i;
for (i = 0; i < rectLines.length; i ++) {
pt = this.intersect(rectLines[i]);
if (pt !== null && dedupeArr.indexOf(pt.toString()) < 0) {
points.push(pt);
dedupeArr.push(pt.toString());
}
}
return points.length > 0 ? points : null;
}
// Passed in parameter is neither a Line nor a Rectangle.
return null;
},
// @return {double} length of the line
length: function() {
return sqrt(this.squaredLength());
},
// @return {point} my midpoint
midpoint: function() {
return Point((this.start.x + this.end.x) / 2,
(this.start.y + this.end.y) / 2);
},
// @return {point} my point at 't' <0,1>
pointAt: function(t) {
var x = (1 - t) * this.start.x + t * this.end.x;
var y = (1 - t) * this.start.y + t * this.end.y;
return Point(x, y);
},
// @return {number} the offset of the point `p` from the line. + if the point `p` is on the right side of the line, - if on the left and 0 if on the line.
pointOffset: function(p) {
// Find the sign of the determinant of vectors (start,end), where p is the query point.
return ((this.end.x - this.start.x) * (p.y - this.start.y) - (this.end.y - this.start.y) * (p.x - this.start.x)) / 2;
},
// @return {integer} length without sqrt
// @note for applications where the exact length is not necessary (e.g. compare only)
squaredLength: function() {
var x0 = this.start.x;
var y0 = this.start.y;
var x1 = this.end.x;
var y1 = this.end.y;
return (x0 -= x1) * x0 + (y0 -= y1) * y0;
},
toString: function() {
return this.start.toString() + ' ' + this.end.toString();
}
};
// For backwards compatibility:
g.Line.prototype.intersection = g.Line.prototype.intersect;
/*
Point is the most basic object consisting of x/y coordinate.
Possible instantiations are:
* `Point(10, 20)`
* `new Point(10, 20)`
* `Point('10 20')`
* `Point(Point(10, 20))`
*/
var Point = g.Point = function(x, y) {
if (!(this instanceof Point)) {
return new Point(x, y);
}
if (typeof x === 'string') {
var xy = x.split(x.indexOf('@') === -1 ? ' ' : '@');
x = parseInt(xy[0], 10);
y = parseInt(xy[1], 10);
} else if (Object(x) === x) {
y = x.y;
x = x.x;
}
this.x = x === undefined ? 0 : x;
this.y = y === undefined ? 0 : y;
};
// Alternative constructor, from polar coordinates.
// @param {number} Distance.
// @param {number} Angle in radians.
// @param {point} [optional] Origin.
g.Point.fromPolar = function(distance, angle, origin) {
origin = (origin && Point(origin)) || Point(0, 0);
var x = abs(distance * cos(angle));
var y = abs(distance * sin(angle));
var deg = normalizeAngle(toDeg(angle));
if (deg < 90) {
y = -y;
} else if (deg < 180) {
x = -x;
y = -y;
} else if (deg < 270) {
x = -x;
}
return Point(origin.x + x, origin.y + y);
};
// Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`.
g.Point.random = function(x1, x2, y1, y2) {
return Point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1));
};
g.Point.prototype = {
// If point lies outside rectangle `r`, return the nearest point on the boundary of rect `r`,
// otherwise return point itself.
// (see Squeak Smalltalk, Point>>adhereTo:)
adhereToRect: function(r) {
if (r.containsPoint(this)) {
return this;
}
this.x = mmin(mmax(this.x, r.x), r.x + r.width);
this.y = mmin(mmax(this.y, r.y), r.y + r.height);
return this;
},
// Return the bearing between me and the given point.
bearing: function(point) {
return Line(this, point).bearing();
},
// Returns change in angle from my previous position (-dx, -dy) to my new position
// relative to ref point.
changeInAngle: function(dx, dy, ref) {
// Revert the translation and measure the change in angle around x-axis.
return Point(this).offset(-dx, -dy).theta(ref) - this.theta(ref);
},
clone: function() {
return Point(this);
},
difference: function(dx, dy) {
if ((Object(dx) === dx)) {
dy = dx.y;
dx = dx.x;
}
return Point(this.x - (dx || 0), this.y - (dy || 0));
},
// Returns distance between me and point `p`.
distance: function(p) {
return Line(this, p).length();
},
equals: function(p) {
return this.x === p.x && this.y === p.y;
},
magnitude: function() {
return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01;
},
// Returns a manhattan (taxi-cab) distance between me and point `p`.
manhattanDistance: function(p) {
return abs(p.x - this.x) + abs(p.y - this.y);
},
// Move point on line starting from ref ending at me by
// distance distance.
move: function(ref, distance) {
var theta = toRad(Point(ref).theta(this));
return this.offset(cos(theta) * distance, -sin(theta) * distance);
},
// Scales x and y such that the distance between the point and the origin (0,0) is equal to the given length.
normalize: function(length) {
var scale = (length || 1) / this.magnitude();
return this.scale(scale, scale);
},
// Offset me by the specified amount.
offset: function(dx, dy) {
if ((Object(dx) === dx)) {
dy = dx.y;
dx = dx.x;
}
this.x += dx || 0;
this.y += dy || 0;
return this;
},
// Returns a point that is the reflection of me with
// the center of inversion in ref point.
reflection: function(ref) {
return Point(ref).move(this, this.distance(ref));
},
// Rotate point by angle around origin.
rotate: function(origin, angle) {
angle = (angle + 360) % 360;
this.toPolar(origin);
this.y += toRad(angle);
var point = Point.fromPolar(this.x, this.y, origin);
this.x = point.x;
this.y = point.y;
return this;
},
round: function(precision) {
var f = pow(10, precision || 0);
this.x = round(this.x * f) / f;
this.y = round(this.y * f) / f;
return this;
},
// Scale point with origin.
scale: function(sx, sy, origin) {
origin = (origin && Point(origin)) || Point(0, 0);
this.x = origin.x + sx * (this.x - origin.x);
this.y = origin.y + sy * (this.y - origin.y);
return this;
},
snapToGrid: function(gx, gy) {
this.x = snapToGrid(this.x, gx);
this.y = snapToGrid(this.y, gy || gx);
return this;
},
// Compute the angle between me and `p` and the x axis.
// (cartesian-to-polar coordinates conversion)
// Return theta angle in degrees.
theta: function(p) {
p = Point(p);
// Invert the y-axis.
var y = -(p.y - this.y);
var x = p.x - this.x;
// Makes sure that the comparison with zero takes rounding errors into account.
var PRECISION = 10;
// Note that `atan2` is not defined for `x`, `y` both equal zero.
var rad = (y.toFixed(PRECISION) == 0 && x.toFixed(PRECISION) == 0) ? 0 : atan2(y, x);
// Correction for III. and IV. quadrant.
if (rad < 0) {
rad = 2 * PI + rad;
}
return 180 * rad / PI;
},
toJSON: function() {
return { x: this.x, y: this.y };
},
// Converts rectangular to polar coordinates.
// An origin can be specified, otherwise it's 0@0.
toPolar: function(o) {
o = (o && Point(o)) || Point(0, 0);
var x = this.x;
var y = this.y;
this.x = sqrt((x - o.x) * (x - o.x) + (y - o.y) * (y - o.y)); // r
this.y = toRad(o.theta(Point(x, y)));
return this;
},
toString: function() {
return this.x + '@' + this.y;
},
update: function(x, y) {
this.x = x || 0;
this.y = y || 0;
return this;
}
};
var Rect = g.Rect = function(x, y, w, h) {
if (!(this instanceof Rect)) {
return new Rect(x, y, w, h);
}
if ((Object(x) === x)) {
y = x.y;
w = x.width;
h = x.height;
x = x.x;
}
this.x = x === undefined ? 0 : x;
this.y = y === undefined ? 0 : y;
this.width = w === undefined ? 0 : w;
this.height = h === undefined ? 0 : h;
};
g.Rect.fromEllipse = function(e) {
e = Ellipse(e);
return Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b);
};
g.Rect.prototype = {
// Find my bounding box when I'm rotated with the center of rotation in the center of me.
// @return r {rectangle} representing a bounding box
bbox: function(angle) {
var theta = toRad(angle || 0);
var st = abs(sin(theta));
var ct = abs(cos(theta));
var w = this.width * ct + this.height * st;
var h = this.width * st + this.height * ct;
return Rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h);
},
bottomLeft: function() {
return Point(this.x, this.y + this.height);
},
bottomLine: function() {
return Line(this.bottomLeft(), this.corner());
},
bottomMiddle: function() {
return Point(this.x + this.width / 2, this.y + this.height);
},
center: function() {
return Point(this.x + this.width / 2, this.y + this.height / 2);
},
clone: function() {
return Rect(this);
},
// @return {bool} true if point p is insight me
containsPoint: function(p) {
p = Point(p);
return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height;
},
// @return {bool} true if rectangle `r` is inside me.
containsRect: function(r) {
var r0 = Rect(this).normalize();
var r1 = Rect(r).normalize();
var w0 = r0.width;
var h0 = r0.height;
var w1 = r1.width;
var h1 = r1.height;
if (!w0 || !h0 || !w1 || !h1) {
// At least one of the dimensions is 0
return false;
}
var x0 = r0.x;
var y0 = r0.y;
var x1 = r1.x;
var y1 = r1.y;
w1 += x1;
w0 += x0;
h1 += y1;
h0 += y0;
return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0;
},
corner: function() {
return Point(this.x + this.width, this.y + this.height);
},
// @return {boolean} true if rectangles are equal.
equals: function(r) {
var mr = Rect(this).normalize();
var nr = Rect(r).normalize();
return mr.x === nr.x && mr.y === nr.y && mr.width === nr.width && mr.height === nr.height;
},
// @return {rect} if rectangles intersect, {null} if not.
intersect: function(r) {
var myOrigin = this.origin();
var myCorner = this.corner();
var rOrigin = r.origin();
var rCorner = r.corner();
// No intersection found
if (rCorner.x <= myOrigin.x ||
rCorner.y <= myOrigin.y ||
rOrigin.x >= myCorner.x ||
rOrigin.y >= myCorner.y) return null;
var x = Math.max(myOrigin.x, rOrigin.x);
var y = Math.max(myOrigin.y, rOrigin.y);
return Rect(x, y, Math.min(myCorner.x, rCorner.x) - x, Math.min(myCorner.y, rCorner.y) - y);
},
// Find point on my boundary where line starting
// from my center ending in point p intersects me.
// @param {number} angle If angle is specified, intersection with rotated rectangle is computed.
intersectionWithLineFromCenterToPoint: function(p, angle) {
p = Point(p);
var center = Point(this.x + this.width / 2, this.y + this.height / 2);
var result;
if (angle) p.rotate(center, angle);
// (clockwise, starting from the top side)
var sides = [
Line(this.origin(), this.topRight()),
Line(this.topRight(), this.corner()),
Line(this.corner(), this.bottomLeft()),
Line(this.bottomLeft(), this.origin())
];
var connector = Line(center, p);
for (var i = sides.length - 1; i >= 0; --i) {
var intersection = sides[i].intersection(connector);
if (intersection !== null) {
result = intersection;
break;
}
}
if (result && angle) result.rotate(center, -angle);
return result;
},
leftLine: function() {
return Line(this.origin(), this.bottomLeft());
},
leftMiddle: function() {
return Point(this.x , this.y + this.height / 2);
},
// Move and expand me.
// @param r {rectangle} representing deltas
moveAndExpand: function(r) {
this.x += r.x || 0;
this.y += r.y || 0;
this.width += r.width || 0;
this.height += r.height || 0;
return this;
},
// inflate by dx and dy, recompute origin [x, y]
// @param dx {delta_x} representing additional size to x
// @param dy {delta_y} representing additional size to y -
// dy param is not required -> in that case y is sized by dx
inflate: function(dx, dy) {
if (dx === undefined) {
dx = 0;
}
if (dy === undefined) {
dy = dx;
}
this.x -= dx;
this.y -= dy;
this.width += 2 * dx;
this.height += 2 * dy;
return this;
},
// Normalize the rectangle; i.e., make it so that it has a non-negative width and height.
// If width < 0 the function swaps the left and right corners,
// and it swaps the top and bottom corners if height < 0
// like in http://qt-project.org/doc/qt-4.8/qrectf.html#normalized
normalize: function() {
var newx = this.x;
var newy = this.y;
var newwidth = this.width;
var newheight = this.height;
if (this.width < 0) {
newx = this.x + this.width;
newwidth = -this.width;
}
if (this.height < 0) {
newy = this.y + this.height;
newheight = -this.height;
}
this.x = newx;
this.y = newy;
this.width = newwidth;
this.height = newheight;
return this;
},
origin: function() {
return Point(this.x, this.y);
},
// @return {point} a point on my boundary nearest to the given point.
// @see Squeak Smalltalk, Rectangle>>pointNearestTo:
pointNearestToPoint: function(point) {
point = Point(point);
if (this.containsPoint(point)) {
var side = this.sideNearestToPoint(point);
switch (side){
case 'right': return Point(this.x + this.width, point.y);
case 'left': return Point(this.x, point.y);
case 'bottom': return Point(point.x, this.y + this.height);
case 'top': return Point(point.x, this.y);
}
}
return point.adhereToRect(this);
},
rightLine: function() {
return Line(this.topRight(), this.corner());
},
rightMiddle: function() {
return Point(this.x + this.width, this.y + this.height / 2);
},
round: function(precision) {
var f = pow(10, precision || 0);
this.x = round(this.x * f) / f;
this.y = round(this.y * f) / f;
this.width = round(this.width * f) / f;
this.height = round(this.height * f) / f;
return this;
},
// Scale rectangle with origin.
scale: function(sx, sy, origin) {
origin = this.origin().scale(sx, sy, origin);
this.x = origin.x;
this.y = origin.y;
this.width *= sx;
this.height *= sy;
return this;
},
maxRectScaleToFit: function(rect, origin) {
rect = g.Rect(rect);
origin || (origin = rect.center());
var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4;
var ox = origin.x;
var oy = origin.y;
// Here we find the maximal possible scale for all corner points (for x and y axis) of the rectangle,
// so when the scale is applied the point is still inside the rectangle.
sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity;
// Top Left
var p1 = rect.origin();
if (p1.x < ox) {
sx1 = (this.x - ox) / (p1.x - ox);
}
if (p1.y < oy) {
sy1 = (this.y - oy) / (p1.y - oy);
}
// Bottom Right
var p2 = rect.corner();
if (p2.x > ox) {
sx2 = (this.x + this.width - ox) / (p2.x - ox);
}
if (p2.y > oy) {
sy2 = (this.y + this.height - oy) / (p2.y - oy);
}
// Top Right
var p3 = rect.topRight();
if (p3.x > ox) {
sx3 = (this.x + this.width - ox) / (p3.x - ox);
}
if (p3.y < oy) {
sy3 = (this.y - oy) / (p3.y - oy);
}
// Bottom Left
var p4 = rect.bottomLeft();
if (p4.x < ox) {
sx4 = (this.x - ox) / (p4.x - ox);
}
if (p4.y > oy) {
sy4 = (this.y + this.height - oy) / (p4.y - oy);
}
return {
sx: Math.min(sx1, sx2, sx3, sx4),
sy: Math.min(sy1, sy2, sy3, sy4)
};
},
maxRectUniformScaleToFit: function(rect, origin) {
var scale = this.maxRectScaleToFit(rect, origin);
return Math.min(scale.sx, scale.sy);
},
// @return {string} (left|right|top|bottom) side which is nearest to point
// @see Squeak Smalltalk, Rectangle>>sideNearestTo:
sideNearestToPoint: function(point) {
point = Point(point);
var distToLeft = point.x - this.x;
var distToRight = (this.x + this.width) - point.x;
var distToTop = point.y - this.y;
var distToBottom = (this.y + this.height) - point.y;
var closest = distToLeft;
var side = 'left';
if (distToRight < closest) {
closest = distToRight;
side = 'right';
}
if (distToTop < closest) {
closest = distToTop;
side = 'top';
}
if (distToBottom < closest) {
closest = distToBottom;
side = 'bottom';
}
return side;
},
snapToGrid: function(gx, gy) {
var origin = this.origin().snapToGrid(gx, gy);
var corner = this.corner().snapToGrid(gx, gy);
this.x = origin.x;
this.y = origin.y;
this.width = corner.x - origin.x;
this.height = corner.y - origin.y;
return this;
},
topLine: function() {
return Line(this.origin(), this.topRight());
},
topMiddle: function() {
return Point(this.x + this.width / 2, this.y);
},
topRight: function() {
return Point(this.x + this.width, this.y);
},
toJSON: function() {
return { x: this.x, y: this.y, width: this.width, height: this.height };
},
toString: function() {
return this.origin().toString() + ' ' + this.corner().toString();
},
// @return {rect} representing the union of both rectangles.
union: function(rect) {
var myOrigin = this.origin();
var myCorner = this.corner();
var rOrigin = rect.origin();
var rCorner = rect.corner();
var originX = Math.min(myOrigin.x, rOrigin.x);
var originY = Math.min(myOrigin.y, rOrigin.y);
var cornerX = Math.max(myCorner.x, rCorner.x);
var cornerY = Math.max(myCorner.y, rCorner.y);
return Rect(originX, originY, cornerX - originX, cornerY - originY);
}
};
var normalizeAngle = g.normalizeAngle = function(angle) {
return (angle % 360) + (angle < 0 ? 360 : 0);
};
g.scale = {
// Return the `value` from the `domain` interval scaled to the `range` interval.
linear: function(domain, range, value) {
var domainSpan = domain[1] - domain[0];
var rangeSpan = range[1] - range[0];
return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0;
}
};
var snapToGrid = g.snapToGrid = function(value, gridSize) {
return gridSize * Math.round(value / gridSize);
};
var toDeg = g.toDeg = function(rad) {
return (180 * rad / PI) % 360;
};
var toRad = g.toRad = function(deg, over360) {
over360 = over360 || false;
deg = over360 ? deg : (deg % 360);
return deg * PI / 180;
};
// For backwards compatibility:
g.ellipse = g.Ellipse;
g.line = g.Line;
g.point = g.Point;
g.rect = g.Rect;
return g;
})();
// Vectorizer.
// -----------
// A tiny library for making your life easier when dealing with SVG.
// The only Vectorizer dependency is the Geometry library.
var V;
var Vectorizer;
V = Vectorizer = (function() {
'use strict';
var hasSvg = typeof window === 'object' &&
!!(
window.SVGAngle ||
document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1')
);
// SVG support is required.
if (!hasSvg) {
// Return a function that throws an error when it is used.
return function() {
throw new Error('SVG is required to use Vectorizer.');
};
}
// XML namespaces.
var ns = {
xmlns: 'http://www.w3.org/2000/svg',
xml: 'http://www.w3.org/XML/1998/namespace',
xlink: 'http://www.w3.org/1999/xlink'
};
var SVGversion = '1.1';
var V = function(el, attrs, children) {
// This allows using V() without the new keyword.
if (!(this instanceof V)) {
return V.apply(Object.create(V.prototype), arguments);
}
if (!el) return;
if (V.isV(el)) {
el = el.node;
}
attrs = attrs || {};
if (V.isString(el)) {
if (el.toLowerCase() === 'svg') {
// Create a new SVG canvas.
el = V.createSvgDocument();
} else if (el[0] === '<') {
// Create element from an SVG string.
// Allows constructs of type: `document.appendChild(V('').node)`.
var svgDoc = V.createSvgDocument(el);
// Note that `V()` might also return an array should the SVG string passed as
// the first argument contain more than one root element.
if (svgDoc.childNodes.length > 1) {
// Map child nodes to `V`s.
var arrayOfVels = [];
var i, len;
for (i = 0, len = svgDoc.childNodes.length; i < len; i++) {
var childNode = svgDoc.childNodes[i];
arrayOfVels.push(new V(document.importNode(childNode, true)));
}
return arrayOfVels;
}
el = document.importNode(svgDoc.firstChild, true);
} else {
el = document.createElementNS(ns.xmlns, el);
}
V.ensureId(el);
}
this.node = el;
this.setAttributes(attrs);
if (children) {
this.append(children);
}
return this;
};
/**
* @param {SVGGElement} toElem
* @returns {SVGMatrix}
*/
V.prototype.getTransformToElement = function(toElem) {
toElem = V.toNode(toElem);
return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM());
};
/**
* @param {SVGMatrix} matrix
* @param {Object=} opt
* @returns {Vectorizer|SVGMatrix} Setter / Getter
*/
V.prototype.transform = function(matrix, opt) {
var node = this.node;
if (V.isUndefined(matrix)) {
return V.transformStringToMatrix(this.attr('transform'));
}
if (opt && opt.absolute) {
return this.attr('transform', V.matrixToTransformString(matrix));
}
var svgTransform = V.createSVGTransform(matrix);
node.transform.baseVal.appendItem(svgTransform);
return this;
};
V.prototype.translate = function(tx, ty, opt) {
opt = opt || {};
ty = ty || 0;
var transformAttr = this.attr('transform') || '';
var transform = V.parseTransformString(transformAttr);
transformAttr = transform.value;
// Is it a getter?
if (V.isUndefined(tx)) {
return transform.translate;
}
transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim();
var newTx = opt.absolute ? tx : transform.translate.tx + tx;
var newTy = opt.absolute ? ty : transform.translate.ty + ty;
var newTranslate = 'translate(' + newTx + ',' + newTy + ')';
// Note that `translate()` is always the first transformation. This is
// usually the desired case.
this.attr('transform', (newTranslate + ' ' + transformAttr).trim());
return this;
};
V.prototype.rotate = function(angle, cx, cy, opt) {
opt = opt || {};
var transformAttr = this.attr('transform') || '';
var transform = V.parseTransformString(transformAttr);
transformAttr = transform.value;
// Is it a getter?
if (V.isUndefined(angle)) {
return transform.rotate;
}
transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim();
angle %= 360;
var newAngle = opt.absolute ? angle : transform.rotate.angle + angle;
var newOrigin = (cx !== undefined && cy !== undefined) ? ',' + cx + ',' + cy : '';
var newRotate = 'rotate(' + newAngle + newOrigin + ')';
this.attr('transform', (transformAttr + ' ' + newRotate).trim());
return this;
};
// Note that `scale` as the only transformation does not combine with previous values.
V.prototype.scale = function(sx, sy) {
sy = V.isUndefined(sy) ? sx : sy;
var transformAttr = this.attr('transform') || '';
var transform = V.parseTransformString(transformAttr);
transformAttr = transform.value;
// Is it a getter?
if (V.isUndefined(sx)) {
return transform.scale;
}
transformAttr = transformAttr.replace(/scale\([^\)]*\)/g, '').trim();
var newScale = 'scale(' + sx + ',' + sy + ')';
this.attr('transform', (transformAttr + ' ' + newScale).trim());
return this;
};
// Get SVGRect that contains coordinates and dimension of the real bounding box,
// i.e. after transformations are applied.
// If `target` is specified, bounding box will be computed relatively to `target` element.
V.prototype.bbox = function(withoutTransformations, target) {
var box;
var node = this.node;
var ownerSVGElement = node.ownerSVGElement;
// If the element is not in the live DOM, it does not have a bounding box defined and
// so fall back to 'zero' dimension element.
if (!ownerSVGElement) {
return g.Rect(0, 0, 0, 0);
}
try {
box = node.getBBox();
} catch (e) {
// Fallback for IE.
box = {
x: node.clientLeft,
y: node.clientTop,
width: node.clientWidth,
height: node.clientHeight
};
}
if (withoutTransformations) {
return g.Rect(box);
}
var matrix = this.getTransformToElement(target || ownerSVGElement);
return V.transformRect(box, matrix);
};
V.prototype.text = function(content, opt) {
// Replace all spaces with the Unicode No-break space (http://www.fileformat.info/info/unicode/char/a0/index.htm).
// IE would otherwise collapse all spaces into one.
content = V.sanitizeText(content);
opt = opt || {};
var lines = content.split('\n');
var tspan;
// `alignment-baseline` does not work in Firefox.
// Setting `dominant-baseline` on the `` element doesn't work in IE9.
// In order to have the 0,0 coordinate of the `` element (or the first ``)
// in the top left corner we translate the `` element by `0.8em`.
// See `http://www.w3.org/Graphics/SVG/WG/wiki/How_to_determine_dominant_baseline`.
// See also `http://apike.ca/prog_svg_text_style.html`.
var y = this.attr('y');
if (!y) {
this.attr('y', '0.8em');
}
// An empty text gets rendered into the DOM in webkit-based browsers.
// In order to unify this behaviour across all browsers
// we rather hide the text element when it's empty.
if (content) {
this.removeAttr('display');
} else {
this.attr('display', 'none');
}
// Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one.
this.attr('xml:space', 'preserve');
// Easy way to erase all `` children;
this.node.textContent = '';
var textNode = this.node;
if (opt.textPath) {
// Wrap the text in the SVG element that points
// to a path defined by `opt.textPath` inside the internal `` element.
var defs = this.find('defs');
if (defs.length === 0) {
defs = V('defs');
this.append(defs);
}
// If `opt.textPath` is a plain string, consider it to be directly the
// SVG path data for the text to go along (this is a shortcut).
// Otherwise if it is an object and contains the `d` property, then this is our path.
var d = Object(opt.textPath) === opt.textPath ? opt.textPath.d : opt.textPath;
if (d) {
var path = V('path', { d: d });
defs.append(path);
}
var textPath = V('textPath');
// Set attributes on the ``. The most important one
// is the `xlink:href` that points to our newly created `` element in ``.
// Note that we also allow the following construct:
// `t.text('my text', { textPath: { 'xlink:href': '#my-other-path' } })`.
// In other words, one can completely skip the auto-creation of the path
// and use any other arbitrary path that is in the document.
if (!opt.textPath['xlink:href'] && path) {
textPath.attr('xlink:href', '#' + path.node.id);
}
if (Object(opt.textPath) === opt.textPath) {
textPath.attr(opt.textPath);
}
this.append(textPath);
// Now all the ``s will be inside the ``.
textNode = textPath.node;
}
var offset = 0;
var x = this.attr('x') || 0;
// Shift all the but first by one line (`1em`)
var lineHeight = opt.lineHeight || '1em';
if (opt.lineHeight === 'auto') {
lineHeight = '1.5em';
}
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var vLine = V('tspan', { 'class': 'v-line', dy: (i == 0 ? '0em' : lineHeight), x: x });
if (line) {
if (opt.annotations) {
// Get the line height based on the biggest font size in the annotations for this line.
var maxFontSize = 0;
// Find the *compacted* annotations for this line.
var lineAnnotations = V.annotateString(lines[i], V.isArray(opt.annotations) ? opt.annotations : [opt.annotations], { offset: -offset, includeAnnotationIndices: opt.includeAnnotationIndices });
for (var j = 0; j < lineAnnotations.length; j++) {
var annotation = lineAnnotations[j];
if (V.isObject(annotation)) {
var fontSize = parseInt(annotation.attrs['font-size'], 10);
if (fontSize && fontSize > maxFontSize) {
maxFontSize = fontSize;
}
tspan = V('tspan', annotation.attrs);
if (opt.includeAnnotationIndices) {
// If `opt.includeAnnotationIndices` is `true`,
// set the list of indices of all the applied annotations
// in the `annotations` attribute. This list is a comma
// separated list of indices.
tspan.attr('annotations', annotation.annotations);
}
if (annotation.attrs['class']) {
tspan.addClass(annotation.attrs['class']);
}
tspan.node.textContent = annotation.t;
} else {
tspan = document.createTextNode(annotation || ' ');
}
vLine.append(tspan);
}
if (opt.lineHeight === 'auto' && maxFontSize && i !== 0) {
vLine.attr('dy', (maxFontSize * 1.2) + 'px');
}
} else {
vLine.node.textContent = line;
}
} else {
// Make sure the textContent is never empty. If it is, add a dummy
// character and make it invisible, making the following lines correctly
// relatively positioned. `dy=1em` won't work with empty lines otherwise.
vLine.addClass('v-empty-line');
// 'opacity' needs to be specified with fill, stroke. Opacity without specification
// is not applied in Firefox
vLine.node.style.fillOpacity = 0;
vLine.node.style.strokeOpacity = 0;
vLine.node.textContent = '-';
}
V(textNode).append(vLine);
offset += line.length + 1; // + 1 = newline character.
}
return this;
};
/**
* @public
* @param {string} name
* @returns {Vectorizer}
*/
V.prototype.removeAttr = function(name) {
var qualifiedName = V.qualifyAttr(name);
var el = this.node;
if (qualifiedName.ns) {
if (el.hasAttributeNS(qualifiedName.ns, qualifiedName.local)) {
el.removeAttributeNS(qualifiedName.ns, qualifiedName.local);
}
} else if (el.hasAttribute(name)) {
el.removeAttribute(name);
}
return this;
};
V.prototype.attr = function(name, value) {
if (V.isUndefined(name)) {
// Return all attributes.
var attributes = this.node.attributes;
var attrs = {};
for (var i = 0; i < attributes.length; i++) {
attrs[attributes[i].name] = attributes[i].value;
}
return attrs;
}
if (V.isString(name) && V.isUndefined(value)) {
return this.node.getAttribute(name);
}
if (typeof name === 'object') {
for (var attrName in name) {
if (name.hasOwnProperty(attrName)) {
this.setAttribute(attrName, name[attrName]);
}
}
} else {
this.setAttribute(name, value);
}
return this;
};
V.prototype.remove = function() {
if (this.node.parentNode) {
this.node.parentNode.removeChild(this.node);
}
return this;
};
V.prototype.empty = function() {
while (this.node.firstChild) {
this.node.removeChild(this.node.firstChild);
}
return this;
};
V.prototype.setAttributes = function(attrs) {
for (var key in attrs) {
if (attrs.hasOwnProperty(key)) {
this.setAttribute(key, attrs[key]);
}
}
return this;
};
V.prototype.append = function(els) {
if (!V.isArray(els)) {
els = [els];
}
for (var i = 0, len = els.length; i < len; i++) {
this.node.appendChild(V.toNode(els[i]));
}
return this;
};
V.prototype.prepend = function(els) {
var child = this.node.firstChild;
return child ? V(child).before(els) : this.append(els);
};
V.prototype.before = function(els) {
var node = this.node;
var parent = node.parentNode;
if (parent) {
if (!V.isArray(els)) {
els = [els];
}
for (var i = 0, len = els.length; i < len; i++) {
parent.insertBefore(V.toNode(els[i]), node);
}
}
return this;
};
V.prototype.appendTo = function(node) {
V.toNode(node).appendChild(this.node);
return this;
},
V.prototype.svg = function() {
return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement);
};
V.prototype.defs = function() {
var defs = this.svg().node.getElementsByTagName('defs');
return (defs && defs.length) ? V(defs[0]) : undefined;
};
V.prototype.clone = function() {
var clone = V(this.node.cloneNode(true/* deep */));
// Note that clone inherits also ID. Therefore, we need to change it here.
clone.node.id = V.uniqueId();
return clone;
};
V.prototype.findOne = function(selector) {
var found = this.node.querySelector(selector);
return found ? V(found) : undefined;
};
V.prototype.find = function(selector) {
var vels = [];
var nodes = this.node.querySelectorAll(selector);
if (nodes) {
// Map DOM elements to `V`s.
for (var i = 0; i < nodes.length; i++) {
vels.push(V(nodes[i]));
}
}
return vels;
};
// Find an index of an element inside its container.
V.prototype.index = function() {
var index = 0;
var node = this.node.previousSibling;
while (node) {
// nodeType 1 for ELEMENT_NODE
if (node.nodeType === 1) index++;
node = node.previousSibling;
}
return index;
};
V.prototype.findParentByClass = function(className, terminator) {
var ownerSVGElement = this.node.ownerSVGElement;
var node = this.node.parentNode;
while (node && node !== terminator && node !== ownerSVGElement) {
var vel = V(node);
if (vel.hasClass(className)) {
return vel;
}
node = node.parentNode;
}
return null;
};
// https://jsperf.com/get-common-parent
V.prototype.contains = function(el) {
var a = this.node;
var b = V.toNode(el);
var bup = b && b.parentNode;
return (a === bup) || !!(bup && bup.nodeType === 1 && (a.compareDocumentPosition(bup) & 16));
};
// Convert global point into the coordinate space of this element.
V.prototype.toLocalPoint = function(x, y) {
var svg = this.svg().node;
var p = svg.createSVGPoint();
p.x = x;
p.y = y;
try {
var globalPoint = p.matrixTransform(svg.getScreenCTM().inverse());
var globalToLocalMatrix = this.getTransformToElement(svg).inverse();
} catch (e) {
// IE9 throws an exception in odd cases. (`Unexpected call to method or property access`)
// We have to make do with the original coordianates.
return p;
}
return globalPoint.matrixTransform(globalToLocalMatrix);
};
V.prototype.translateCenterToPoint = function(p) {
var bbox = this.bbox();
var center = g.rect(bbox).center();
this.translate(p.x - center.x, p.y - center.y);
};
// Efficiently auto-orient an element. This basically implements the orient=auto attribute
// of markers. The easiest way of understanding on what this does is to imagine the element is an
// arrowhead. Calling this method on the arrowhead makes it point to the `position` point while
// being auto-oriented (properly rotated) towards the `reference` point.
// `target` is the element relative to which the transformations are applied. Usually a viewport.
V.prototype.translateAndAutoOrient = function(position, reference, target) {
// Clean-up previously set transformations except the scale. If we didn't clean up the
// previous transformations then they'd add up with the old ones. Scale is an exception as
// it doesn't add up, consider: `this.scale(2).scale(2).scale(2)`. The result is that the
// element is scaled by the factor 2, not 8.
var s = this.scale();
this.attr('transform', '');
this.scale(s.sx, s.sy);
var svg = this.svg().node;
var bbox = this.bbox(false, target);
// 1. Translate to origin.
var translateToOrigin = svg.createSVGTransform();
translateToOrigin.setTranslate(-bbox.x - bbox.width / 2, -bbox.y - bbox.height / 2);
// 2. Rotate around origin.
var rotateAroundOrigin = svg.createSVGTransform();
var angle = g.point(position).changeInAngle(position.x - reference.x, position.y - reference.y, reference);
rotateAroundOrigin.setRotate(angle, 0, 0);
// 3. Translate to the `position` + the offset (half my width) towards the `reference` point.
var translateFinal = svg.createSVGTransform();
var finalPosition = g.point(position).move(reference, bbox.width / 2);
translateFinal.setTranslate(position.x + (position.x - finalPosition.x), position.y + (position.y - finalPosition.y));
// 4. Apply transformations.
var ctm = this.getTransformToElement(target);
var transform = svg.createSVGTransform();
transform.setMatrix(
translateFinal.matrix.multiply(
rotateAroundOrigin.matrix.multiply(
translateToOrigin.matrix.multiply(
ctm)))
);
// Instead of directly setting the `matrix()` transform on the element, first, decompose
// the matrix into separate transforms. This allows us to use normal Vectorizer methods
// as they don't work on matrices. An example of this is to retrieve a scale of an element.
// this.node.transform.baseVal.initialize(transform);
var decomposition = V.decomposeMatrix(transform.matrix);
this.translate(decomposition.translateX, decomposition.translateY);
this.rotate(decomposition.rotation);
// Note that scale has been already applied, hence the following line stays commented. (it's here just for reference).
//this.scale(decomposition.scaleX, decomposition.scaleY);
return this;
};
V.prototype.animateAlongPath = function(attrs, path) {
path = V.toNode(path);
var id = V.ensureId(path);
var animateMotion = V('animateMotion', attrs);
var mpath = V('mpath', { 'xlink:href': '#' + id });
animateMotion.append(mpath);
this.append(animateMotion);
try {
animateMotion.node.beginElement();
} catch (e) {
// Fallback for IE 9.
// Run the animation programatically if FakeSmile (`http://leunen.me/fakesmile/`) present
if (document.documentElement.getAttribute('smiling') === 'fake') {
// Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`)
var animation = animateMotion.node;
animation.animators = [];
var animationID = animation.getAttribute('id');
if (animationID) id2anim[animationID] = animation;
var targets = getTargets(animation);
for (var i = 0, len = targets.length; i < len; i++) {
var target = targets[i];
var animator = new Animator(animation, target, i);
animators.push(animator);
animation.animators[i] = animator;
animator.register();
}
}
}
};
V.prototype.hasClass = function(className) {
return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class'));
};
V.prototype.addClass = function(className) {
if (!this.hasClass(className)) {
var prevClasses = this.node.getAttribute('class') || '';
this.node.setAttribute('class', (prevClasses + ' ' + className).trim());
}
return this;
};
V.prototype.removeClass = function(className) {
if (this.hasClass(className)) {
var newClasses = this.node.getAttribute('class').replace(new RegExp('(\\s|^)' + className + '(\\s|$)', 'g'), '$2');
this.node.setAttribute('class', newClasses);
}
return this;
};
V.prototype.toggleClass = function(className, toAdd) {
var toRemove = V.isUndefined(toAdd) ? this.hasClass(className) : !toAdd;
if (toRemove) {
this.removeClass(className);
} else {
this.addClass(className);
}
return this;
};
// Interpolate path by discrete points. The precision of the sampling
// is controlled by `interval`. In other words, `sample()` will generate
// a point on the path starting at the beginning of the path going to the end
// every `interval` pixels.
// The sampler can be very useful for e.g. finding intersection between two
// paths (finding the two closest points from two samples).
V.prototype.sample = function(interval) {
interval = interval || 1;
var node = this.node;
var length = node.getTotalLength();
var samples = [];
var distance = 0;
var sample;
while (distance < length) {
sample = node.getPointAtLength(distance);
samples.push({ x: sample.x, y: sample.y, distance: distance });
distance += interval;
}
return samples;
};
V.prototype.convertToPath = function() {
var path = V('path');
path.attr(this.attr());
var d = this.convertToPathData();
if (d) {
path.attr('d', d);
}
return path;
};
V.prototype.convertToPathData = function() {
var tagName = this.node.tagName.toUpperCase();
switch (tagName) {
case 'PATH':
return this.attr('d');
case 'LINE':
return V.convertLineToPathData(this.node);
case 'POLYGON':
return V.convertPolygonToPathData(this.node);
case 'POLYLINE':
return V.convertPolylineToPathData(this.node);
case 'ELLIPSE':
return V.convertEllipseToPathData(this.node);
case 'CIRCLE':
return V.convertCircleToPathData(this.node);
case 'RECT':
return V.convertRectToPathData(this.node);
}
throw new Error(tagName + ' cannot be converted to PATH.');
};
// Find the intersection of a line starting in the center
// of the SVG `node` ending in the point `ref`.
// `target` is an SVG element to which `node`s transformations are relative to.
// In JointJS, `target` is the `paper.viewport` SVG group element.
// Note that `ref` point must be in the coordinate system of the `target` for this function to work properly.
// Returns a point in the `target` coordinte system (the same system as `ref` is in) if
// an intersection is found. Returns `undefined` otherwise.
V.prototype.findIntersection = function(ref, target) {
var svg = this.svg().node;
target = target || svg;
var bbox = g.rect(this.bbox(false, target));
var center = bbox.center();
if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined;
var spot;
var tagName = this.node.localName.toUpperCase();
// Little speed up optimalization for `` element. We do not do conversion
// to path element and sampling but directly calculate the intersection through
// a transformed geometrical rectangle.
if (tagName === 'RECT') {
var gRect = g.rect(
parseFloat(this.attr('x') || 0),
parseFloat(this.attr('y') || 0),
parseFloat(this.attr('width')),
parseFloat(this.attr('height'))
);
// Get the rect transformation matrix with regards to the SVG document.
var rectMatrix = this.getTransformToElement(target);
// Decompose the matrix to find the rotation angle.
var rectMatrixComponents = V.decomposeMatrix(rectMatrix);
// Now we want to rotate the rectangle back so that we
// can use `intersectionWithLineFromCenterToPoint()` passing the angle as the second argument.
var resetRotation = svg.createSVGTransform();
resetRotation.setRotate(-rectMatrixComponents.rotation, center.x, center.y);
var rect = V.transformRect(gRect, resetRotation.matrix.multiply(rectMatrix));
spot = g.rect(rect).intersectionWithLineFromCenterToPoint(ref, rectMatrixComponents.rotation);
} else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') {
var pathNode = (tagName === 'PATH') ? this : this.convertToPath();
var samples = pathNode.sample();
var minDistance = Infinity;
var closestSamples = [];
var i, sample, gp, centerDistance, refDistance, distance;
for (i = 0; i < samples.length; i++) {
sample = samples[i];
// Convert the sample point in the local coordinate system to the global coordinate system.
gp = V.createSVGPoint(sample.x, sample.y);
gp = gp.matrixTransform(this.getTransformToElement(target));
sample = g.point(gp);
centerDistance = sample.distance(center);
// Penalize a higher distance to the reference point by 10%.
// This gives better results. This is due to
// inaccuracies introduced by rounding errors and getPointAtLength() returns.
refDistance = sample.distance(ref) * 1.1;
distance = centerDistance + refDistance;
if (distance < minDistance) {
minDistance = distance;
closestSamples = [{ sample: sample, refDistance: refDistance }];
} else if (distance < minDistance + 1) {
closestSamples.push({ sample: sample, refDistance: refDistance });
}
}
closestSamples.sort(function(a, b) {
return a.refDistance - b.refDistance;
});
if (closestSamples[0]) {
spot = closestSamples[0].sample;
}
}
return spot;
};
/**
* @private
* @param {string} name
* @param {string} value
* @returns {Vectorizer}
*/
V.prototype.setAttribute = function(name, value) {
var el = this.node;
if (value === null) {
this.removeAttr(name);
return this;
}
var qualifiedName = V.qualifyAttr(name);
if (qualifiedName.ns) {
// Attribute names can be namespaced. E.g. `image` elements
// have a `xlink:href` attribute to set the source of the image.
el.setAttributeNS(qualifiedName.ns, name, value);
} else if (name === 'id') {
el.id = value;
} else {
el.setAttribute(name, value);
}
return this;
};
// Create an SVG document element.
// If `content` is passed, it will be used as the SVG content of the `