/*! JointJS v0.9.7 - JavaScript diagramming library 2016-04-20
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) {
// AMD. Register as an anonymous module.
define(['g'], function(g) {
return factory(g);
});
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
var g = require('./geometry');
module.exports = factory(g);
} else {
// Browser globals.
var g = root.g;
root.Vectorizer = root.V = factory(g);
}
}(this, function(g) {
// Vectorizer.
// -----------
// A tiny library for making your life easier when dealing with SVG.
// The only Vectorizer dependency is the Geometry library.
// Copyright © 2012 - 2015 client IO (http://client.io)
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',
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);
}
}
this.node = el;
if (!this.node.id) {
this.node.id = V.uniqueId();
}
this.setAttributes(attrs);
if (children) {
this.append(children);
}
return this;
};
/**
* @param {SVGGElement} toElem
* @returns {SVGMatrix}
*/
V.prototype.getTransformToElement = function(toElem) {
return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM());
};
/**
* @param {SVGMatrix} matrix
* @returns {V, SVGMatrix} Setter / Getter
*/
V.prototype.transform = function(matrix) {
if (V.isUndefined(matrix)) {
return (this.node.parentNode)
? this.getTransformToElement(this.node.parentNode)
: this.node.getScreenCTM();
}
var svgTransform = V.createSVGTransform(matrix);
this.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);
// 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);
// 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);
// 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) {
// 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 (!this.node.ownerSVGElement) return { x: 0, y: 0, width: 0, height: 0 };
var box;
try {
box = this.node.getBBox();
// We are creating a new object as the standard says that you can't
// modify the attributes of a bbox.
box = { x: box.x, y: box.y, width: box.width, height: box.height };
} catch (e) {
// Fallback for IE.
box = {
x: this.node.clientLeft,
y: this.node.clientTop,
width: this.node.clientWidth,
height: this.node.clientHeight
};
}
if (withoutTransformations) {
return box;
}
var matrix = this.getTransformToElement(target || this.node.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 i = 0;
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.
this.attr('display', content ? null : 'none');
// Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one.
this.node.setAttributeNS('http://www.w3.org/XML/1998/namespace', '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;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
// Shift all the but first by one line (`1em`)
var lineHeight = opt.lineHeight || '1em';
if (opt.lineHeight === 'auto') {
lineHeight = '1.5em';
}
var vLine = V('tspan', { dy: (i == 0 ? '0em' : lineHeight), x: this.attr('x') || 0 });
vLine.addClass('v-line');
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');
vLine.node.style.opacity = 0;
vLine.node.textContent = '-';
}
V(textNode).append(vLine);
offset += line.length + 1; // + 1 = newline character.
}
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].nodeName] = attributes[i].nodeValue;
}
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)) {
V.setAttribute(this.node, attrName, name[attrName]);
}
}
} else {
V.setAttribute(this.node, 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) {
var key;
for (key in attrs) {
V.setAttribute(this.node, key, attrs[key]);
}
return this;
};
V.prototype.append = function(els) {
if (!V.isArray(els)) {
els = [els];
}
var i, len, el;
for (i = 0, len = els.length; i < len; i++) {
el = els[i];
this.node.appendChild(V.isV(el) ? el.node : (el.nodeName && el || el[0]));
}
return this;
};
V.prototype.prepend = function(el) {
this.node.insertBefore(V.isV(el) ? el.node : el, this.node.firstChild);
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;
};
// 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) {
var animateMotion = V('animateMotion', attrs);
var mpath = V('mpath', { 'xlink:href': '#' + V(path).node.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;
};
// Create an SVG document element.
// If `content` is passed, it will be used as the SVG content of the `